diff --git a/CMakeLists.txt b/CMakeLists.txt index 984c6453b..51ebf94be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,11 @@ option(OPENTS_EXPERIMENTAL_CLANG_CL "Build with clang-cl using the MSVC ABI" OFF option(OPENTS_EXPERIMENTAL_X64 "Configure an unsupported 64-bit Windows build" OFF) option(OPENTS_EXPERIMENTAL_NATIVE "Configure a native build for the host platform" OFF) +# Lets a target's FOLDER place it in a Visual Studio solution folder. The bgfx submodule sets +# this as well, so declare it here rather than inherit it from a vendored build. +set_property(GLOBAL PROPERTY USE_FOLDERS ON) + + if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") if(NOT OPENTS_EXPERIMENTAL_CLANG_CL) @@ -59,6 +64,17 @@ else() "-DOPENTS_EXPERIMENTAL_NATIVE=ON.") endif() +# Only MSVC names the target architecture in a compiler variable, so the native and +# cross builds fall back on the system they are configured for. +string(TOLOWER "${CMAKE_CXX_COMPILER_ARCHITECTURE_ID}" OPENTS_ARCH) +if(NOT OPENTS_ARCH) + string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" OPENTS_ARCH) +endif() +if(NOT OPENTS_ARCH) + set(OPENTS_ARCH "unknown") +endif() + +# Only Windows carries resource scripts, and no other toolchain supplies a compiler for them. if(WIN32) enable_language(RC) endif() @@ -91,6 +107,7 @@ set(OPENTS_STAMP_ARGS "-DOPENTS_SOURCE_DIR=${CMAKE_SOURCE_DIR}" "-DOPENTS_VERSION_HEADER=${OPENTS_VERSION_HEADER}" "-DOPENTS_STAMP_HEADER=${OPENTS_STAMP_HEADER}" + "-DOPENTS_ARCH=${OPENTS_ARCH}" -P "${CMAKE_SOURCE_DIR}/cmake/GitStamp.cmake" ) diff --git a/README.md b/README.md index ea1dc0fb7..69af7d9b9 100644 --- a/README.md +++ b/README.md @@ -69,13 +69,13 @@ endorsed by Electronic Arts. 2. Extract the release zip into the Tiberian Sun game directory. 3. Run `Game.exe`. +OpenTS supports Windows 10 version 1903 (build 18362) and newer. Earlier +Windows versions are untested and unsupported. Wine may work, but there is no +supported native Linux build. + OpenTS supplies the engine, not the game data: the installation above provides the original assets. There is no installer, and no extra runtime -library or launch argument is required. Windows is supported; Wine may work, -but there is no supported native Linux build. The engine asks Windows for the -UTF-8 code page, which needs Windows 10 version 1903 or newer. Older Windows -keeps its own code page, so game text still shows, but a path or file name -holding a character that code page lacks may fail. +library or launch argument is required. ## Running on macOS diff --git a/cmake/GitStamp.cmake b/cmake/GitStamp.cmake index 3438dc44d..b13c7aaff 100644 --- a/cmake/GitStamp.cmake +++ b/cmake/GitStamp.cmake @@ -71,6 +71,8 @@ set(VERSION_CONTENT #else #define OPENTS_RC_PRERELEASE_FLAG 0x0L #endif + +#define OPENTS_ARCH \"${OPENTS_ARCH}\" ") set(OPENTS_COMMIT "unknown") @@ -173,13 +175,13 @@ if(OPENTS_COMMIT_DIRTY) endif() if(OPENTS_OFFICIAL_BUILD) - set(OPENTS_VERSION_DISPLAY "${OPENTS_VERSION}") + set(OPENTS_VERSION_DISPLAY "${OPENTS_VERSION} (${OPENTS_ARCH})") elseif(OPENTS_COMMIT STREQUAL "unknown") - set(OPENTS_VERSION_DISPLAY "${OPENTS_VERSION}") + set(OPENTS_VERSION_DISPLAY "${OPENTS_VERSION} (${OPENTS_ARCH})") elseif(OPENTS_COMMIT_DIRTY) - set(OPENTS_VERSION_DISPLAY "${OPENTS_VERSION} (${OPENTS_COMMIT}, modified)") + set(OPENTS_VERSION_DISPLAY "${OPENTS_VERSION} (${OPENTS_COMMIT}, modified, ${OPENTS_ARCH})") else() - set(OPENTS_VERSION_DISPLAY "${OPENTS_VERSION} (${OPENTS_COMMIT})") + set(OPENTS_VERSION_DISPLAY "${OPENTS_VERSION} (${OPENTS_COMMIT}, ${OPENTS_ARCH})") endif() set(STAMP_CONTENT diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index 04df86923..9ba1d699c 100644 --- a/code/CMakeLists.txt +++ b/code/CMakeLists.txt @@ -316,7 +316,7 @@ if(WIN32) winmm ws2_32 kernel32 user32 gdi32 winspool comdlg32 advapi32 shell32 - ole32 oleaut32 uuid odbc32 odbccp32 + odbc32 odbccp32 ) endif() diff --git a/code/_astar.h b/code/_astar.h index 805dae938..6d549c0ea 100644 --- a/code/_astar.h +++ b/code/_astar.h @@ -17,6 +17,13 @@ class AStarClass; +/* + * The most cells a path may run to. A buffer handed to Find_Path holds one more, because a + * finished path of length L is padded out to entry L. + */ +inline constexpr int PATH_LENGTH_MAX = 2000; + + /************************************************************************** ** Find_Path returns with a pointer to this structure. */ diff --git a/code/addon.cpp b/code/addon.cpp index c795d0ad5..16e954b3a 100644 --- a/code/addon.cpp +++ b/code/addon.cpp @@ -11,8 +11,10 @@ #include "addon.h" +#include "_deploymentconfig.h" #include "ccfile.h" #include "data.h" +#include "deploymentconfig.h" #include "init.h" #include "language/language.h" #include "ui/uigametype.h" @@ -82,7 +84,7 @@ void Detect_Addons(void) AvailableAddOns = (1 << ADDON_BASE_GAME); ActiveAddOns = (1 << ADDON_BASE_GAME); - if (CCFileClass("FIRESTRM.INI").Is_Available() == true) { + if (CCFileClass(DeploymentConfig.RulesExpansionFile.c_str()).Is_Available() == true) { AvailableAddOns |= (1 << ADDON_FIRESTORM); } } diff --git a/code/animfile.h b/code/animfile.h index f684e4a6d..8ab0f1144 100644 --- a/code/animfile.h +++ b/code/animfile.h @@ -108,6 +108,7 @@ class AnimFile : public Animate unsigned short framesPerSecond; /// Number of frames to play per second. unsigned short pad2[29]; /// 58 bytes of filler to round up to 128 bytes total. }; + static_assert(sizeof(LPFHeader) == 128, "the LPF header is 128 bytes on disk"); struct LPDescriptor { unsigned short baseRecord; /// Number of first record in this large page. @@ -116,6 +117,7 @@ class AnimFile : public Animate /// bit 14 of "nRecords" == "final record continues on next lp". unsigned short nBytes; /// Total number of bytes of contents, excluding header. }; + static_assert(sizeof(LPDescriptor) == 6, "a large page descriptor is 6 bytes on disk"); /* * Structure of a single Large Page in an anim file. @@ -134,6 +136,7 @@ class AnimFile : public Animate unsigned short RecordSizes[1]; /// [nRecords] Array of lengths of each record in the large page. }; + static_assert(sizeof(LPStruct) == 10, "a large page header is 8 bytes on disk, and the struct carries the first of the record sizes that follow it"); /* * These are the color cycling ranges Deluxe Paint stores after the header, each diff --git a/code/astar.cpp b/code/astar.cpp index 1d914447f..535e7e76b 100644 --- a/code/astar.cpp +++ b/code/astar.cpp @@ -60,7 +60,7 @@ /* Define a couple of variables which are private to the module they are */ /* declared in. */ /*=========================================================================*/ -static unsigned int CellHeights[2000]; +static unsigned int CellHeights[PATH_LENGTH_MAX + 1]; unsigned int MapCellStride; @@ -312,7 +312,7 @@ PathStruct * AStarClass::Find_Path_Regular(Cell const & from, Cell const & to, F HierNodeIndex = 0; HierLastNodeCell = from; int * fine_final_ids = HierOnPath[0]; - RegularOpenNode * working_node = Create_Node(0, from_pptr, to, 0.0); + std::optional working_node = Create_Node(std::nullopt, from_pptr, to, 0.0); if (from == to && CurrentCellHeight == DestCellHeight) { return(NULL); @@ -373,7 +373,7 @@ PathStruct * AStarClass::Find_Path_Regular(Cell const & from, Cell const & to, F break; } Cell & from_id = working_from[0]->CellID; - RegularOpenNode * temp_node = NULL; + std::optional temp_node; int from_index = from_id.X + MapCellStride * from_id.Y; for (face = FACING_FIRST; face <= FACING_COUNT; face++) { @@ -412,7 +412,7 @@ PathStruct * AStarClass::Find_Path_Regular(Cell const & from, Cell const & to, F int zone_index = Map.Get_Cell_Zone_Index(neighbor_id); CellSubzoneStruct * subzones = Map.CellSubzones; - short subzone_id = subzones[zone_index].SubzoneID[SUBZONE_FINE]; + int subzone_id = subzones[zone_index].SubzoneID[SUBZONE_FINE]; if (fine_final_ids[subzone_id] != UniqueID && base_level && !neighbor_cell->AdjacentObjectCount && with_hs) { continue; } @@ -445,24 +445,42 @@ PathStruct * AStarClass::Find_Path_Regular(Cell const & from, Cell const & to, F continue; } - RegularOpenNode * new_node = Create_Node(working_node, working_to, to, movement_cost); - if (temp_node == NULL) { + /* + * The pool is handed out and never released within a pass, and nothing else + * bounds how many cells a pass may reach. Passing the cell over costs the + * search a route. Writing past the pool would land on the count of what it + * holds. + */ + if (RegularNodes->ActiveCount >= ARRAY_SIZE(RegularNodes->Nodes)) { + continue; + } + + /* + * The destination is tested before a node is expanded, so a node at the + * limit can still finish a path; it just cannot extend one. + */ + if (working_node->PathLength >= PATH_LENGTH_MAX) { + continue; + } + + RegularOpenNode new_node = Create_Node(working_node, working_to, to, movement_cost); + if (!temp_node) { temp_node = new_node; } else { - if (new_node->Score < temp_node->Score) { + if (new_node.Score < temp_node->Score) { RegularQueue->Insert(*temp_node); temp_node = new_node; } else { - RegularQueue->Insert(*new_node); + RegularQueue->Insert(new_node); } } if (base_level) { RegularVisited[node_index] = UniqueID; - RegularMovementCosts[node_index] = new_node->MovementCost; + RegularMovementCosts[node_index] = new_node.MovementCost; } else { RegularBridgeVisited[node_index] = UniqueID; - RegularBridgeMovementCosts[node_index] = new_node->MovementCost; + RegularBridgeMovementCosts[node_index] = new_node.MovementCost; } if (subzone_id == HierSubzonePath[SUBZONE_FINE][HierNodeIndex + 1]) { HierNodeIndex++; @@ -489,14 +507,14 @@ PathStruct * AStarClass::Find_Path_Regular(Cell const & from, Cell const & to, F breakout: - if (tries == 10000 || working_node == NULL || tries == max_loops || working_node->PathLength < 2) { + if (tries == 10000 || !working_node || tries == max_loops || working_node->PathLength < 2) { if (Avoidance != AVOIDANCE_NONE) { Apply_Path_Collision_Avoidance(foot); } return(NULL); } - PathStruct * final_path = Build_Final_Path(working_node, moves); + PathStruct * final_path = Build_Final_Path(*working_node, moves); Cut_Corners(final_path, foot); /* @@ -522,14 +540,14 @@ PathStruct * AStarClass::Find_Path_Regular(Cell const & from, Cell const & to, F /// The map array slot of the cell the node stands for. /// The destination cell, used to estimate the remaining cost. /// The cost of the step from the parent into this cell. -/// Returns with a pointer to the new node, taken from the search's node pool. -AStarClass::RegularOpenNode * AStarClass::Create_Node(RegularOpenNode * parent, CellClass ** cell, Cell const & to, float movement_cost) +/// Returns with the new open set entry, naming a cell record taken from the pool. +AStarClass::RegularOpenNode AStarClass::Create_Node(std::optional const & parent, CellClass ** cell, Cell const & to, float movement_cost) { - RegularOpenNode * open_node = &RegularOpenNodes->Nodes[RegularOpenNodes->ActiveCount++]; + RegularOpenNode open_node; RegularNode * node_data = &RegularNodes->Nodes[RegularNodes->ActiveCount++]; node_data->CellSlot = cell; - if (parent != NULL) { + if (parent) { node_data->Parent = parent->Node; CellClass * current_cell = *node_data->CellSlot; CellClass * parent_cell = *parent->Node->CellSlot; @@ -547,20 +565,20 @@ AStarClass::RegularOpenNode * AStarClass::Create_Node(RegularOpenNode * parent, node_data->CellHeight = CurrentCellHeight; } - open_node->Node = node_data; - if (parent != NULL) { - open_node->MovementCost = movement_cost + parent->MovementCost; - open_node->PathLength = parent->PathLength + 1; + open_node.Node = node_data; + if (parent) { + open_node.MovementCost = movement_cost + parent->MovementCost; + open_node.PathLength = parent->PathLength + 1; } else { - open_node->MovementCost = 0.0; - open_node->PathLength = 1; + open_node.MovementCost = 0.0; + open_node.PathLength = 1; } Cell & cell_id = (*cell)->CellID; int dx = abs(cell_id.X - to.X); int dy = abs(cell_id.Y - to.Y); - open_node->Score = open_node->MovementCost + std::sqrt(dx * dx + dy * dy); + open_node.Score = open_node.MovementCost + std::sqrt(dx * dx + dy * dy); return(open_node); } @@ -575,7 +593,6 @@ void AStarClass::Clear(void) { int i; RegularNodes->ActiveCount = 0; - RegularOpenNodes->ActiveCount = 0; RegularQueue->Clear(); HierQueue->Clear(); @@ -654,11 +671,9 @@ AStarClass::AStarClass(void) : { RegularQueue = new PriorityQueueClass(65536); HierQueue = new PriorityQueueClass(10000); - RegularOpenNodes = new RegularOpenNodePool; RegularNodes = new RegularNodePool; RegularNodes->ActiveCount = 0; - RegularOpenNodes->ActiveCount = 0; RegularQueue->Clear(); HierQueue->Clear(); @@ -666,11 +681,11 @@ AStarClass::AStarClass(void) : HierOnPath[i] = NULL; HierOpened[i] = NULL; HierCosts[i] = NULL; - HierBannedEdges[i].Clear(); + HierBannedEdges[i].clear(); memset(HierSubzonePath[i], 0, sizeof(HierSubzonePath[i])); HierSubzonePathCount[i] = 0; } - HierNodePool = new AStarHierarchicalNode[10000]; + HierNodePool.reserve(10000); } @@ -685,54 +700,48 @@ AStarClass::~AStarClass(void) delete HierQueue; HierQueue = NULL; - if (RegularOpenNodes != NULL) { - delete RegularOpenNodes; - } - RegularOpenNodes = NULL; - if (RegularNodes != NULL) { delete RegularNodes; } RegularNodes = NULL; if (RegularVisited != NULL) { - delete RegularVisited; + delete [] RegularVisited; RegularVisited = NULL; } if (RegularBridgeVisited != NULL) { - delete RegularBridgeVisited; + delete [] RegularBridgeVisited; RegularBridgeVisited = NULL; } if (RegularMovementCosts != NULL) { - delete RegularMovementCosts; + delete [] RegularMovementCosts; RegularMovementCosts = NULL; } if (RegularBridgeMovementCosts != NULL) { - delete RegularBridgeMovementCosts; + delete [] RegularBridgeMovementCosts; RegularBridgeMovementCosts = NULL; } for (int i = 0; i < 3; i++) { if (HierOnPath[i]) { - delete HierOnPath[i]; + delete [] HierOnPath[i]; HierOnPath[i] = NULL; } if (HierOpened[i]) { - delete HierOpened[i]; + delete [] HierOpened[i]; HierOpened[i] = NULL; } if (HierCosts[i]) { - delete HierCosts[i]; + delete [] HierCosts[i]; HierCosts[i] = NULL; } } - delete [] HierNodePool; } @@ -766,22 +775,22 @@ FacingType Facing_Between(Cell const & cell1, Cell const & cell2) /// Returns with a pointer to the completed path control structure. /// There is only one path control structure, so building a new path invalidates /// the last one this routine handed out. -PathStruct * AStarClass::Build_Final_Path(RegularOpenNode *final_node, FacingType *moves) +PathStruct * AStarClass::Build_Final_Path(RegularOpenNode const & final_node, FacingType * moves) { static PathStruct path; // Main path control. - path.Cost = int(final_node->Score); - path.Length = final_node->PathLength; + path.Cost = int(final_node.Score); + path.Length = final_node.PathLength; path.Overlap = 0; path.LastOverlap.X = 0; path.LastOverlap.Y = 0; path.Command = moves; path.Height = CellHeights; - RegularNode * current = final_node->Node; + RegularNode * current = final_node.Node; RegularNode * parent = current->Parent; - for (int i = (final_node->PathLength - 2); i >= 0; i--) { + for (int i = (final_node.PathLength - 2); i >= 0; i--) { if (parent != NULL) { CellHeights[i] = parent->CellHeight; moves[i] = Facing_Between((*current->CellSlot)->CellID, (*parent->CellSlot)->CellID); @@ -790,7 +799,7 @@ PathStruct * AStarClass::Build_Final_Path(RegularOpenNode *final_node, FacingTyp parent = parent->Parent; } - moves[final_node->PathLength - 1] = FACING_NONE; + moves[final_node.PathLength - 1] = FACING_NONE; path.Start = (*current->CellSlot)->CellID; @@ -812,28 +821,33 @@ PathStruct * AStarClass::Build_Final_Path(RegularOpenNode *final_node, FacingTyp void AStarClass::Update_Map_Dimensions(Rect const & dimensions) { if (RegularVisited != NULL) { - delete RegularVisited; + delete [] RegularVisited; RegularVisited = NULL; } if (RegularBridgeVisited != NULL) { - delete RegularBridgeVisited; + delete [] RegularBridgeVisited; RegularBridgeVisited = NULL; } if (RegularMovementCosts != NULL) { - delete RegularMovementCosts; + delete [] RegularMovementCosts; RegularMovementCosts = NULL; } if (RegularBridgeMovementCosts != NULL) { - delete RegularBridgeMovementCosts; + delete [] RegularBridgeMovementCosts; RegularBridgeMovementCosts = NULL; } MapCellStride = dimensions.Height + dimensions.Width + 1; int count = MapCellStride * MapCellStride; - RegularBridgeVisited = new int[count]; - RegularVisited = new int[count]; - RegularMovementCosts = new float[count]; - RegularBridgeMovementCosts = new float[count]; + + /* + * A search reads a cell's stamp before writing one, and only a wrap of UniqueID clears + * these tables, so they have to start at a value no search can own. + */ + RegularBridgeVisited = new int[count](); + RegularVisited = new int[count](); + RegularMovementCosts = new float[count](); + RegularBridgeMovementCosts = new float[count](); AStarFacingToOffset[FACING_N] = -MapCellStride; AStarFacingToOffset[FACING_NE] = 1 - MapCellStride; @@ -1604,6 +1618,7 @@ bool AStarClass::Find_Path_Hierarchical(Cell const & from, Cell const & to, MZon for (int subzone_level = SUBZONE_COARSE; subzone_level >= SUBZONE_FINE; subzone_level--) { HierQueue->Clear(); + HierNodePool.clear(); CellSubzoneStruct & start_cell_subzones = Map.CellSubzones[Map.Get_Cell_Zone_Index(from)]; int start_subzone = start_cell_subzones.SubzoneID[subzone_level]; @@ -1625,30 +1640,25 @@ bool AStarClass::Find_Path_Hierarchical(Cell const & from, Cell const & to, MZon final_ids[end_subzone] = UniqueID; if (start_subzone == end_subzone) { - if (subzone_level == SUBZONE_FINE) { - AStarHierarchicalNode * node = &HierNodePool[0]; - node->Depth = 0; - node->SubzoneID = start_subzone; - } HierSubzonePath[subzone_level][0] = start_subzone; HierSubzonePathCount[subzone_level] = 1; } else { - int node_count = 0; - AStarHierarchicalNode * start_node = &HierNodePool[0]; - start_node->ParentIndex = -1; - start_node->SubzoneID = start_subzone; - start_node->Score = 0.0; - start_node->Depth = 0; - - HierQueue->Insert(*start_node); - node_count++; + AStarHierarchicalNode start_node; + start_node.PoolIndex = 0; + start_node.ParentIndex = -1; + start_node.SubzoneID = start_subzone; + start_node.Score = 0.0; + start_node.Depth = 0; + + HierNodePool.push_back(start_node); + HierQueue->Insert(start_node); working_ids[start_subzone] = UniqueID; costs[start_subzone] = 0.0; - AStarHierarchicalNode * best_node = HierQueue->Extract_Min(); - bool no_banned_edges = HierBannedEdges[subzone_level].Count() == 0; + std::optional best_node = HierQueue->Extract_Min(); + bool no_banned_edges = HierBannedEdges[subzone_level].empty(); - while (best_node != NULL) { + while (best_node) { int from_subzone = best_node->SubzoneID; if (from_subzone == end_subzone) { break; @@ -1675,41 +1685,53 @@ bool AStarClass::Find_Path_Hierarchical(Cell const & from, Cell const & to, MZon if ((working_ids[to_subzone] != UniqueID || costs[to_subzone] > score) && (is_coarse || coarser_final_ids[to_coarser_subzone] == UniqueID || passability == PASSABLE_CRUSH) && pass_table[passability] == TRAVERSAL_PASSABLE) { if (no_banned_edges || !Subzone_Edge_Banned(from_subzone, to_subzone, subzone_level)) { - AStarHierarchicalNode * new_node = &HierNodePool[node_count]; - new_node->ParentIndex = best_node - HierNodePool; - new_node->SubzoneID = to_subzone; - new_node->Score = score; - new_node->Depth = best_node->Depth + 1; - HierQueue->Insert(*new_node); + AStarHierarchicalNode new_node; + new_node.PoolIndex = (int)HierNodePool.size(); + new_node.ParentIndex = best_node->PoolIndex; + new_node.SubzoneID = to_subzone; + new_node.Score = score; + new_node.Depth = best_node->Depth + 1; + HierNodePool.push_back(new_node); + HierQueue->Insert(new_node); working_ids[to_subzone] = UniqueID; costs[to_subzone] = score; - node_count++; } } } best_node = HierQueue->Extract_Min(); } - if (best_node == NULL) { + if (!best_node) { return(false); } - AStarHierarchicalNode * tail_node = best_node; - while (best_node->ParentIndex != -1) { - final_ids[best_node->SubzoneID] = UniqueID; - best_node = &HierNodePool[best_node->ParentIndex]; + /* + * The chain is written into a list of a fixed length at whatever length the + * block search settled on. Giving up on a corridor too long to record leaves + * the caller to search the map unrestricted. Writing it out would run over the + * lists for the coarser block sizes behind this one. + */ + if (best_node->Depth + 1 > ARRAY_SIZE(HierSubzonePath[subzone_level])) { + return(false); } - int path_count = tail_node->Depth + 1; + int trace = best_node->PoolIndex; + while (HierNodePool[trace].ParentIndex != -1) { + final_ids[HierNodePool[trace].SubzoneID] = UniqueID; + trace = HierNodePool[trace].ParentIndex; + } + + int tail_node = best_node->PoolIndex; + int path_count = HierNodePool[tail_node].Depth + 1; HierSubzonePathCount[subzone_level] = path_count; path_count--; while (path_count > 0) { - HierSubzonePath[subzone_level][path_count] = tail_node->SubzoneID; - tail_node = &HierNodePool[tail_node->ParentIndex]; + HierSubzonePath[subzone_level][path_count] = HierNodePool[tail_node].SubzoneID; + tail_node = HierNodePool[tail_node].ParentIndex; path_count--; } - HierSubzonePath[subzone_level][0] = tail_node->SubzoneID; + HierSubzonePath[subzone_level][0] = HierNodePool[tail_node].SubzoneID; } } return(true); @@ -1739,7 +1761,7 @@ PathStruct * AStarClass::Find_Path(Cell const & from, Cell const & to, FootClass Clear(); for (i = 0; i < ARRAY_SIZE(HierBannedEdges); i++) { - HierBannedEdges[i].Clear(); + HierBannedEdges[i].clear(); } Avoidance = avoidance; @@ -1857,13 +1879,13 @@ void AStarClass::Ban_Blocked_Subzone_Edges(FootClass const * foot) CellSubzoneStruct & subzone = Map.CellSubzones[Map.Get_Cell_Zone_Index(HierLastNodeCell)]; for (int subzone_level = 0; subzone_level < SUBZONE_COUNT; subzone_level++) { - unsigned short from_subzone = subzone.SubzoneID[subzone_level]; - DynamicVectorClass to_subzones; + int from_subzone = subzone.SubzoneID[subzone_level]; + DynamicVectorClass to_subzones; to_subzones.Clear(); if (Map.Build_Reachable_Subzones(&Map[HierLastNodeCell], subzone_level, to_subzones, foot)) { CellSubzoneStruct & last_subzone = Map.CellSubzones[Map.Get_Cell_Zone_Index(HierLastNodeCell)]; - unsigned int subzone_id = (unsigned short)last_subzone.SubzoneID[subzone_level]; + int subzone_id = last_subzone.SubzoneID[subzone_level]; Ban_Neighborhood_Subzone_Edges(subzone_id, subzone_level); continue; } @@ -1882,20 +1904,12 @@ void AStarClass::Ban_Blocked_Subzone_Edges(FootClass const * foot) /// /// The coarseness level the link belongs to. /// bool; Is the link banned? -bool AStarClass::Subzone_Edge_Banned(unsigned short subzone1, unsigned short subzone2, int subzone_level) +bool AStarClass::Subzone_Edge_Banned(int subzone1, int subzone2, int subzone_level) { if (subzone2 < subzone1) { std::swap(subzone1, subzone2); } - unsigned int edge = subzone2 | (subzone1 << 16); - - DynamicVectorClass & edges = HierBannedEdges[subzone_level]; - for (int i = edges.Count() - 1; i >= 0; i--) { - if (edges[i] == edge) { - return(true); - } - } - return(false); + return(HierBannedEdges[subzone_level].contains(std::pair(subzone1, subzone2))); } @@ -1905,14 +1919,13 @@ bool AStarClass::Subzone_Edge_Banned(unsigned short subzone1, unsigned short sub /// corridor that the regular search has already proven it cannot walk. /// /// The coarseness level the link belongs to. -void AStarClass::Ban_Subzone_Edge(unsigned int subzone1, unsigned int subzone2, int subzone_level) +void AStarClass::Ban_Subzone_Edge(int subzone1, int subzone2, int subzone_level) { if (subzone1 != subzone2) { if (subzone2 < subzone1) { std::swap(subzone2, subzone1); } - unsigned int edge = subzone2 | (subzone1 << 16); - HierBannedEdges[subzone_level].Add(edge); + HierBannedEdges[subzone_level].insert(std::pair(subzone1, subzone2)); } } @@ -1926,7 +1939,7 @@ void AStarClass::Ban_Subzone_Edge(unsigned int subzone1, unsigned int subzone2, /// /// The subzone the search became stuck in. /// The coarseness level to work at. -void AStarClass::Ban_Neighborhood_Subzone_Edges(unsigned int subzone, int subzone_level) +void AStarClass::Ban_Neighborhood_Subzone_Edges(int subzone, int subzone_level) { int i; int j; @@ -1948,8 +1961,8 @@ void AStarClass::Ban_Neighborhood_Subzone_Edges(unsigned int subzone, int subzon if (path_index == -1) { IsHSEnabled = false; } else { - unsigned short path_node; - unsigned short path_neighbor; + int path_node; + int path_neighbor; if (path_index == path_count - 1) { path_node = HierSubzonePath[subzone_level][path_index]; path_neighbor = HierSubzonePath[subzone_level][path_index - 1]; @@ -1964,7 +1977,7 @@ void AStarClass::Ban_Neighborhood_Subzone_Edges(unsigned int subzone, int subzon DynamicVectorClass & neighbor_conn = Map.SubzoneTracking[subzone_level][path_neighbor].Connections; for (j = node_conn.Count() - 1; j >= 0; j--) { - unsigned short common_subzone = node_conn[j].SubzoneID; + int common_subzone = node_conn[j].SubzoneID; if (common_subzone != path_neighbor) { for (i = neighbor_conn.Count() - 1; i >= 0; i--) { if (neighbor_conn[i].SubzoneID == common_subzone) { @@ -1997,7 +2010,7 @@ int AStarClass::Test_Cell_Walk(Cell const & from, Cell const & to, FootClass con Clear(); for (int i = 0; i < ARRAY_SIZE(HierBannedEdges); i++) { - HierBannedEdges[i].Clear(); + HierBannedEdges[i].clear(); } CellClass * from_ptr = &Map[from]; diff --git a/code/astar.h b/code/astar.h index 30c8d23de..90f337f4b 100644 --- a/code/astar.h +++ b/code/astar.h @@ -19,6 +19,11 @@ #include "priority.h" #include "vector.h" +#include +#include +#include +#include + #include "facing.hh" #include "move.hh" #include "mzone.hh" @@ -135,32 +140,17 @@ class AStarClass RegularNodePool(void) : ActiveCount(0) {} }; - struct RegularOpenNodePool { - /* - * These are all the open set entries one path attempt may use, handed out in - * order as the search reaches new cells. - */ - RegularOpenNode Nodes[65536]; - - /* - * This is the number of entries handed out so far, and so the index the next - * one comes from. - */ - int ActiveCount; - RegularOpenNodePool(void) : ActiveCount(0) {} - }; - private: /* ---------------------------------------------------------------------------------- * Regular pathfinder (cell-level A*) */ PathStruct * Find_Path_Regular(Cell const & from, Cell const & to, FootClass * foot, FacingType * moves, int max_loops, bool with_hs); - RegularOpenNode * Create_Node(RegularOpenNode * parent, CellClass ** cell, Cell const & to, float movement_cost); + RegularOpenNode Create_Node(std::optional const & parent, CellClass ** cell, Cell const & to, float movement_cost); bool Is_Visited(int, bool base_level, int index); double Get_Movement_Cost(CellClass ** from, CellClass ** to, bool bridge, MoveType move, FootClass * foot); void Apply_Path_Collision_Avoidance(FootClass * foot); FootClass * Find_Moving_Blocker(Cell const & cell, int cell_height); - PathStruct * Build_Final_Path(RegularOpenNode * nodes, FacingType * moves); + PathStruct * Build_Final_Path(RegularOpenNode const & final_node, FacingType * moves); void Cut_Corners(PathStruct * path, FootClass * foot); int Try_Diagonal_Shortcut(FootClass * foot, FacingType * moves, unsigned int * heights, int initial_first_leg_length, int initial_second_leg_length, Cell & cell); void Optimize_Moves(PathStruct * path, FootClass * foot); @@ -172,9 +162,9 @@ class AStarClass */ bool Find_Path_Hierarchical(Cell const & from, Cell const & to, MZoneType mzone, FootClass const * foot); void Ban_Blocked_Subzone_Edges(FootClass const * foot); - bool Subzone_Edge_Banned(unsigned short subzone1, unsigned short subzone2, int subzone_level); - void Ban_Subzone_Edge(unsigned int subzone1, unsigned int subzone2, int subzone_level); - void Ban_Neighborhood_Subzone_Edges(unsigned int subzone, int subzone_level); + bool Subzone_Edge_Banned(int subzone1, int subzone2, int subzone_level); + void Ban_Subzone_Edge(int subzone1, int subzone2, int subzone_level); + void Ban_Neighborhood_Subzone_Edges(int subzone, int subzone_level); private: /* ----------------------------------------------------------------------------------- @@ -214,12 +204,11 @@ class AStarClass bool UseLocomotorEnterCheck; /* - * These are the two node pools the cell level search draws on -- one for the cell and - * parent records that the finished route is recovered from, and one for the open set - * entries handed to the RegularQueue. + * This is the pool of cell and parent records the finished route is recovered from. + * The open set entries handed to the RegularQueue name these rather than holding a + * cell of their own. */ RegularNodePool * RegularNodes; - RegularOpenNodePool * RegularOpenNodes; /* * This is the open set for the cell level search, ordered by score so that the most @@ -314,10 +303,11 @@ class AStarClass float * HierCosts[SUBZONE_COUNT]; /* - * Linear pool of hierarchical A* nodes, allocated by index during expansion. - * The priority queue itself is HierQueue; this is only the node storage. + * The nodes one level of the hierarchical search reaches, in the order it reached + * them. A node names its parent by its position here, so this outlives the queue + * entries taken from it and is emptied for every level. */ - AStarHierarchicalNode * HierNodePool; + std::vector HierNodePool; /* * This is the open set for the hierarchical search, ordered by score so that the @@ -344,12 +334,12 @@ class AStarClass * per hierarchy level; the hierarchical search refuses to expand across * these when it retries after a regular pathfinding failure */ - DynamicVectorClass HierBannedEdges[SUBZONE_COUNT]; + std::set> HierBannedEdges[SUBZONE_COUNT]; /* * Ordered list of subzone IDs forming the hierarchical path per level */ - unsigned short HierSubzonePath[SUBZONE_COUNT][500]; + int HierSubzonePath[SUBZONE_COUNT][500]; /* * Number of valid entries in each hierarchical subzone path diff --git a/code/audio/audiodecode.h b/code/audio/audiodecode.h index 661924abf..48caea132 100644 --- a/code/audio/audiodecode.h +++ b/code/audio/audiodecode.h @@ -32,17 +32,17 @@ struct AUDHeaderType { uint8_t Flags; uint8_t Compression; }; +static_assert(sizeof(AUDHeaderType) == 12, "the AUD header is 12 bytes on disk"); struct AUDChunkHeaderType { uint16_t CompSize; uint16_t UncompSize; uint32_t Magic; }; +static_assert(sizeof(AUDChunkHeaderType) == 8, "an AUD chunk header is 8 bytes on disk"); #pragma pack(pop) -static_assert(sizeof(AUDHeaderType) == 12, "AUD file header layout changed"); static_assert(offsetof(AUDHeaderType, Flags) == 10, "AUD file header layout changed"); -static_assert(sizeof(AUDChunkHeaderType) == 8, "AUD chunk header layout changed"); static_assert(offsetof(AUDChunkHeaderType, Magic) == 4, "AUD chunk header layout changed"); diff --git a/code/blowfish.h b/code/blowfish.h index cb994291f..41d2e7e2b 100644 --- a/code/blowfish.h +++ b/code/blowfish.h @@ -31,9 +31,7 @@ #pragma once -#ifdef _WIN32 #include "win.h" -#endif #include diff --git a/code/building.cpp b/code/building.cpp index 77fc34c57..55e5d25a1 100644 --- a/code/building.cpp +++ b/code/building.cpp @@ -123,6 +123,7 @@ #include "bullettype.h" #include "ccrand.h" #include "cell.h" +#include "classids.h" #include "combat.h" #include "conquer.h" #include "dbgprint.h" @@ -137,7 +138,6 @@ #include "house.h" #include "houstype.h" #include "iloco.h" -#include "classids.h" #include "incdec.h" #include "infantry.h" #include "infatype.h" @@ -6261,7 +6261,7 @@ int BuildingClass::Do_MISSION_UNLOAD(void) walk->Link_To_Object(unit); piggy = Piggyback_Of(walk.get()); if (piggy != NULL) { - piggy->Begin_Piggyback(std::move(unit->Locomotion)); + piggy->Begin_Piggyback(unit->Locomotion); unit->Locomotion = std::move(walk); unit->Locomotion->Force_Track(DriveLocomotionClass::OUT_OF_WEAPON_FACTORY, coord); } else { diff --git a/code/cell.cpp b/code/cell.cpp index 40f782f8f..6218a1685 100644 --- a/code/cell.cpp +++ b/code/cell.cpp @@ -4365,6 +4365,13 @@ void CellClass::Serialize(SaveStreamClass & stream) stream.Serialize(CellID); + // Post_Load installs the cell in the array slot this coordinate names, so a coordinate + // that names none is refused here, while the record can still be thrown away whole. + if (stream.Is_Loading() && Map.Cell_Slot(CellID) < 0) { + stream.Fail(); + return; + } + /* * The snapshot list is built only once something standing here has been fogged over, * so whether the cell has one at all travels ahead of its contents. @@ -4468,7 +4475,11 @@ void CellClass::Post_Load(void) { BASECLASS::Post_Load(); - int id = CellID.X + (CellID.Y << 9); + int id = Map.Cell_Slot(CellID); + if (id < 0) { + return; + } + if (Map.Array[id] != NULL) { delete Map.Array[id]; Map.Array[id] = NULL; diff --git a/code/classfactory.cpp b/code/classfactory.cpp index 63d70e527..bf3c87adb 100644 --- a/code/classfactory.cpp +++ b/code/classfactory.cpp @@ -49,9 +49,9 @@ void Unregister_Classes(void) /// /// Creates a new object of the registered class named by the identifier. /// -/// The object, owned by the caller, or NULL with a debug line naming the +/// The object, owned by the caller, or nothing with a debug line naming the /// identifier when no class was registered for it. -IPersistent * Create_Object(ClassID const & classid) +std::unique_ptr Create_Object(ClassID const & classid) { for (ClassEntryType const & entry : Classes) { if (entry.Class == classid) { @@ -63,5 +63,5 @@ IPersistent * Create_Object(ClassID const & classid) (unsigned long)classid.Data1, (unsigned int)classid.Data2, (unsigned int)classid.Data3, classid.Data4[0], classid.Data4[1], classid.Data4[2], classid.Data4[3], classid.Data4[4], classid.Data4[5], classid.Data4[6], classid.Data4[7]); - return(NULL); + return(nullptr); } diff --git a/code/classfactory.h b/code/classfactory.h index 23830735c..cebba65c9 100644 --- a/code/classfactory.h +++ b/code/classfactory.h @@ -11,16 +11,18 @@ #include "persist.h" +#include + // The classes a saved game or a unit type can name by class identifier. Startup // registers each one; nothing is created for an identifier nobody registered. -typedef IPersistent * (* ClassCreatorType)(void); +using ClassCreatorType = std::unique_ptr (*)(void); void Register_Class(ClassID const & classid, ClassCreatorType creator); void Unregister_Classes(void); -IPersistent * Create_Object(ClassID const & classid); +std::unique_ptr Create_Object(ClassID const & classid); template void Register_Class(ClassID const & classid) { - Register_Class(classid, []() -> IPersistent * { return(new T); }); + Register_Class(classid, []() -> std::unique_ptr { return(std::make_unique()); }); } diff --git a/code/crc.cpp b/code/crc.cpp index 60238085d..bd7f3a4eb 100644 --- a/code/crc.cpp +++ b/code/crc.cpp @@ -295,7 +295,7 @@ unsigned int CRC::_Table[ 256 ] = /// The CRC value to accumulate onto. Pass the result of a previous /// call in order to chain several blocks into one value. /// Returns with the CRC of the block. -unsigned int CRC::Memory( unsigned char *data, unsigned int length, unsigned int crc ) +unsigned int CRC::Memory( unsigned char const *data, unsigned int length, unsigned int crc ) { crc ^= 0xFFFFFFFF; // invert previous CRC while ( length-- ) { diff --git a/code/crc.h b/code/crc.h index 490f48cee..9fa56c6ab 100644 --- a/code/crc.h +++ b/code/crc.h @@ -50,7 +50,7 @@ class CRC { public: // get the CRC of a block of memory - static unsigned int Memory( unsigned char *data, unsigned int length, unsigned int crc = 0 ); + static unsigned int Memory( unsigned char const *data, unsigned int length, unsigned int crc = 0 ); // get the CRC of a null-terminated string static unsigned int String( const char *string, unsigned int crc = 0 ); diff --git a/code/dbgprint.cpp b/code/dbgprint.cpp index 4705e266a..92db0f841 100644 --- a/code/dbgprint.cpp +++ b/code/dbgprint.cpp @@ -392,7 +392,7 @@ R"ART( char line[512]; - snprintf(line, sizeof(line), "Version : OpenTS %s (%s build)\n", OPENTS_VERSION, BuildType); + snprintf(line, sizeof(line), "Version : OpenTS %s (%s %s build)\n", OPENTS_VERSION, OPENTS_ARCH, BuildType); Write_Message_Locked(line, false); snprintf(line, sizeof(line), "Commit : %s on %s%s\n", OPENTS_COMMIT, OPENTS_BRANCH, diff --git a/code/deploymentconfig.cpp b/code/deploymentconfig.cpp index 4f8caf97d..3b9911732 100644 --- a/code/deploymentconfig.cpp +++ b/code/deploymentconfig.cpp @@ -27,6 +27,25 @@ void DeploymentConfigClass::Read_INI(INIClass const & ini) { SearchPaths = ini.Get_String("Paths", "SearchPaths", SearchPaths.c_str()); CarryScenarioFile = ini.Get_Bool("Saves", "CarryScenarioFile", CarryScenarioFile); + RulesFile = ini.Get_String("Files", "Rules", RulesFile.c_str()); + RulesExpansionFile = ini.Get_String("Files", "RulesExpansion", RulesExpansionFile.c_str()); + ArtFile = ini.Get_String("Files", "Art", ArtFile.c_str()); + ArtExpansionFile = ini.Get_String("Files", "ArtExpansion", ArtExpansionFile.c_str()); + AIFile = ini.Get_String("Files", "AI", AIFile.c_str()); + AIExpansionFile = ini.Get_String("Files", "AIExpansion", AIExpansionFile.c_str()); + SoundFile = ini.Get_String("Files", "Sound", SoundFile.c_str()); + SoundExpansionFile = ini.Get_String("Files", "SoundExpansion", SoundExpansionFile.c_str()); + ThemeFile = ini.Get_String("Files", "Theme", ThemeFile.c_str()); + ThemeExpansionFile = ini.Get_String("Files", "ThemeExpansion", ThemeExpansionFile.c_str()); + BattleFile = ini.Get_String("Files", "Battle", BattleFile.c_str()); + BattleExpansionFile = ini.Get_String("Files", "BattleExpansion", BattleExpansionFile.c_str()); + LanguageRulesFile = ini.Get_String("Files", "LanguageRules", LanguageRulesFile.c_str()); + LanguageRulesExpansionFile = ini.Get_String("Files", "LanguageRulesExpansion", LanguageRulesExpansionFile.c_str()); + TutorialFile = ini.Get_String("Files", "Tutorial", TutorialFile.c_str()); + UIFile = ini.Get_String("Files", "UI", UIFile.c_str()); + SettingsFile = ini.Get_String("Files", "Settings", SettingsFile.c_str()); + SchemePaletteFile = ini.Get_String("Palettes", "Scheme", SchemePaletteFile.c_str()); + GamePaletteFile = ini.Get_String("Palettes", "Game", GamePaletteFile.c_str()); } diff --git a/code/deploymentconfig.h b/code/deploymentconfig.h index 6d383270c..8836a3e96 100644 --- a/code/deploymentconfig.h +++ b/code/deploymentconfig.h @@ -26,6 +26,33 @@ class DeploymentConfigClass // Whether a save carries the scenario file it was played from, which enlarges a save by half again. bool CarryScenarioFile = false; + // The files the game reads its rules, artwork and text from. Whether the expansion is + // installed at all is decided by looking for the rules expansion. + std::string RulesFile = "RULES.INI"; + std::string RulesExpansionFile = "FIRESTRM.INI"; + std::string ArtFile = "ART.INI"; + std::string ArtExpansionFile = "ARTFS.INI"; + std::string AIFile = "AI.INI"; + std::string AIExpansionFile = "AIFS.INI"; + std::string SoundFile = "SOUND.INI"; + std::string SoundExpansionFile = "SOUND01.INI"; + std::string ThemeFile = "THEME.INI"; + std::string ThemeExpansionFile = "THEME01.INI"; + std::string BattleFile = "BATTLE.INI"; + std::string BattleExpansionFile = "BATTLEFS.INI"; + std::string LanguageRulesFile = "LANGRULE.INI"; + std::string LanguageRulesExpansionFile = "LANGFS.INI"; + std::string TutorialFile = "TUTORIAL.INI"; + std::string UIFile = "UI.INI"; + + // The file a player's own settings are read from and written back to. + std::string SettingsFile = "SUN.INI"; + + // The palettes in force until a theater is loaded, which the theater roster cannot name + // because they are read before the rules that declare it. + std::string SchemePaletteFile = "UNITSNO.PAL"; + std::string GamePaletteFile = "TEMPERAT.PAL"; + void Read_INI(INIClass const & ini); /* diff --git a/code/drive.cpp b/code/drive.cpp index 53acce07a..ded8d082f 100644 --- a/code/drive.cpp +++ b/code/drive.cpp @@ -117,8 +117,7 @@ DriveLocomotionClass::DriveLocomotionClass(void) : SpeedAccum(0), TargetSpeed(0), TrackNumber(-1), - TrackIndex(-1), - Piggybacker() + TrackIndex(-1) { } @@ -181,9 +180,9 @@ void DriveLocomotionClass::Serialize(SaveStreamClass & stream) /// /// The locomotor that is to take over the unit. /// bool; Was the locomotor taken on? One already carrying a locomotor refuses. -bool DriveLocomotionClass::Begin_Piggyback(std::unique_ptr carried) +bool DriveLocomotionClass::Begin_Piggyback(std::unique_ptr & carried) { - if (carried == NULL || Piggybacker != NULL) { + if (carried == nullptr || Piggybacker != nullptr) { return(false); } Piggybacker = std::move(carried); diff --git a/code/drive.h b/code/drive.h index f830beda6..54b7d6d8c 100644 --- a/code/drive.h +++ b/code/drive.h @@ -89,10 +89,10 @@ class DriveLocomotionClass : public LocomotionClass, public IPiggyback virtual int Get_Track_Index(void) override; virtual int Get_Speed_Accum(void) override; - virtual bool Begin_Piggyback(std::unique_ptr carried) override; + virtual bool Begin_Piggyback(std::unique_ptr & carried) override; virtual std::unique_ptr End_Piggyback(void) override; virtual bool Is_Ok_To_End(void) override; - virtual bool Is_Piggybacking(void) override {return(Piggybacker != NULL);} + virtual bool Is_Piggybacking(void) override {return(Piggybacker != nullptr);} /*--------------------------------------------------------------------- ** Member function prototypes. diff --git a/code/droppod.cpp b/code/droppod.cpp index dbb375f2b..d4eab0915 100644 --- a/code/droppod.cpp +++ b/code/droppod.cpp @@ -38,8 +38,7 @@ DropPodLocomotionClass::DropPodLocomotionClass(void) : BASECLASS(), Direction(DPOD_DIR_NE), - DestinationCoord(COORD_NONE), - Piggybacker() + DestinationCoord(COORD_NONE) { } @@ -82,6 +81,11 @@ Coord DropPodLocomotionClass::Destination(void) /// bool DropPodLocomotionClass::Process(void) { + // Handing the carried locomotor back leaves this pod unowned, so it holds itself for + // the rest of the routine. The slot is declared here rather than beside the hand-back + // so that the pod outlives every member this routine still reads. + std::unique_ptr self; + Coord coord = LinkedTo->PositionCoord; Coord smoke_coord = coord; @@ -118,10 +122,12 @@ bool DropPodLocomotionClass::Process(void) coord = linked->PositionCoord; linked->Limbo(); - // Handing the carried locomotor back makes this pod unowned, so it holds itself - // until the landing is finished and is deleted on return. - std::unique_ptr const self = std::move(LinkedTo->Locomotion); - LinkedTo->Locomotion = End_Piggyback(); + // A pod that carries nothing stays the object's locomotor. + std::unique_ptr carried = End_Piggyback(); + if (carried != nullptr) { + self = std::move(LinkedTo->Locomotion); + LinkedTo->Locomotion = std::move(carried); + } if (!linked->Unlimbo(coord, DIR_N)) { Explosion_Damage(coord, 100, LinkedTo, Rule->C4Warhead); @@ -265,9 +271,9 @@ void DropPodLocomotionClass::Stop_Moving(void) /// /// The locomotor that is to take over the unit. /// bool; Was the locomotor taken on? One already carrying a locomotor refuses. -bool DropPodLocomotionClass::Begin_Piggyback(std::unique_ptr carried) +bool DropPodLocomotionClass::Begin_Piggyback(std::unique_ptr & carried) { - if (carried == NULL || Piggybacker != NULL) { + if (carried == nullptr || Piggybacker != nullptr) { return(false); } Piggybacker = std::move(carried); diff --git a/code/droppod.h b/code/droppod.h index 1727e5354..de6c0b7cd 100644 --- a/code/droppod.h +++ b/code/droppod.h @@ -44,10 +44,10 @@ class DropPodLocomotionClass : public LocomotionClass, public IPiggyback virtual LayerType In_Which_Layer(void) override; virtual int Drawing_Code(void) override; - virtual bool Begin_Piggyback(std::unique_ptr carried) override; + virtual bool Begin_Piggyback(std::unique_ptr & carried) override; virtual std::unique_ptr End_Piggyback(void) override; virtual bool Is_Ok_To_End(void) override; - virtual bool Is_Piggybacking(void) override {return(Piggybacker != NULL);} + virtual bool Is_Piggybacking(void) override {return(Piggybacker != nullptr);} private: enum DropPodDirType { diff --git a/code/foot.cpp b/code/foot.cpp index 068dd7be2..412526c3b 100644 --- a/code/foot.cpp +++ b/code/foot.cpp @@ -502,7 +502,7 @@ bool FootClass::Basic_Path(Cell cell, int path_offset, int avoidance) */ bool found = false; // Found a best path yet? PathStruct path1; - FacingType workpath[200*10]; // Staging area for path list. + FacingType workpath[PATH_LENGTH_MAX + 1]; // Staging area for path list. MoveType maxtype = MOVE_OK; path = Find_Path(cell, &workpath[0], ARRAY_SIZE(workpath), maxtype, path_offset, avoidance); @@ -3408,7 +3408,7 @@ ZGradientType FootClass::Get_Z_Gradient(void) const /// void FootClass::Draw_Voxel_Shadow(VoxelDataStruct const & voxeldata, int layer_index, int key, VoxelIndexClass * cache, Rect const & cliprect, Point2D const & point, Matrix3D const & matrix, bool force_cache) const { - if (Locomotion != NULL && Locomotion->Is_To_Have_Shadow() == (bool)true) { + if (Locomotion != nullptr && Locomotion->Is_To_Have_Shadow() == (bool)true) { Point2D drawpoint = point; if (Locomotion != NULL) { drawpoint = Point2D(Locomotion->Shadow_Point()) + point; @@ -3589,7 +3589,7 @@ void FootClass::Link_DropPod(void) std::unique_ptr ballistic = Create_Locomotor(ClassID_BallisticLocomotion); ballistic->Link_To_Object(this); IPiggyback * piggy = Piggyback_Of(ballistic.get()); - piggy->Begin_Piggyback(std::move(locomotion)); + piggy->Begin_Piggyback(locomotion); Locomotion = std::move(ballistic); } diff --git a/code/foot.h b/code/foot.h index 5bbb1502f..3d159dc0a 100644 --- a/code/foot.h +++ b/code/foot.h @@ -39,6 +39,7 @@ #include #include +#include class UnitClass; class BuildingClass; diff --git a/code/hashtable.h b/code/hashtable.h deleted file mode 100644 index 49c5869c2..000000000 --- a/code/hashtable.h +++ /dev/null @@ -1,253 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2026 OpenTS contributors - * - * See LICENSE.md for applicable additional terms and warranty disclaimers. - ******************************************************************************/ - -#pragma once - -#include "vector.h" - - -/* - * A single key/value entry stored within a hash table bucket. Equality and - * the "empty" test are keyed off the Key alone. - */ -template -struct HashObject { - HashObject(void) {} - HashObject(K const & key, V const & value) : Key(key), Value(value) {} - HashObject(HashObject const & that) : Key(that.Key), Value(that.Value) {} - - bool operator!=(const HashObject & that) const { return(Key != that.Key); } - bool operator==(const HashObject & that) const { return(Key == that.Key); } - - bool operator!() const { return(Key == 0); } - - /* - * This is the key the entry is filed under. It carries the entry's whole identity, - * so a bucket never holds two entries whose keys match. - */ - K Key; - - /* - * This is the value filed under the key. A table with nothing of its own to store - * packs everything it needs into the key and merely repeats it here. - */ - V Value; -}; - - -/* - * A chained (separate-chaining) hash table. It owns a fixed array of - * NumBuckets buckets, each bucket being a growable vector of key/value - * pairs. Collisions within a bucket are resolved by a linear scan. - * The same template backs every hash table in the game -- the map's - * subzone/zone connection tables and the radar object tracking table -- - * which differ only in their key/value types and in how the owning class - * derives a bucket index for a given key. Because of that, two styles of - * insertion coexist: - * * The caller computes the bucket index itself and passes it in - * (the subzone connection tables, whose keys are packed zone ids). - * * The table hashes the key directly via Key.Hash() and picks head or - * tail placement via Key.Use_Head() (the radar tracking table). - * Only the members actually used by a given instantiation are generated, - * so a key type need not provide Hash()/Use_Head() unless the key-hashed - * overloads are called for it. - * The owning class is a friend and also reaches into Buckets directly for - * iteration and some lookups. - */ -template -class HashTableClass -{ - public: - friend class RadarClass; - - typedef HashObject ObjectType; - typedef DynamicVectorClass BucketType; - typedef int (*HASH_FUNC)(const K &); - - public: - HashTableClass(int growth_step, int num_buckets, HASH_FUNC hash_func); - ~HashTableClass(void); - - /* - * Caller-hashed insertion. The packed key supplies its own bucket - * index, or the index is passed in explicitly. - */ - bool Add_Object(ObjectType const & object); - bool Add_Object(int bucket_index, ObjectType const & object); - - /* - * Key-hashed insertion: bucket index comes from Key.Hash() and the - * entry is placed at the head or tail based on Key.Use_Head(). - */ - bool Add_Object(ObjectType const & o, bool head); - - bool Get(const K & key, V & out); - bool Remove_Object(const K & key, const V & value); - - void Destroy(void); - void Clear(void); - - public: - /* - * The array of NumBuckets buckets. - */ - BucketType * Buckets; - - /* - * A pointer to the function for hashing an item's key. Nothing calls - * through it -- the bucket index is derived at each call site instead. - */ - HASH_FUNC HashFunction; - - /* - * The number of buckets in the hash table. - */ - int NumBuckets; - - /* - * When a bucket has insufficient room left, it will grow by the number - * of objects specified by this value. - */ - int GrowthStep; -}; - - -template -inline HashTableClass::HashTableClass(int growth_step, int num_buckets, HASH_FUNC hash_func) : - HashFunction(hash_func), - NumBuckets(num_buckets), - GrowthStep(growth_step) -{ - Buckets = new BucketType[num_buckets]; - for (int index = 0; index < NumBuckets; index++) { - Buckets[index].Set_Growth_Step(growth_step); - } -} - - -template -inline HashTableClass::~HashTableClass(void) -{ - Destroy(); -} - - -template -inline bool HashTableClass::Add_Object(ObjectType const & object) -{ - unsigned packed = object.Value | (object.Key << 16); - BucketType & bucket = Buckets[object.Value & 0xF | ((object.Key & 0xF) << 4)]; - - if (bucket.Count() > 0) { - ObjectType * obj = &bucket[0]; - for (int index = bucket.Count() - 1; index >= 0; index--) { - if (ObjectType(packed, packed) == *obj) { - return(false); - } - obj++; - } - } - - return(bucket.Add(ObjectType(packed, packed))); -} - - -template -inline bool HashTableClass::Add_Object(int bucket_index, ObjectType const & object) -{ - BucketType & bucket = Buckets[bucket_index]; - - if (bucket.Count() > 0) { - ObjectType * obj = &bucket[0]; - for (int index = bucket.Count() - 1; index >= 0; index--) { - if ((K &)object == (K &)*obj) { - - return(false); - } - obj++; - } - } - bucket.Add(object); - return(true); -} - - -template -inline bool HashTableClass::Add_Object(const ObjectType & o, bool head) -{ - BucketType & bucket = Buckets[o.Key.Hash()]; - if (bucket.Count() > 0) { - const ObjectType * oo = &bucket[0]; - - for (int index = 0; index < bucket.Count(); index++) { - if (oo[index] == o) { - return(false); - } - } - } - - if (o.Key.Use_Head()) { - bucket.Add_Head(o); - } else { - bucket.Add(o); - } - return(true); -} - - -template -inline bool HashTableClass::Get(const K & key, V & out) -{ - BucketType & bucket = Buckets[key.Hash()]; - int index = bucket.Count() - 1; - out = V(); - - if (index >= 0) { - ObjectType * oo = &bucket[0]; - while (true) { - V value = (V &)bucket[index].Key; - if ((K &)oo[index].Key == (K &)key) { - out = value; - return(true); - } - index--; - if (index < 0) { - break; - } - } - } - - return(false); -} - - -template -inline bool HashTableClass::Remove_Object(const K & key, const V & value) -{ - BucketType * bucket = &Buckets[key.Hash()]; - ObjectType a(key, value); - - return(bucket->Delete(a)); -} - - -template -inline void HashTableClass::Destroy(void) -{ - delete[] Buckets; -} - - -template -inline void HashTableClass::Clear(void) -{ - for (int index = 0; index < NumBuckets; index++) { - Buckets[index].Clear(); - } -} diff --git a/code/house.cpp b/code/house.cpp index e187c2d08..a1bf5b687 100644 --- a/code/house.cpp +++ b/code/house.cpp @@ -693,7 +693,7 @@ HouseClass::~HouseClass (void) // removed here, or the list would never drain. while (HouseTags.Count() > 0) { TagClass * const tag = HouseTags[0]; - if (tag == NULL) { + if (tag == nullptr) { HouseTags.Delete_Index(0); } else { delete tag; diff --git a/code/houstype.cpp b/code/houstype.cpp index 9083169e3..25721afd6 100644 --- a/code/houstype.cpp +++ b/code/houstype.cpp @@ -295,5 +295,3 @@ int HouseTypeClass::Fetch_Heap_ID(void) const { return(HeapID); } - - diff --git a/code/iff.h b/code/iff.h index 8cdb39882..433a7cf1e 100644 --- a/code/iff.h +++ b/code/iff.h @@ -82,9 +82,9 @@ struct CompHeaderType { std::int32_t Size; // Size of the uncompressed data. std::int16_t Skip; // Number of bytes to skip before data. }; +static_assert(sizeof(CompHeaderType) == 8, "the compressed file header is 8 bytes on disk"); #pragma pack(pop) -static_assert(sizeof(CompHeaderType) == 8, "Compressed block header layout changed"); static_assert(offsetof(CompHeaderType, Size) == 2, "Compressed block header layout changed"); static_assert(offsetof(CompHeaderType, Skip) == 6, "Compressed block header layout changed"); diff --git a/code/iflyctrl.h b/code/iflyctrl.h index 527143e0f..8a2415ee6 100644 --- a/code/iflyctrl.h +++ b/code/iflyctrl.h @@ -40,4 +40,3 @@ struct IFlyControl */ virtual LONG Is_Locked(void) = 0; }; - diff --git a/code/iloco.h b/code/iloco.h index 19f01d2bf..d83e775cf 100644 --- a/code/iloco.h +++ b/code/iloco.h @@ -20,8 +20,6 @@ #include "visual.hh" #include "zgrad.hh" -#include - /* @@ -237,12 +235,13 @@ struct ILocomotion virtual void Stop_Movement_Animation(void) = 0; /* - * Locks the locomotor from being deleted + * Locks the locomotor against being handed back, so that one piggybacking on it keeps + * control of the object. */ virtual void Lock(void) = 0; /* - * Unlocks the locomotor from being deleted + * Unlocks the locomotor, so that a piggyback riding on it may end. */ virtual void Unlock(void) = 0; @@ -261,4 +260,3 @@ struct ILocomotion */ virtual int Get_Speed_Accum(void) = 0; }; - diff --git a/code/infantry.cpp b/code/infantry.cpp index 907572bb8..1a6103742 100644 --- a/code/infantry.cpp +++ b/code/infantry.cpp @@ -99,6 +99,7 @@ #include "builtype.h" #include "ccrand.h" #include "cell.h" +#include "classids.h" #include "combat.h" #include "data.h" #include "draw.h" @@ -107,7 +108,6 @@ #include "goptions.h" #include "house.h" #include "houstype.h" -#include "classids.h" #include "incdec.h" #include "infatype.h" #include "inline.h" @@ -1195,7 +1195,7 @@ void InfantryClass::Assign_Destination(AbstractClass * target, bool immediate) walk->Link_To_Object(this); piggy = Piggyback_Of(walk.get()); if (piggy != NULL) { - piggy->Begin_Piggyback(std::move(Locomotion)); + piggy->Begin_Piggyback(Locomotion); Locomotion = std::move(walk); } } @@ -4156,7 +4156,7 @@ bool InfantryClass::JumpJet_To_Walk(void) piggy = Piggyback_Of(walk.get()); if (piggy != NULL) { Path[0] = FACING_NONE; - piggy->Begin_Piggyback(std::move(Locomotion)); + piggy->Begin_Piggyback(Locomotion); Locomotion = std::move(walk); Locomotion->Move_To(NavCom->Center_Coord()); return(true); diff --git a/code/ini.cpp b/code/ini.cpp index 7b2874133..64a8536f7 100644 --- a/code/ini.cpp +++ b/code/ini.cpp @@ -957,41 +957,71 @@ int INIClass::Get_Int(char const * section, char const * entry, int defvalue) co } -// A class identifier as the registry writes it, braces optional: eight, four, four, four -// and twelve hexadecimal digits separated by hyphens. +// One field of a class identifier, exactly the many hexadecimal digits it is written with. +static bool Parse_Hex_Field(char const * & ptr, int digits, unsigned int & value) +{ + value = 0; + + for (int index = 0; index < digits; index++) { + char const letter = *ptr++; + unsigned int digit; + + if (letter >= '0' && letter <= '9') { + digit = (unsigned int)(letter - '0'); + } else if (letter >= 'A' && letter <= 'F') { + digit = (unsigned int)(letter - 'A') + 10; + } else if (letter >= 'a' && letter <= 'f') { + digit = (unsigned int)(letter - 'a') + 10; + } else { + return(false); + } + + value = (value << 4) | digit; + } + + return(true); +} + + +// A class identifier as the registry writes it, the surrounding braces optional: eight, +// four, four, four and twelve hexadecimal digits separated by hyphens, and nothing else. static bool Parse_ClassID(char const * text, ClassID & clsid) { - char digits[40]; - unsigned int length = 0; + char const * ptr = text; + std::size_t length = strlen(text); - for (char const * ptr = text; *ptr != '\0'; ptr++) { - if (*ptr == '{' || *ptr == '}') { - continue; + if (length == 38 && ptr[0] == '{' && ptr[37] == '}') { + ptr++; + length -= 2; + } + if (length != 36) { + return(false); + } + + unsigned int data1; + unsigned int data2; + unsigned int data3; + if (!Parse_Hex_Field(ptr, 8, data1) || *ptr++ != '-') return(false); + if (!Parse_Hex_Field(ptr, 4, data2) || *ptr++ != '-') return(false); + if (!Parse_Hex_Field(ptr, 4, data3) || *ptr++ != '-') return(false); + + unsigned char data4[8]; + for (int index = 0; index < ARRAY_SIZE(data4); index++) { + unsigned int byte; + if (!Parse_Hex_Field(ptr, 2, byte)) { + return(false); } - if (length >= sizeof(digits) - 1) { + data4[index] = (unsigned char)byte; + if (index == 1 && *ptr++ != '-') { return(false); } - digits[length++] = *ptr; - } - digits[length] = '\0'; - - unsigned int data1 = 0; - unsigned int data2 = 0; - unsigned int data3 = 0; - unsigned int data4[8] = { 0 }; - int const scanned = sscanf(digits, "%8x-%4x-%4x-%2x%2x-%2x%2x%2x%2x%2x%2x", - &data1, &data2, &data3, - &data4[0], &data4[1], &data4[2], &data4[3], - &data4[4], &data4[5], &data4[6], &data4[7]); - if (scanned != 11 || length != 36) { - return(false); } clsid.Data1 = data1; clsid.Data2 = (unsigned short)data2; clsid.Data3 = (unsigned short)data3; - for (int index = 0; index < 8; index++) { - clsid.Data4[index] = (unsigned char)data4[index]; + for (int index = 0; index < ARRAY_SIZE(data4); index++) { + clsid.Data4[index] = data4[index]; } return(true); } @@ -1007,15 +1037,6 @@ static void Format_ClassID(ClassID const & clsid, char * text) } -/// -/// Stores a class identifier into the INI database. -/// This routine will convert the identifier into its printable brace and hyphen form before -/// storing it, so that the resulting entry stays readable and can be edited by hand. -/// -/// The identifier for the section that the entry will be placed in. -/// The entry identifier to tag to the class identifier specified. -/// The class identifier to store. -/// bool; Was the class identifier placed into the INI database? /// /// Fetches a class identifier from the specified section. /// This routine will fetch the printable form of a class identifier from the entry and @@ -1041,6 +1062,15 @@ ClassID const INIClass::Get_ClassID(char const * section, char const * entry, Cl } +/// +/// Stores a class identifier into the INI database. +/// This routine will convert the identifier into its printable brace and hyphen form before +/// storing it, so that the resulting entry stays readable and can be edited by hand. +/// +/// The identifier for the section that the entry will be placed in. +/// The entry identifier to tag to the class identifier specified. +/// The class identifier to store. +/// bool; Was the class identifier placed into the INI database? bool INIClass::Put_ClassID(char const * section, char const * entry, ClassID const & value) { char buffer[40]; diff --git a/code/init.cpp b/code/init.cpp index 0f550bd03..a085baf64 100644 --- a/code/init.cpp +++ b/code/init.cpp @@ -61,6 +61,7 @@ #include "_bench.h" #include "_command.h" #include "_convert.h" +#include "_deploymentconfig.h" #include "_font.h" #include "_keyboar.h" #include "_logic.h" @@ -102,6 +103,7 @@ #include "conquer.h" #include "data.h" #include "dbgprint.h" +#include "deploymentconfig.h" #include "dialog.h" #include "audio/audioengine.h" #include "dsurface.h" @@ -484,19 +486,19 @@ int Init_Game(int , char * []) return(-1); } - DebugString("Reading UI.INI\n"); - if (!UIControls.Read_INI_File("UI.INI", true)) { - DebugString("UI.INI not found, using the defaults.\n"); + DebugString("Reading %s\n", DeploymentConfig.UIFile.c_str()); + if (!UIControls.Read_INI_File(DeploymentConfig.UIFile.c_str(), true)) { + DebugString("%s not found, using the defaults.\n", DeploymentConfig.UIFile.c_str()); } /* ** */ - DebugString("Reading SOUND.INI\n"); + DebugString("Reading %s\n", DeploymentConfig.SoundFile.c_str()); CCINIClass voc_ini; - if (!Read_INI_And_Expansion(voc_ini, "SOUND.INI", "SOUND01.INI")) { - DebugString("Failed to read SOUND.INI or SOUND01.INI!\n"); + if (!Read_INI_And_Expansion(voc_ini, DeploymentConfig.SoundFile.c_str(), DeploymentConfig.SoundExpansionFile.c_str())) { + DebugString("Failed to read %s or %s!\n", DeploymentConfig.SoundFile.c_str(), DeploymentConfig.SoundExpansionFile.c_str()); return(-1); } @@ -519,11 +521,11 @@ int Init_Game(int , char * []) // A score's Side= names a side the rules declare, so the roster is built before the scores are read. Prepare_Side_Roster(); - DebugString("Reading THEME.INI\n"); + DebugString("Reading %s\n", DeploymentConfig.ThemeFile.c_str()); CCINIClass theme_ini; - if (!Read_INI_And_Expansion(theme_ini, "THEME.INI", "THEME01.INI")) { - DebugString("Failed to read THEME.INI or THEME01.INI!\n"); + if (!Read_INI_And_Expansion(theme_ini, DeploymentConfig.ThemeFile.c_str(), DeploymentConfig.ThemeExpansionFile.c_str())) { + DebugString("Failed to read %s or %s!\n", DeploymentConfig.ThemeFile.c_str(), DeploymentConfig.ThemeExpansionFile.c_str()); return(-1); } @@ -643,7 +645,7 @@ void Init_Campaigns(void) CCINIClass * ini = new CCINIClass; ini->Load(file, false); - if (stricmp(name.c_str(), "BATTLE.INI") == 0) { + if (stricmp(name.c_str(), DeploymentConfig.BattleFile.c_str()) == 0) { found = true; } @@ -652,7 +654,7 @@ void Init_Campaigns(void) } if (!found) { - CCFileClass file("BATTLE.INI"); + CCFileClass file(DeploymentConfig.BattleFile.c_str()); CCINIClass * ini = new CCINIClass; if (ini != NULL) { @@ -662,7 +664,7 @@ void Init_Campaigns(void) } } - CCFileClass file("BATTLEFS.INI"); + CCFileClass file(DeploymentConfig.BattleExpansionFile.c_str()); if (file.Is_Available() == true) { CCINIClass * ini = new CCINIClass; @@ -767,7 +769,7 @@ static bool Init_Rules(void) rule->Load(file, false); - if (stricmp(name.c_str(), "RULES.INI") == 0) { + if (stricmp(name.c_str(), DeploymentConfig.RulesFile.c_str()) == 0) { found = true; Rules.Add_Head(rule); } else { @@ -776,7 +778,7 @@ static bool Init_Rules(void) } if (!found) { - CCFileClass file("RULES.INI"); + CCFileClass file(DeploymentConfig.RulesFile.c_str()); CCINIClass * rule = new CCINIClass; rule->Load(file, false); Rules.Add_Head(rule); @@ -788,26 +790,26 @@ static bool Init_Rules(void) return(false); } - CCFileClass art_file("ART.INI"); + CCFileClass art_file(DeploymentConfig.ArtFile.c_str()); if (!ArtINI.Load(art_file, false)) { - DebugString("Failed to load ART.INI!\n"); + DebugString("Failed to load %s!\n", DeploymentConfig.ArtFile.c_str()); return(false); } CCINIClass art_ini; - CCFileClass art_fs_file("ARTFS.INI"); + CCFileClass art_fs_file(DeploymentConfig.ArtExpansionFile.c_str()); if (art_fs_file.Is_Available() == true) { art_ini.Load(art_fs_file, false); } if (Addon_Installed(ADDON_FIRESTORM)) { - CCFileClass rules_fs_file("FIRESTRM.INI"); + CCFileClass rules_fs_file(DeploymentConfig.RulesExpansionFile.c_str()); if (rules_fs_file.Is_Available() == true) { CCINIClass rule_fs; if (!FSRuleINI.Load(rules_fs_file, false)) { - DebugString("Failed to load FIRESTRM.INI!\n"); + DebugString("Failed to load %s!\n", DeploymentConfig.RulesExpansionFile.c_str()); return(false); } } @@ -844,7 +846,7 @@ static bool Init_Rules(void) Session.Options.AIPlayers = 0; Session.Options.AIDifficulty = DIFF_NORMAL; - CCFileClass lang_file("LANGRULE.INI"); + CCFileClass lang_file(DeploymentConfig.LanguageRulesFile.c_str()); if (lang_file.Is_Available() == true) { CCINIClass lang_ini; @@ -862,15 +864,15 @@ static bool Init_Rules(void) } } - CCFileClass ai_file("AI.INI"); + CCFileClass ai_file(DeploymentConfig.AIFile.c_str()); AIINI.Load(ai_file, true); if (Addon_Installed(ADDON_FIRESTORM)) { - CCFileClass ai_fs_file("AIFS.INI"); + CCFileClass ai_fs_file(DeploymentConfig.AIExpansionFile.c_str()); if (ai_fs_file.Is_Available() == true) { CCINIClass ai_fs_ini; if (!FSAIINI.Load(ai_fs_file, false)) { - DebugString("Failed to load AIFS.INI!\n"); + DebugString("Failed to load %s!\n", DeploymentConfig.AIExpansionFile.c_str()); return(false); } } @@ -2196,6 +2198,31 @@ static void Init_Patch_Mixfiles(void) } +/// +/// Reads a palette out of the mounted archives and expands it to the game's colour range. +/// +/// The palette to fill, left unchanged if the file is not there. +/// The palette file to read. +static void Read_Palette(PaletteClass & palette, char const * name) +{ + void const * data = MFCD::Retrieve(name); + + if (data == NULL) { + DebugString("%s not found; leaving that palette unchanged.\n", name); + return; + } + + memmove(&palette[0], data, sizeof(palette)); + + for (int index = 0; index < PaletteClass::COLOR_COUNT; index++) { + palette[index] = RGBClass( + (unsigned char)(palette[index].Get_Red()<<2), + (unsigned char)(palette[index].Get_Green()<<2), + (unsigned char)(palette[index].Get_Blue()<<2)); + } +} + + /*********************************************************************************************** * Init_Bootstrap_Mixfiles -- Registers and caches any mixfiles needed for bootstrapping. * * * @@ -2402,8 +2429,6 @@ static bool Init_Secondary_Mixfiles(void) *=============================================================================================*/ static bool Bootstrap(void) { - int index; - /* ** Process the message loop until we are in focus. We need to be in focus to read pixels from ** the screen. @@ -2452,39 +2477,18 @@ static bool Bootstrap(void) /* * House specific scheme palette initialization. */ - memmove((unsigned char *)&SchemePalette[0], (void *)MFCD::Retrieve("UNITSNO.PAL"), sizeof(SchemePalette)); - - for (index = 0; index < 256; index++) { - SchemePalette[index] = RGBClass( - (unsigned char)(SchemePalette[index].Get_Red()<<2), - (unsigned char)(SchemePalette[index].Get_Green()<<2), - (unsigned char)(SchemePalette[index].Get_Blue()<<2)); - } + Read_Palette(SchemePalette, DeploymentConfig.SchemePaletteFile.c_str()); /* ** Default palette initialization. */ - memmove((unsigned char *)&GamePalette[0], (void *)MFCD::Retrieve("TEMPERAT.PAL"), sizeof(GamePalette)); - - for (index = 0; index < 256; index++) { - GamePalette[index] = RGBClass( - (unsigned char)(GamePalette[index].Get_Red()<<2), - (unsigned char)(GamePalette[index].Get_Green()<<2), - (unsigned char)(GamePalette[index].Get_Blue()<<2)); - } + Read_Palette(GamePalette, DeploymentConfig.GamePaletteFile.c_str()); OriginalPalette = GamePalette; CCPalette = GamePalette; WhitePalette[0] = BlackPalette[0]; - memmove((unsigned char *)&WaypointPalette[0], (void *)MFCD::Retrieve("WAYPOINT.PAL"), sizeof(WaypointPalette)); - - for (index = 0; index < 256; index++) { - WaypointPalette[index] = RGBClass( - (unsigned char)(WaypointPalette[index].Get_Red()<<2), - (unsigned char)(WaypointPalette[index].Get_Green()<<2), - (unsigned char)(WaypointPalette[index].Get_Blue()<<2)); - } + Read_Palette(WaypointPalette, "WAYPOINT.PAL"); /* * Voxel system initialization. @@ -2510,54 +2514,21 @@ static bool Bootstrap(void) */ TerrainDrawer = new ConvertClass(GamePalette, GamePalette, *VisibleSurface, NUM_INTENSITY_LEVELS); - PaletteClass pal; - - memmove((unsigned char *)&pal[0], (void *)MFCD::Retrieve("ANIM.PAL"), sizeof(pal)); - for (index = 0; index < 256; index++) { - pal[index] = RGBClass( - (unsigned char)(pal[index].Get_Red()<<2), - (unsigned char)(pal[index].Get_Green()<<2), - (unsigned char)(pal[index].Get_Blue()<<2)); - } + PaletteClass pal = BlackPalette; + Read_Palette(pal, "ANIM.PAL"); AnimDrawer = new ConvertClass(pal, GamePalette, *VisibleSurface, NUM_INTENSITY_LEVELS); - memmove((unsigned char *)&pal[0], (void *)MFCD::Retrieve("PALETTE.PAL"), sizeof(pal)); - for (index = 0; index < 256; index++) { - pal[index] = RGBClass( - (unsigned char)(pal[index].Get_Red()<<2), - (unsigned char)(pal[index].Get_Green()<<2), - (unsigned char)(pal[index].Get_Blue()<<2)); - } - + Read_Palette(pal, "PALETTE.PAL"); NormalDrawer = new ConvertClass(pal, GamePalette, *VisibleSurface, NUM_INTENSITY_LEVELS); - memmove((unsigned char *)&pal[0], (void *)MFCD::Retrieve("UNITSNO.PAL"), sizeof(pal)); - for (index = 0; index < 256; index++) { - pal[index] = RGBClass( - (unsigned char)(pal[index].Get_Red()<<2), - (unsigned char)(pal[index].Get_Green()<<2), - (unsigned char)(pal[index].Get_Blue()<<2)); - } - + Read_Palette(pal, DeploymentConfig.SchemePaletteFile.c_str()); VoxelDrawer = new ConvertClass(pal, GamePalette, *VisibleSurface, NUM_INTENSITY_LEVELS); - memmove((unsigned char *)&pal[0], (void *)MFCD::Retrieve("CAMEO.PAL"), sizeof(pal)); - for (index = 0; index < 256; index++) { - pal[index] = RGBClass( - (unsigned char)(pal[index].Get_Red()<<2), - (unsigned char)(pal[index].Get_Green()<<2), - (unsigned char)(pal[index].Get_Blue()<<2)); - } + Read_Palette(pal, "CAMEO.PAL"); CameoDrawer = new ConvertClass(pal, GamePalette, *VisibleSurface, NUM_INTENSITY_LEVELS); - memmove((unsigned char *)&pal[0], (void *)MFCD::Retrieve("MOUSEPAL.PAL"), sizeof(pal)); - for (index = 0; index < 256; index++) { - pal[index] = RGBClass( - (unsigned char)(pal[index].Get_Red()<<2), - (unsigned char)(pal[index].Get_Green()<<2), - (unsigned char)(pal[index].Get_Blue()<<2)); - } + Read_Palette(pal, "MOUSEPAL.PAL"); MouseDrawer = new ConvertClass(pal, GamePalette, *VisibleSurface); TiberiumDrawer = VoxelDrawer; @@ -2657,7 +2628,7 @@ static bool Init_Bulk_Data(void) ** Fetch the tutorial message data. */ INIClass ini; - CCFileClass file("TUTORIAL.INI"); + CCFileClass file(DeploymentConfig.TutorialFile.c_str()); ini.Load(file); TutorialText.Read_Base(ini); @@ -6072,7 +6043,7 @@ bool Prep_For_Side(SideType side) } sprintf(name, "SIDENC%02d.MIX", id); - DebugString(" Initilizing %s\n", name); + DebugString(" Initializing %s\n", name); if (CCFileClass(name).Is_Available()) { SideNCMix = new MFCD(name, &FastKey); @@ -6086,7 +6057,7 @@ bool Prep_For_Side(SideType side) sprintf(name, "E%02dSCD%02d.MIX", Get_Required_Addon(), id); } - DebugString(" Initilizing %s\n", name); + DebugString(" Initializing %s\n", name); if (CCFileClass(name).Is_Available()) { SideCDMix = new MFCD(name, &FastKey); } @@ -6097,7 +6068,7 @@ bool Prep_For_Side(SideType side) } // A side archive may carry its own copy of the file. - UIControls.Read_INI_File("UI.INI", true); + UIControls.Read_INI_File(DeploymentConfig.UIFile.c_str(), true); Map.Init_For_House(); @@ -6150,7 +6121,7 @@ bool Prep_Speech_For_Side(SideType side) } sprintf(name, "SPEECH%02d.MIX", id); - DebugString(" Initilizing %s\n", name); + DebugString(" Initializing %s\n", name); if (CCFileClass(name).Is_Available()) { SpeechMix = new MFCD(name, &FastKey); } diff --git a/code/ipiggy.h b/code/ipiggy.h index 3afbd6f18..39cdd1d1f 100644 --- a/code/ipiggy.h +++ b/code/ipiggy.h @@ -11,13 +11,16 @@ #include "iloco.h" +#include + struct IPiggyback { /* - * Piggybacks a locomotor onto this one. + * Piggybacks a locomotor onto this one. The locomotor is taken only when the answer + * is true; a refusal leaves it with the caller rather than destroying it. */ - virtual bool Begin_Piggyback(std::unique_ptr carried) = 0; + virtual bool Begin_Piggyback(std::unique_ptr & carried) = 0; /* * Hands the carried locomotor back, or nothing when none is carried. @@ -36,7 +39,7 @@ struct IPiggyback }; -// The piggyback side of a locomotor, or NULL when it cannot carry one. +// The piggyback side of a locomotor, or null when it cannot carry one. inline IPiggyback * Piggyback_Of(ILocomotion * locomotion) { return(dynamic_cast(locomotion)); diff --git a/code/isotype.h b/code/isotype.h index 52c72f69d..1bef21f33 100644 --- a/code/isotype.h +++ b/code/isotype.h @@ -110,9 +110,9 @@ struct IsoTileRecord RGBStruct LowColor; RGBStruct HighColor; }; +static_assert(sizeof(IsoTileRecord) == 52, "a TMP tile record is 52 bytes on disk"); #pragma pack() -static_assert(sizeof(IsoTileRecord) == 52, "Isometric tile record layout changed"); static_assert(offsetof(IsoTileRecord, ExtraZOffset) == 16, "Isometric tile record layout changed"); static_assert(offsetof(IsoTileRecord, Height) == 40, "Isometric tile record layout changed"); static_assert(offsetof(IsoTileRecord, LowColor) == 43, "Isometric tile record layout changed"); @@ -201,10 +201,9 @@ class IsoTileSet IsoTileSet(IsoTileSet const & rvalue); IsoTileSet const & operator = (IsoTileSet const & rvalue); }; +static_assert(sizeof(IsoTileSet) == 20, "the TMP header is 16 bytes on disk, followed by the four-byte tile offsets"); #pragma pack() -static_assert(sizeof(IsoTileSet) == 20, "Isometric tile set header layout changed"); - /**************************************************************************** ** The tile type objects are controlled by this class. It specifies the form diff --git a/code/loco.cpp b/code/loco.cpp index 5027f2916..f75059f61 100644 --- a/code/loco.cpp +++ b/code/loco.cpp @@ -16,7 +16,6 @@ #include "cell.h" #include "classfactory.h" #include "coord.h" -#include "dbgprint.h" #include "foot.h" #include "globals.h" #include "map.h" @@ -185,10 +184,10 @@ bool LocomotionClass::Is_Ion_Sensitive(void) std::unique_ptr Create_Locomotor(ClassID const & classid) { - IPersistent * const object = Create_Object(classid); - ILocomotion * const locomotion = dynamic_cast(object); - if (locomotion == NULL) { - delete object; + std::unique_ptr object = Create_Object(classid); + ILocomotion * const locomotion = dynamic_cast(object.get()); + if (locomotion != nullptr) { + object.release(); } return(std::unique_ptr(locomotion)); } @@ -196,23 +195,14 @@ std::unique_ptr Create_Locomotor(ClassID const & classid) std::unique_ptr Load_Locomotor(SaveStreamClass & stream) { - SwizzleManagerClass::MarkType const mark = Swizzler.Mark(); - IPersistent * const object = Load_Object(stream); - ILocomotion * const locomotion = dynamic_cast(object); - if (object != NULL && locomotion == NULL) { - DebugString("Save record of %s at %u is not a locomotor\n", typeid(*object).name(), stream.Offset()); - Swizzler.Abandon(mark); - delete object; - stream.Fail(); - } - return(std::unique_ptr(locomotion)); + return(Load_Object_As(stream)); } ClassID Locomotion_Class_ID(ILocomotion * locomotion) { IPersistent const * const persist = dynamic_cast(locomotion); - return(persist != NULL ? persist->Class_ID() : ClassID()); + return(persist != nullptr ? persist->Class_ID() : ClassID()); } @@ -255,13 +245,6 @@ bool LocomotionClass::Save_Members(SaveStreamClass & stream, bool cleardirty) } -/// -/// Reads the members this locomotor describes back from the save stream. -/// The saved identity is handed to the swizzle system so that pointers elsewhere in the -/// save game can be remapped onto this locomotor, and the members follow. -/// -/// The stream to read from. -/// bool; Was the record read whole? bool LocomotionClass::Load_Members(SaveStreamClass & stream) { SwizzleIDType id = 0; @@ -287,7 +270,6 @@ void LocomotionClass::Serialize(SaveStreamClass & stream) stream.Serialize(LinkedTo); stream.Serialize(IsPowered); stream.Serialize(Dirty); - } @@ -571,4 +553,3 @@ int LocomotionClass::Apparent_Speed(void) { return(LinkedTo->Current_Speed()); } - diff --git a/code/loco.h b/code/loco.h index 24ae81d76..dc00c0c92 100644 --- a/code/loco.h +++ b/code/loco.h @@ -9,11 +9,13 @@ #pragma once -#include "coord.h" #include "classids.h" +#include "coord.h" #include "iloco.h" #include "persist.h" +#include + class FootClass; class SaveStreamClass; diff --git a/code/map.cpp b/code/map.cpp index ad65bac3f..45441ad4a 100644 --- a/code/map.cpp +++ b/code/map.cpp @@ -78,7 +78,6 @@ #include "dbgprint.h" #include "foot.h" #include "globals.h" -#include "hashtable.h" #include "house.h" #include "houstype.h" #include "incdec.h" @@ -109,6 +108,7 @@ #include #include #include +#include Cell const MapClass::RadiusOffset[] = { /* 0 */ Cell(0,0), @@ -209,7 +209,6 @@ FacingType BridgeSideFacings[BRIDGE_COUNT] = { }; -int SubzoneHash(unsigned int const & key); unsigned int Pick_Random_UInt(unsigned int start, unsigned int end); double Random_Fraction(void); @@ -246,22 +245,10 @@ MapClass::~MapClass(void) CellZones = NULL; } - if (ZoneAdjacency != NULL) { - delete ZoneAdjacency; - ZoneAdjacency = NULL; - } - if (CellSubzones != NULL) { delete [] CellSubzones; CellSubzones = NULL; } - - for (int i = 0; i < SUBZONE_COUNT; i++) { - if (SubzoneConnectionHashTable[i] != NULL) { - delete SubzoneConnectionHashTable[i]; - SubzoneConnectionHashTable[i] = NULL; - } - } } @@ -275,8 +262,7 @@ void MapClass::Serialize(SaveStreamClass & stream) { BASECLASS::Serialize(stream); - // ZoneAdjacency -- the zone graph. These tables are raw heap blocks that MouseClass::Load - // allocates and reads outside the archive. + // ZoneAdjacency -- scratch for the zone rebuild, which fills it again from the loaded terrain. // Zones stream.Serialize(ZoneCount); // ZoneConnections -- likewise part of the zone graph, read outside the archive. @@ -284,7 +270,7 @@ void MapClass::Serialize(SaveStreamClass & stream) stream.Serialize(CellZoneCount); // CellSubzones -- the subzone graph, grown again from the loaded terrain. // SubzoneTrackingEntryCount - // SubzoneConnectionHashTable + // SubzoneConnectionStaging // SubzoneTracking stream.Serialize(PendingBridgeCells); stream.Serialize(DirtyIceCells); @@ -313,6 +299,16 @@ void MapClass::Serialize(SaveStreamClass & stream) stream.Serialize(XSize); stream.Serialize(YSize); stream.Serialize(Size); + + // The cell array is reallocated to Size and every cell then installs itself in it by + // coordinate, so a saved extent other than the one this build lays out is refused + // before anything is sized from it. + if (stream.Is_Loading() + && (XSize != MAP_CELL_W || YSize != MAP_CELL_H || Size != MAP_CELL_TOTAL)) { + stream.Fail(); + return; + } + stream.Serialize(Crates); stream.Serialize(Redraws); stream.Serialize(TaggedCells); @@ -496,6 +492,26 @@ bool MapClass::Is_Valid(Cell const & cell) } +/// +/// Fetches the slot a cell coordinate names in the cell array. +/// A cell coordinate arrives from a saved game as two signed shorts, so this is what tells +/// a coordinate that names a slot from one that does not. +/// +/// Returns with the index into Array, or -1 when the coordinate names no slot. +int MapClass::Cell_Slot(Cell const & cell) const +{ + if (cell.X < 0 || cell.X >= MAP_CELL_W || cell.Y < 0 || cell.Y >= MAP_CELL_H) { + return(-1); + } + + int const cellnum = cell.X + cell.Y * MAP_CELL_W; + if (cellnum >= Array.Length()) { + return(-1); + } + return(cellnum); +} + + /*********************************************************************************************** * MapClass::One_Time -- Performs special one time initializations for the map. * * * @@ -526,14 +542,9 @@ void MapClass::One_Time(void) */ Alloc_Cells(); - if (ZoneAdjacency == NULL) { - ZoneAdjacency = new ZONE_PAIR_HASH_SET(20, 256, SubzoneHash); - } - int i; for (i = 0; i < ARRAY_SIZE(SubzoneTracking); i++) { SubzoneTracking[i].Clear(); - SubzoneConnectionHashTable[i] = new SUBZONE_CONNECTION_HASH_SET(20, 256, SubzoneHash); } for (i = 0; i < MZONE_COUNT; i++) { @@ -2781,26 +2792,13 @@ ObjectClass * MapClass::Close_Object(Coord const & coord) const /// -/// Packs a pair of zone numbers into a single key. -/// The zone adjacency table records which zones touch by storing pairs in this packed -/// form, so that a pair can be added and found as one value. +/// Stages a link between two subzones for the rebuild in progress to write out. +/// A rebuild stages the same pair many times and the first staging wins, so the cross block +/// flag is the one from the fill that reached the boundary first. /// -/// Returns with the two zone numbers packed into one key. -static unsigned Zone_Pack32(int zone1, int zone2) +static void Stage_Subzone_Link(SubzoneLinkStaging & staging, int subzone1, int subzone2, bool crossblock) { - return(zone2 | (zone1 << 16)); -} - - -/// -/// Packs a pair of zone numbers into a bucket index. -/// Use this routine to find which bucket of the zone adjacency table a pair of -/// neighboring zones is filed under. -/// -/// Returns with the bucket index for the pair of zones. -static unsigned Zone_Pack8(int zone1, int zone2) -{ - return(zone2 & 0xF | ((zone1 & 0xF) << 4)); + staging.try_emplace(ZonePair(subzone1, subzone2), crossblock); } @@ -2836,11 +2834,11 @@ int MapClass::Zone_Reset(void) DynamicVectorClass vec; vec.Set_Growth_Step(300); - ZoneAdjacency->Clear(); + ZoneAdjacency.clear(); for (i = 0; i < MZONE_COUNT; i++) { if (Zones[i] != NULL) { - delete Zones[i]; + delete [] Zones[i]; Zones[i] = NULL; } } @@ -2862,7 +2860,7 @@ int MapClass::Zone_Reset(void) LastAdjacentZone = 0; int span = Zone_Span(czone, zone, skip); if (span > bestspan) { - bestzone = (unsigned short)zone; + bestzone = zone; bestspan = span; } vec.Add(pass); @@ -2873,7 +2871,7 @@ int MapClass::Zone_Reset(void) } } - ZoneCount = (unsigned short)zone; + ZoneCount = zone; for (i = ZoneConnections.Count() - 1; i >= 0; i--) { ZoneConnectionClass * connection = &ZoneConnections[i]; @@ -2884,71 +2882,46 @@ int MapClass::Zone_Reset(void) if (to_zone < from_zone) { std::swap(to_zone, from_zone); } - ZoneAdjacency->Add_Object(ZONE_PAIR_HASH_SET::ObjectType(from_zone, to_zone)); + ZoneAdjacency.emplace(from_zone, to_zone); } } } - unsigned short * zone_degree = new unsigned short[ZoneCount]; - for (i = 0; i < ZoneCount; i++) { - zone_degree[i] = 0; - } + std::vector zone_degree(ZoneCount, 0); - for (i = 0; i < 256; i++) { - ZONE_PAIR_HASH_SET::BucketType & bucket = ZoneAdjacency->Buckets[i]; - j = bucket.Count(); - if (j > 0) { - ZONE_PAIR_HASH_SET::ObjectType * obj = &bucket[0]; - do { - unsigned int value = obj->Value; - unsigned short zone1 = LOWORD(value); - unsigned short zone2 = HIWORD(value); - zone_degree[zone1]++; - zone_degree[zone2]++; - obj++; - } while (--j); - } + for (ZonePair const & pair : ZoneAdjacency) { + zone_degree[pair.second]++; + zone_degree[pair.first]++; } - unsigned short ** zone_neighbors = new unsigned short *[ZoneCount]; + std::vector> zone_neighbors(ZoneCount); for (i = 0; i < ZoneCount; i++) { - zone_neighbors[i] = new unsigned short[zone_degree[i]]; + zone_neighbors[i].resize(zone_degree[i]); } for (i = 0; i < ZoneCount; i++) { zone_degree[i] = 0; } - for (i = 0; i < 256; i++) { - ZONE_PAIR_HASH_SET::BucketType & bucket = ZoneAdjacency->Buckets[i]; - j = bucket.Count(); - if (j > 0) { - ZONE_PAIR_HASH_SET::ObjectType * obj = &bucket[0]; - do { - unsigned int value = obj->Value; - unsigned short zone1 = LOWORD(value); - unsigned short zone2 = HIWORD(value); - zone_neighbors[zone1][zone_degree[zone1]] = zone2; - zone_neighbors[zone2][zone_degree[zone2]] = zone1; - zone_degree[zone1]++; - zone_degree[zone2]++; - obj++; - } while (--j); - } + for (ZonePair const & pair : ZoneAdjacency) { + zone_neighbors[pair.second][zone_degree[pair.second]] = pair.first; + zone_neighbors[pair.first][zone_degree[pair.first]] = pair.second; + zone_degree[pair.second]++; + zone_degree[pair.first]++; } - unsigned char * zone_passability = new unsigned char[ZoneCount]; + std::vector zone_passability(ZoneCount); for (i = 0; i < ZoneCount; i++) { zone_passability[i] = vec[i]; } - unsigned short * stack = new unsigned short[ZoneCount]; + std::vector stack(ZoneCount); for (MZoneType mzone = MZONE_FIRST; mzone < MZONE_COUNT; mzone++) { int next_movement_zone = 2; int * table = MZonePassability[mzone]; - unsigned short * nzone = new unsigned short[ZoneCount]; + int * nzone = new int[ZoneCount]; Zones[mzone] = nzone; for (j = 0; j < ZoneCount; j++) { @@ -2963,7 +2936,7 @@ int MapClass::Zone_Reset(void) int passable = table[zone_passability[zone_index]]; while (stackcount) { int current = stack[--stackcount]; - unsigned short * neighbors = zone_neighbors[current]; + std::vector const & neighbors = zone_neighbors[current]; int degree = zone_degree[current]; for (k = degree - 1; k >= 0; k--) { int neighbor = neighbors[k]; @@ -2977,35 +2950,17 @@ int MapClass::Zone_Reset(void) } } - nzone[0] = -1; - } - - for (i = 0 ; i < ZoneCount; i++) { - delete zone_neighbors[i]; + /* + * Ground off the playfield lands here. Callers read a zone of -1 as "any zone will + * do", so this must not be -1. + */ + nzone[0] = 0xFFFF; } - delete [] zone_passability; - delete [] zone_neighbors; - delete [] zone_degree; - delete [] stack; - return(Zones[0][bestzone]); } -/// -/// Computes the hash of a packed pair of zone numbers. -/// The zone adjacency table and the subzone connection tables are all constructed with -/// this routine as their hash function. -/// -/// The packed pair of zone numbers to hash. -/// Returns with the bucket that the pair belongs in. -int SubzoneHash(unsigned int const & key) -{ - return(key & 0xF | ((key >> 12) & 0xF)); -} - - /*********************************************************************************************** * MapClass::Zone_Span -- Flood fills the specified zone from the cell origin. * * * @@ -3052,8 +3007,8 @@ int MapClass::Zone_Span(CellZoneStruct * data, int zone, int & skip) } int begin_zone = begin->ZoneID; - if (begin_zone != 0 && (abs(begin->Height - cell_height) < 2 || nopass) && begin_zone != LastAdjacentZone && begin_zone != (unsigned short)zone) { - ZoneAdjacency->Add_Object(ZONE_PAIR_HASH_SET::ObjectType(begin_zone, (unsigned short)zone)); + if (begin_zone != 0 && (abs(begin->Height - cell_height) < 2 || nopass) && begin_zone != LastAdjacentZone && begin_zone != zone) { + ZoneAdjacency.emplace(begin_zone, zone); LastAdjacentZone = begin_zone; } @@ -3072,8 +3027,8 @@ int MapClass::Zone_Span(CellZoneStruct * data, int zone, int & skip) } int end_zone = end->ZoneID; - if (end_zone != 0 && (abs(end->Height - cell_height) < 2 || nopass) && end_zone != LastAdjacentZone && end_zone != (unsigned short)zone) { - ZoneAdjacency->Add_Object(ZONE_PAIR_HASH_SET::ObjectType(end_zone, (unsigned short)zone)); + if (end_zone != 0 && (abs(end->Height - cell_height) < 2 || nopass) && end_zone != LastAdjacentZone && end_zone != zone) { + ZoneAdjacency.emplace(end_zone, zone); LastAdjacentZone = end_zone; } @@ -3112,8 +3067,8 @@ int MapClass::Zone_Span(CellZoneStruct * data, int zone, int & skip) fbegin++; } } else { - if (zzone != (unsigned short)zone && zzone != LastAdjacentZone && (abs(fbegin->Height - adjacent->Height) < 2 || nopass)) { - ZoneAdjacency->Add_Object(ZONE_PAIR_HASH_SET::ObjectType(zzone, (unsigned short)zone)); + if (zzone != zone && zzone != LastAdjacentZone && (abs(fbegin->Height - adjacent->Height) < 2 || nopass)) { + ZoneAdjacency.emplace(zzone, zone); LastAdjacentZone = zzone; } fbegin++; @@ -3143,8 +3098,8 @@ int MapClass::Zone_Span(CellZoneStruct * data, int zone, int & skip) fbegin2++; } } else { - if (id != (unsigned short)zone && id != LastAdjacentZone && (abs(fbegin2->Height - adjacent->Height) < 2 || nopass)) { - ZoneAdjacency->Add_Object(ZONE_PAIR_HASH_SET::ObjectType(id, (unsigned short)zone)); + if (id != zone && id != LastAdjacentZone && (abs(fbegin2->Height - adjacent->Height) < 2 || nopass)) { + ZoneAdjacency.emplace(id, zone); LastAdjacentZone = id; } fbegin2++; @@ -9734,22 +9689,16 @@ void MapClass::Shutdown(void) for (index = 0; index < MZONE_COUNT; index++) { if (Zones[index] != NULL) { - delete Zones[index]; + delete [] Zones[index]; Zones[index] = NULL; } } - if (ZoneAdjacency != NULL) { - delete ZoneAdjacency; - ZoneAdjacency = NULL; - } + ZoneAdjacency.clear(); for (index = 0; index < SUBZONE_COUNT; index++) { SubzoneTracking[index].Clear(); - if (SubzoneConnectionHashTable[index] != NULL) { - delete SubzoneConnectionHashTable[index]; - SubzoneConnectionHashTable[index] = NULL; - } + SubzoneConnectionStaging[index].clear(); } VeinholeMonsterClass::Reset(); @@ -10034,15 +9983,10 @@ void MapClass::Reset_All_Subzones(void) /// The subzone level to rebuild. void MapClass::Reset_Subzone(int subzone) { - /* - * Clear out every bucket of the connection hash set for this subzone level. - */ - SUBZONE_CONNECTION_HASH_SET * set = SubzoneConnectionHashTable[subzone]; + SubzoneConnectionStaging[subzone].clear(); + DynamicVectorClass * track = &SubzoneTracking[subzone]; CellSubzoneStruct * subend = &CellSubzones[CellZoneCount]; - for (int bucket = 0; bucket < set->NumBuckets; bucket++) { - set->Buckets[bucket].Clear(); - } /* * Reset the per-cell subzone identifiers for this level and refresh the @@ -10109,8 +10053,8 @@ void MapClass::Reset_Subzone(int subzone) track->Add(SubzoneTrackingStruct()); - SubzoneTrackingStruct * added = &(*track)[(unsigned short)entry_count]; - added->ParentSubzoneID = (unsigned short)parent; + SubzoneTrackingStruct * added = &(*track)[entry_count]; + added->ParentSubzoneID = parent; added->Passability = (PassabilityType)passability; added->Connections.Set_Growth_Step(16); added->ThreatRegion = (short)X / REGION_WIDTH + MAP_REGION_WIDTH * ((short)Y / REGION_HEIGHT) + (MAP_REGION_WIDTH + 1); @@ -10133,40 +10077,14 @@ void MapClass::Reset_Subzone(int subzone) } } - SubzoneTrackingEntryCount[subzone] = (unsigned short)entry_count; + SubzoneTrackingEntryCount[subzone] = entry_count; /* * Re-register the zone-to-zone connections for this subzone level. */ Register_Subzone_Zone_Connections(subzone); - /* - * Walk every bucket of the connection hash set and register both endpoints - * of each recorded connection into the subzone tracking list. - */ - for (int bucket_index = 0; bucket_index < 256; bucket_index++) { - if (set->Buckets[bucket_index].Count() > 0) { - SUBZONE_CONNECTION_HASH_SET::ObjectType * object = &set->Buckets[bucket_index][0]; - for (int index = set->Buckets[bucket_index].Count(); index > 0; index--) { - unsigned packed = object->Value.SubzoneID; - WORD low = LOWORD(packed); - WORD high = HIWORD(packed); - bool costly = object->Value.IsCrossBlock; - - SubzoneConnectionStruct conn; - conn.SubzoneID = high; - conn.IsCrossBlock = costly; - (*track)[low].Connections.Add(conn); - - SubzoneConnectionStruct conn2; - conn2.SubzoneID = low; - conn2.IsCrossBlock = costly; - (*track)[high].Connections.Add(conn2); - - object++; - } - } - } + Register_Staged_Subzone_Connections(subzone); } @@ -10189,7 +10107,6 @@ int MapClass::Subzone_Span(CellSubzoneStruct * seed, int subzone_level, int subz CellSubzoneStruct * begin = seed; CellSubzoneStruct * end = seed; - SUBZONE_CONNECTION_HASH_SET::ObjectType entry; int x = cell.X; int y = cell.Y; @@ -10202,7 +10119,7 @@ int MapClass::Subzone_Span(CellSubzoneStruct * seed, int subzone_level, int subz int end_x = x; int ymax = bounds.Height + bounds.Y - 1; - SUBZONE_CONNECTION_HASH_SET * table = SubzoneConnectionHashTable[subzone_level]; + SubzoneLinkStaging & staging = SubzoneConnectionStaging[subzone_level]; int prev_height = seed->Height; /* @@ -10216,7 +10133,7 @@ int MapClass::Subzone_Span(CellSubzoneStruct * seed, int subzone_level, int subz if (abs(begin->Height - prev_height) >= 2) { break; } - begin->SubzoneID[subzone_level] = (short)subzone_id; + begin->SubzoneID[subzone_level] = subzone_id; prev_height = begin->Height; int prev_zone = begin[-1].ZoneID; begin--; @@ -10234,10 +10151,7 @@ int MapClass::Subzone_Span(CellSubzoneStruct * seed, int subzone_level, int subz if (In_Local_Radar(probe, true)) { probe = Cell(x + 1, y); if (In_Local_Radar(probe, true)) { - int packed_pair = Zone_Pack32(begin_subzone, (unsigned short)subzone_id); - entry.Value.IsCrossBlock = false; - entry.Key = entry.Value.SubzoneID = packed_pair; - table->Add_Object(Zone_Pack8(begin_subzone, (unsigned short)subzone_id), entry); + Stage_Subzone_Link(staging, begin_subzone, subzone_id, false); last_adjacent = begin_subzone; } } @@ -10255,7 +10169,7 @@ int MapClass::Subzone_Span(CellSubzoneStruct * seed, int subzone_level, int subz if (abs(end->Height - prev_height) >= 2) { break; } - end->SubzoneID[subzone_level] = (short)subzone_id; + end->SubzoneID[subzone_level] = subzone_id; prev_height = end->Height; end++; end_x++; @@ -10269,10 +10183,7 @@ int MapClass::Subzone_Span(CellSubzoneStruct * seed, int subzone_level, int subz if (In_Local_Radar(probe, true)) { probe = Cell(end_x - 1, y); if (In_Local_Radar(probe, true)) { - int packed_pair = Zone_Pack32(end_subzone, (unsigned short)subzone_id); - entry.Value.IsCrossBlock = false; - entry.Key = entry.Value.SubzoneID = packed_pair; - table->Add_Object(Zone_Pack8(end_subzone, (unsigned short)subzone_id), entry); + Stage_Subzone_Link(staging, end_subzone, subzone_id, false); last_adjacent = end_subzone; } } @@ -10317,24 +10228,14 @@ int MapClass::Subzone_Span(CellSubzoneStruct * seed, int subzone_level, int subz } if (shadow_subzone != 0 || y <= ymin || x < xmin || x > xmax) { - if (shadow_subzone != (unsigned short)subzone_id && shadow_subzone != last_adjacent) { + if (shadow_subzone != subzone_id && shadow_subzone != last_adjacent) { if (shadow_subzone != 0) { if (abs(span_ptr->Height - above->Height) < 2 && In_Local_Radar(span_cell, true)) { Cell shadow_cell; shadow_cell.Y = y - 1; shadow_cell.X = x; if (In_Local_Radar(shadow_cell, true)) { - int packed_pair = Zone_Pack32(shadow_subzone, (unsigned short)subzone_id); - if (x < xmin) { - entry.Value.IsCrossBlock = true; - } else { - entry.Value.IsCrossBlock = false; - if (x > xmax) { - entry.Value.IsCrossBlock = true; - } - } - entry.Key = entry.Value.SubzoneID = packed_pair; - table->Add_Object(Zone_Pack8(shadow_subzone, (unsigned short)subzone_id), entry); + Stage_Subzone_Link(staging, shadow_subzone, subzone_id, x < xmin || x > xmax); last_adjacent = shadow_subzone; } } @@ -10386,24 +10287,14 @@ int MapClass::Subzone_Span(CellSubzoneStruct * seed, int subzone_level, int subz } if (shadow_subzone != 0 || y >= ymax || x < xmin || x > xmax) { - if (shadow_subzone != (unsigned short)subzone_id && shadow_subzone != last_adjacent) { + if (shadow_subzone != subzone_id && shadow_subzone != last_adjacent) { if (shadow_subzone != 0) { if (abs(below->Height - span_ptr->Height) < 2 && In_Local_Radar(span_cell, true)) { Cell shadow_cell; shadow_cell.X = x; shadow_cell.Y = y + 1; if (In_Local_Radar(shadow_cell, true)) { - int packed_pair = Zone_Pack32(shadow_subzone, (unsigned short)subzone_id); - if (x < xmin) { - entry.Value.IsCrossBlock = true; - } else { - entry.Value.IsCrossBlock = false; - if (x > xmax) { - entry.Value.IsCrossBlock = true; - } - } - entry.Key = entry.Value.SubzoneID = packed_pair; - table->Add_Object(Zone_Pack8(shadow_subzone, (unsigned short)subzone_id), entry); + Stage_Subzone_Link(staging, shadow_subzone, subzone_id, x < xmin || x > xmax); last_adjacent = shadow_subzone; } } @@ -10449,8 +10340,7 @@ void MapClass::Register_Subzone_Zone_Connections(int subzone) /// The subzone level whose staging set receives the links. void MapClass::Register_Zone_Connection_Entries(ZoneConnectionClass & connection, int index) { - SUBZONE_CONNECTION_HASH_SET::ObjectType connstr; - SUBZONE_CONNECTION_HASH_SET * set = SubzoneConnectionHashTable[index]; + SubzoneLinkStaging & staging = SubzoneConnectionStaging[index]; Cell from = connection.From; Cell to = connection.To; @@ -10493,36 +10383,47 @@ void MapClass::Register_Zone_Connection_Entries(ZoneConnectionClass & connection int from_zone = CellSubzones[Get_Cell_Subzone_Index(from)].SubzoneID[index]; int to_zone = CellSubzones[Get_Cell_Subzone_Index(to)].SubzoneID[index]; - int value = Zone_Pack32(from_zone, to_zone); - int bucket_index = Zone_Pack8(from_zone, to_zone); - connstr.Key = value; - connstr.Value = SubzoneConnectionStruct(value); - connstr.Value.IsCrossBlock = 0; - set->Add_Object(bucket_index, connstr); + Stage_Subzone_Link(staging, from_zone, to_zone, false); } { int from_zone = CellSubzones[Get_Cell_Subzone_Index(enter1)].SubzoneID[index]; int to_zone = CellSubzones[Get_Cell_Subzone_Index(newcell1)].SubzoneID[index]; - int value = Zone_Pack32(from_zone, to_zone); - int bucket_index = Zone_Pack8(from_zone, to_zone); - connstr.Key = value; - connstr.Value = SubzoneConnectionStruct(value); - connstr.Value.IsCrossBlock = 0; - set->Add_Object(bucket_index, connstr); + Stage_Subzone_Link(staging, from_zone, to_zone, false); } { int from_zone = CellSubzones[Get_Cell_Subzone_Index(enter2)].SubzoneID[index]; int to_zone = CellSubzones[Get_Cell_Subzone_Index(newcell2)].SubzoneID[index]; - int value = Zone_Pack32(from_zone, to_zone); - int bucket_index = Zone_Pack8(from_zone, to_zone); - connstr.Key = value; - connstr.Value = SubzoneConnectionStruct(value); - connstr.Value.IsCrossBlock = 0; - set->Add_Object(bucket_index, connstr); + Stage_Subzone_Link(staging, from_zone, to_zone, false); + } +} + + +/// +/// Writes the links staged for one subzone level into the two subzones each one joins. +/// Every link is recorded in both subzones, so the graph the route search walks is undirected. +/// The route search reads a subzone's neighbors in list order and settles a tie among equally +/// cheap blocks by which of them it reached first, so the order the links are written in is +/// the order it breaks those ties in. +/// +/// The subzone level whose staged links are to be written out. +void MapClass::Register_Staged_Subzone_Connections(int subzone) +{ + DynamicVectorClass & track = SubzoneTracking[subzone]; + + for (auto const & [link, crossblock] : SubzoneConnectionStaging[subzone]) { + SubzoneConnectionStruct conn; + conn.SubzoneID = link.first; + conn.IsCrossBlock = crossblock; + track[link.second].Connections.Add(conn); + + SubzoneConnectionStruct conn2; + conn2.SubzoneID = link.second; + conn2.IsCrossBlock = crossblock; + track[link.first].Connections.Add(conn2); } } @@ -10783,7 +10684,7 @@ Cell MapClass::Find_Bridge_End_Cell_For_Subzone(Cell const & cell, int subzone_l /// The unit whose ability to enter a cell decides what counts as /// blocked. /// bool; Is part of the subzone itself cut off from the starting cell? -bool MapClass::Build_Reachable_Subzones(CellClass * cptr, int subzone_level, DynamicVectorClass const & connections, FootClass const * foot) +bool MapClass::Build_Reachable_Subzones(CellClass * cptr, int subzone_level, DynamicVectorClass & connections, FootClass const * foot) { /* * The flood is bounded by one coarse block, so the scratch it needs is a fixed size and @@ -10796,7 +10697,7 @@ bool MapClass::Build_Reachable_Subzones(CellClass * cptr, int subzone_level, Dyn int dim = 1 << (subzone_level + 1); - DynamicVectorClass list; + DynamicVectorClass list; list.Clear(); if (dim > 0) { @@ -10805,9 +10706,9 @@ bool MapClass::Build_Reachable_Subzones(CellClass * cptr, int subzone_level, Dyn } } - unsigned short other_subzone = 0; + int other_subzone = 0; int count = 1; - unsigned short start_subzone = CellSubzones[Get_Cell_Subzone_Index(cptr->CellID)].SubzoneID[subzone_level]; + int start_subzone = CellSubzones[Get_Cell_Subzone_Index(cptr->CellID)].SubzoneID[subzone_level]; _subzone_flood_stack[0] = cptr; _subzone_flood_visited[(dim - 1) & cptr->CellID.X][(dim - 1) & cptr->CellID.Y] = 1; @@ -10825,7 +10726,7 @@ bool MapClass::Build_Reachable_Subzones(CellClass * cptr, int subzone_level, Dyn CellClass * adj = &cur->Adjacent_Cell(facing); int ax = (dim - 1) & adj->CellID.X; int ay = (dim - 1) & adj->CellID.Y; - unsigned short adj_subzone = CellSubzones[Get_Cell_Subzone_Index(adj->CellID)].SubzoneID[subzone_level]; + int adj_subzone = CellSubzones[Get_Cell_Subzone_Index(adj->CellID)].SubzoneID[subzone_level]; if (foot->Can_Enter_Cell(adj, facing, adj->Height, NULL, true) == MOVE_OK || mzone_table[adj->Passability] != TRAVERSAL_PASSABLE) { @@ -10877,7 +10778,7 @@ bool MapClass::Build_Reachable_Subzones(CellClass * cptr, int subzone_level, Dyn Cell c = cptr->CellID + Cell(bx, by); if (In_Local_Radar(c, true)) { - if (start_subzone == (unsigned short)CellSubzones[Get_Cell_Subzone_Index(c)].SubzoneID[subzone_level] && !_subzone_flood_visited[row][col]) { + if (start_subzone == CellSubzones[Get_Cell_Subzone_Index(c)].SubzoneID[subzone_level] && !_subzone_flood_visited[row][col]) { return(true); } } @@ -10899,7 +10800,7 @@ bool MapClass::Build_Reachable_Subzones(CellClass * cptr, int subzone_level, Dyn } } if (j == -1) { - ((DynamicVectorClass &)connections).Add(sub); + connections.Add(sub); } conn--; @@ -10941,13 +10842,15 @@ void MapClass::Update_Cell_Subzones(Cell const & cell) bounds.Y = cell.Y - cell.Y % bounds.Height; /* - * Clear out every bucket of the connection hash set for this level. + * The window is aligned down to the block size, so its far edge can reach past + * the last row and column the zone tables hold. */ - DynamicVectorClass collected; - SUBZONE_CONNECTION_HASH_SET * set = SubzoneConnectionHashTable[subzone]; - for (int bucket = 0; bucket < set->NumBuckets; bucket++) { - set->Buckets[bucket].Clear(); - } + int xend = std::min(bounds.X + bounds.Width, PlayRect.Width + PlayRect.Height + 1); + int yend = std::min(bounds.Y + bounds.Height, PlayRect.Width + PlayRect.Height + 1); + + SubzoneConnectionStaging[subzone].clear(); + + DynamicVectorClass collected; DynamicVectorClass * track = &SubzoneTracking[subzone]; @@ -10955,10 +10858,10 @@ void MapClass::Update_Cell_Subzones(Cell const & cell) * Collect the distinct subzone ids that currently occupy the window and * clear each cell's subzone id for this level (refreshing its zone id). */ - for (y = bounds.Y; y < bounds.Y + bounds.Height; y++) { - for (x = bounds.X; x < bounds.Width + bounds.X; x++) { - CellSubzoneStruct * subptr = &CellSubzones[(short)x + (short)y * (PlayRect.Width + PlayRect.Height + 1)]; - unsigned short subid = subptr->SubzoneID[subzone]; + for (y = bounds.Y; y < yend; y++) { + for (x = bounds.X; x < xend; x++) { + CellSubzoneStruct * subptr = &CellSubzones[Get_Cell_Zone_Index(Cell(x, y))]; + int subid = subptr->SubzoneID[subzone]; if (subid != 0) { int found; for (found = collected.Count() - 1; found >= 0; found--) { @@ -10971,7 +10874,7 @@ void MapClass::Update_Cell_Subzones(Cell const & cell) } } subptr->SubzoneID[subzone] = 0; - subptr->ZoneID = CellZones[(short)x + (short)y * (PlayRect.Width + PlayRect.Height + 1)].ZoneID; + subptr->ZoneID = CellZones[Get_Cell_Zone_Index(Cell(x, y))].ZoneID; } } @@ -10981,7 +10884,7 @@ void MapClass::Update_Cell_Subzones(Cell const & cell) * then clear its own connection list. */ for (int idx = collected.Count() - 1; idx >= 0; idx--) { - unsigned short subid = collected[idx]; + int subid = collected[idx]; SubzoneTrackingStruct * entry = &SubzoneTracking[subzone][subid]; for (int i = entry->Connections.Count() - 1; i >= 0; i--) { SubzoneTrackingStruct * neighbor = &SubzoneTracking[subzone][entry->Connections[i].SubzoneID]; @@ -11001,16 +10904,16 @@ void MapClass::Update_Cell_Subzones(Cell const & cell) * window, recording a tracking entry for each new subzone. */ int entry_count = SubzoneTrackingEntryCount[subzone]; - for (y = bounds.Y; y < bounds.Y + bounds.Height; y++) { - for (x = bounds.X; x < bounds.Width + bounds.X; x++) { + for (y = bounds.Y; y < yend; y++) { + for (x = bounds.X; x < xend; x++) { Cell cella; cella.X = x; cella.Y = y; if (In_Local_Radar(cella)) { - CellSubzoneStruct * subptr = &CellSubzones[(short)y * (PlayRect.Width + PlayRect.Height + 1) + (short)x]; - int passability = CellZones[(short)y * (PlayRect.Width + PlayRect.Height + 1) + (short)x].Passability; + CellSubzoneStruct * subptr = &CellSubzones[Get_Cell_Zone_Index(Cell(x, y))]; + int passability = CellZones[Get_Cell_Zone_Index(Cell(x, y))].Passability; if (passability != PASSABLE_OUTSIDE && subptr->SubzoneID[subzone] == 0) { trycell.Y = y; @@ -11024,16 +10927,12 @@ void MapClass::Update_Cell_Subzones(Cell const & cell) if (subzone != 2) { parent = subptr->SubzoneID[subzone + 1]; } - added->ParentSubzoneID = (unsigned short)parent; + added->ParentSubzoneID = parent; added->Passability = (PassabilityType)passability; added->Connections.Set_Growth_Step(16); added->ThreatRegion = (short)x / REGION_WIDTH + MAP_REGION_WIDTH * ((short)y / REGION_HEIGHT) + (MAP_REGION_WIDTH + 1); entry_count++; - if (entry_count == 0) { - Reset_All_Subzones(); - return; - } } } } @@ -11055,33 +10954,7 @@ void MapClass::Update_Cell_Subzones(Cell const & cell) } } - /* - * Walk every bucket of the connection hash set and register both endpoints - * of each recorded connection into the subzone tracking list. - */ - for (int bucket_index = 0; bucket_index < 256; bucket_index++) { - if (set->Buckets[bucket_index].Count() > 0) { - SUBZONE_CONNECTION_HASH_SET::ObjectType * object = &set->Buckets[bucket_index][0]; - for (int index = set->Buckets[bucket_index].Count(); index > 0; index--) { - unsigned packed = object->Value.SubzoneID; - WORD low = LOWORD(packed); - WORD high = HIWORD(packed); - bool costly = object->Value.IsCrossBlock; - - SubzoneConnectionStruct conn; - conn.SubzoneID = high; - conn.IsCrossBlock = costly; - (*track)[low].Connections.Add(conn); - - SubzoneConnectionStruct conn2; - conn2.SubzoneID = low; - conn2.IsCrossBlock = costly; - (*track)[high].Connections.Add(conn2); - - object++; - } - } - } + Register_Staged_Subzone_Connections(subzone); } /* @@ -11090,12 +10963,14 @@ void MapClass::Update_Cell_Subzones(Cell const & cell) */ int basex = cell.X - cell.X % 8; int basey = cell.Y - cell.Y % 8; - for (y = basey; y < basey + 8; y++) { - for (x = basex; x < basex + 8; x++) { + int parent_xend = std::min(basex + 8, PlayRect.Width + PlayRect.Height + 1); + int parent_yend = std::min(basey + 8, PlayRect.Width + PlayRect.Height + 1); + for (y = basey; y < parent_yend; y++) { + for (x = basex; x < parent_xend; x++) { trycell.X = x; trycell.Y = y; if (In_Local_Radar(trycell)) { - CellSubzoneStruct * subptr = &CellSubzones[(short)x + (short)y * (PlayRect.Width + PlayRect.Height + 1)]; + CellSubzoneStruct * subptr = &CellSubzones[Get_Cell_Zone_Index(Cell(x, y))]; for (int level = 0; level < 2; level++) { SubzoneTracking[level][subptr->SubzoneID[level]].ParentSubzoneID = subptr->SubzoneID[level + 1]; } diff --git a/code/map.h b/code/map.h index e43c8b26d..ce521d7a5 100644 --- a/code/map.h +++ b/code/map.h @@ -54,8 +54,6 @@ class CellClass; class BuildingTypeClass; class FootClass; class SaveStreamClass; -template -class HashTableClass; class MapClass: public GScreenClass @@ -89,6 +87,7 @@ class MapClass: public GScreenClass int ID(CellClass * ptr) {return(Array.ID(ptr));}; int ID(CellClass & ptr) {return(Array.ID(&ptr));}; bool Is_Valid(Cell const & cell); + int Cell_Slot(Cell const & cell) const; /* ** Initialization @@ -147,11 +146,12 @@ class MapClass: public GScreenClass int Subzone_Span(CellSubzoneStruct * seed, int subzone_level, int subzone_id, Rect const & bounds, Cell const & cell); void Register_Subzone_Zone_Connections(int subzone); void Register_Zone_Connection_Entries(ZoneConnectionClass & connection, int index); + void Register_Staged_Subzone_Connections(int subzone); Cell Get_Bridge_Zone_Connection_Cell(CellClass * cptr, bool isbridge); Cell Get_Zone_Connection_Destination(Cell const & cell, Cell const & reference); Cell Find_Bridge_Span_End_Cell(Cell const & cell, Cell const & reference); Cell Find_Bridge_End_Cell_For_Subzone(Cell const & cell, int subzone_level, int subzone_id); - bool Build_Reachable_Subzones(CellClass * cptr, int subzone_level, DynamicVectorClass const & connections, FootClass const * foot); + bool Build_Reachable_Subzones(CellClass * cptr, int subzone_level, DynamicVectorClass & connections, FootClass const * foot); void Update_Cell_Subzones(Cell const & cell); void Register_Subzone_Connection(ZoneConnectionClass * connection); void Unregister_Subzone_Connection(ZoneConnectionClass * connection); @@ -347,12 +347,13 @@ class MapClass: public GScreenClass virtual void Set_Local_Dimensions(Rect const & size); /* - * This is the set of base terrain zones that touch one another, keyed by a packed - * pair of zone IDs. It is filled while the zones are being flood filled, and gains a - * pair for every bridge and tunnel that is currently intact, so that the zones can - * then be unioned into the movement zones of each MZoneType. + * These are the pairs of base terrain zones that touch one another, filled while the + * zones are flood filled and gaining a pair for every intact bridge and tunnel, so + * that the zones can be unioned into the movement zones of each MZoneType. A pair is + * held as the site that found it wrote it, so one touching may appear as both (a,b) + * and (b,a). */ - ZONE_PAIR_HASH_SET * ZoneAdjacency; + ZonePairSet ZoneAdjacency; /* * This records the movement zones for this map. Cells share the same zone @@ -362,7 +363,7 @@ class MapClass: public GScreenClass * number, so two cells are mutually reachable by a given movement type only * when their zones map to the same number. */ - unsigned short * Zones[MZONE_COUNT]; + int * Zones[MZONE_COUNT]; /* * This is the number of base terrain zones the last rebuild produced, and thus the @@ -392,12 +393,12 @@ class MapClass: public GScreenClass /* * This is the subzone graph itself, one slot per level of coarseness. SubzoneTracking * holds a level's subzone records and SubzoneTrackingEntryCount how many of them are - * valid, while SubzoneConnectionHashTable is only scratch: a rebuild gathers adjacency - * pairs there so that duplicates fall out, then unpacks them into the records and - * clears it again. + * valid, while SubzoneConnectionStaging is only scratch: a rebuild clears it, gathers + * the level's adjacency pairs there so that duplicates fall out, then unpacks them + * into the records. What it holds after that is left until the next rebuild clears it. */ int SubzoneTrackingEntryCount[SUBZONE_COUNT]; - SUBZONE_CONNECTION_HASH_SET * SubzoneConnectionHashTable[SUBZONE_COUNT]; + SubzoneLinkStaging SubzoneConnectionStaging[SUBZONE_COUNT]; DynamicVectorClass SubzoneTracking[SUBZONE_COUNT]; /* @@ -555,5 +556,4 @@ class MapClass: public GScreenClass extern CellClass BlubCell; -int SubzoneHash(unsigned int const & key); extern int MZonePassability[MZONE_COUNT][PASSABLE_COUNT]; diff --git a/code/mapgen.cpp b/code/mapgen.cpp index 4e5762f7a..7cb487819 100644 --- a/code/mapgen.cpp +++ b/code/mapgen.cpp @@ -41,6 +41,7 @@ #include "language/language.h" #include "netshare.h" #include "nodes.h" +#include "priority.h" #include "overtype.h" #include "pcx.h" #include "progress.h" @@ -67,6 +68,8 @@ #include #include +#include +#include bool (*RMGCallback)() = MapGen_Call_Back; @@ -949,9 +952,8 @@ bool MapRegionClass::Split_Region(void) } int heap_size = (2 * CellCount) + 10; - CellNode * nodes = new CellNode[heap_size]; - PriorityQueueClass * queue = new PriorityQueueClass((2 * CellCount) + 10); - queue->Clear(); + PriorityQueueClass queue((2 * CellCount) + 10); + queue.Clear(); CellData::Reset_Spreads(false); @@ -963,17 +965,17 @@ bool MapRegionClass::Split_Region(void) unsigned int target_count = Pick_Random_UInt(CellCount / 8, CellCount / 3); unsigned int seed_index = Pick_Random_UInt(0, Cells.Count() - 1); - int node_count = 1; Cell seed = Cells[seed_index]; - nodes[0].Element = seed; - nodes[0].Score = 0.0f; + CellNode seed_node; + seed_node.Element = seed; + seed_node.Score = 0.0f; MapRegionClass::Get_Cell_Data(seed).SpreadID = -3; - queue->Insert(nodes[0]); + queue.Insert(seed_node); - CellNode * node = queue->Extract_Min(); + std::optional node = queue.Extract_Min(); double dist = (unsigned int)RMGRandom() * (DEG_TO_RAD(360) / UINT_MAX); - while (node != NULL) { + while (node) { if (spread_count >= (int)target_count) { break; } @@ -981,39 +983,36 @@ bool MapRegionClass::Split_Region(void) CellData::Set_Region(node->Element, -3); int cell_index = node->Element.X + MapCellStride * node->Element.Y; - CellNode * newnode = &nodes[node_count]; for (FacingType dir = FACING_FIRST; dir < FACING_COUNT; dir = (FacingType)(dir + FACING_90)) { Cell ncell = Adjacent_Cell(node->Element, dir); if (My_In_Radar(ncell)) { if (CellData::Get_Region(ncell) == -2 && CellData::Get_Spread(ncell) != -3 && Map[ncell].Is_Tile_Clear()) { - newnode->Element = ncell; - newnode->Score = Get_Angle_Score(seed, ncell, dist); + CellNode newnode; + newnode.Element = ncell; + newnode.Score = Get_Angle_Score(seed, ncell, dist); MapRegionClass::Get_Cell_Data(cell_index + AStarFacingToOffset[dir]).SpreadID = -3; - node_count++; - queue->Insert(*newnode++); + queue.Insert(newnode); } } } spread_count++; dist += Sample_Normal(0, DEG_TO_RAD(22.5)); - node = queue->Extract_Min(); + node = queue.Extract_Min(); } CellData::Reset_Spreads(false); - CellNode * remaining = queue->Extract_Min(); + std::optional remaining = queue.Extract_Min(); DynamicVectorClass ncells; - while (remaining != NULL) { + while (remaining) { Cell c = remaining->Element; MapRegionClass::Get_Cell_Data(c).RegionID = -3; - remaining = queue->Extract_Min(); + remaining = queue.Extract_Min(); } - delete[] nodes; - delete queue; DynamicVectorClass new_regions; for (i = Cells.Count() - 1; i >= 0; i--) { @@ -4895,8 +4894,7 @@ bool MapGeneratorClass::Seed_Lake(const Cell & cell) return(false); } - CellNode *nodes = new CellNode[std::max((2 * spread_limit) + 2, 100)]; - PriorityQueueClass *queue = new PriorityQueueClass(std::max((2 * spread_limit) + 2, 100)); + PriorityQueueClass queue(std::max((2 * spread_limit) + 2, 100)); Map.Reset_Iterator(); CellClass *iter = Map.Iterate(); @@ -4963,21 +4961,21 @@ bool MapGeneratorClass::Seed_Lake(const Cell & cell) double mean = (double)(spread_limit / 3); int spread_count = (int)Sample_Truncated_Normal(mean, scale, 75.0, (double)max_spread); - queue->Clear(); + queue.Clear(); - int marker_index = 1; - nodes[0].Element = seed_cell; - nodes[0].Score = 0.0f; + CellNode seed_node; + seed_node.Element = seed_cell; + seed_node.Score = 0.0f; Set_Cell_Data_Spread(seed_cell, WorkingRegionID); - queue->Insert(nodes[0]); + queue.Insert(seed_node); DynamicVectorClass cells; cells.Set_Growth_Step(2000); - CellNode *node = queue->Extract_Min(); + std::optional node = queue.Extract_Min(); if (spread_count > 0) { do { - if (node == NULL) { + if (!node) { break; } if (!success) { @@ -4995,7 +4993,6 @@ bool MapGeneratorClass::Seed_Lake(const Cell & cell) cells.Add(cellptr->CellID); - CellNode *newnode = &nodes[marker_index]; for (FacingType dir = FACING_FIRST; dir < FACING_COUNT; dir = FacingType(dir + FACING_90)) { Cell newcell = Adjacent_Cell(node->Element, dir); if (My_In_Radar(newcell)) { @@ -5005,22 +5002,22 @@ bool MapGeneratorClass::Seed_Lake(const Cell & cell) success = false; } } else { - newnode->Element = newcell; - newnode->Score = Get_Spread_Score(seed_cell, newcell, seeded_count); + CellNode newnode; + newnode.Element = newcell; + newnode.Score = Get_Spread_Score(seed_cell, newcell, seeded_count); Set_Cell_Data_Spread(newcell, WorkingRegionID); - marker_index++; - queue->Insert(*newnode++); + queue.Insert(newnode); } } } seeded_count++; - node = queue->Extract_Min(); + node = queue.Extract_Min(); } while (seeded_count < spread_count); } - CellNode *remaining = queue->Extract_Min(); - while (remaining != NULL) { + std::optional remaining = queue.Extract_Min(); + while (remaining) { if (!success) { break; } @@ -5039,7 +5036,7 @@ bool MapGeneratorClass::Seed_Lake(const Cell & cell) success = false; } - remaining = queue->Extract_Min(); + remaining = queue.Extract_Min(); seeded_count++; } @@ -5058,7 +5055,7 @@ bool MapGeneratorClass::Seed_Lake(const Cell & cell) int swamp_count = Pick_Random_UInt(0, 2); for (int i = 0; i < swamp_count; i++) { - Generate_Swamp(cells, seeded_count, nodes, queue); + Generate_Swamp(cells, seeded_count); } } } @@ -5066,8 +5063,6 @@ bool MapGeneratorClass::Seed_Lake(const Cell & cell) } } - delete queue; - delete[] nodes; if (success) { SeededWaterAmount += seeded_count; @@ -5089,9 +5084,7 @@ bool MapGeneratorClass::Seed_Lake(const Cell & cell) /// The water cells the swamp may claim. /// The size of the water body, which decides how far the swamp may /// spread. -/// Scratch storage for the spread. -/// The queue that orders the spread. -void MapGeneratorClass::Generate_Swamp(DynamicVectorClass &cells, int last, CellNode *nodes, PriorityQueueClass *queue) +void MapGeneratorClass::Generate_Swamp(DynamicVectorClass &cells, int last) { Cell swamp_origin = CELL_NONE; int tries = 0; @@ -5121,39 +5114,40 @@ void MapGeneratorClass::Generate_Swamp(DynamicVectorClass &cells, int last int min_spread = std::min(last / 8, 50); int spread_count = Pick_Random_UInt(min_spread, max_spread); - queue->Clear(); + PriorityQueueClass queue(std::max((2 * spread_count) + 2, 100)); int marker_index = 1; - nodes[0].Element = swamp_origin; - nodes[0].Score = 0; - queue->Insert(nodes[0]); + CellNode seed_node; + seed_node.Element = swamp_origin; + seed_node.Score = 0; + queue.Insert(seed_node); - CellNode * node = queue->Extract_Min(); + std::optional node = queue.Extract_Min(); int spread_step = 0; - while (spread_step < spread_count && node != NULL) { + while (spread_step < spread_count && node) { CellClass * cptr = &Map[node->Element]; cptr->ITType = IsometricTileTypeClass::SwampTile; cptr->SubTile = 0; swamp_cells.Add(cptr->CellID); - CellNode * newnode = &nodes[marker_index]; for (FacingType dir = FACING_FIRST; dir < FACING_COUNT; dir = FacingType(dir + FACING_90)) { Cell newcell = Adjacent_Cell(node->Element, dir); if (My_In_Radar(newcell)) { MapRegionClass::CellData & data = MapRegionClass::Get_Cell_Data(newcell); CellClass * newcellptr = &Map[newcell]; if (newcellptr->ITType >= IsometricTileTypeClass::WaterSet && newcellptr->ITType < IsometricTileTypeClass::WaterSet + WATER_COUNT) { - newnode->Element = newcell; - newnode->Score = Get_Spread_Score(swamp_origin, newcell, spread_step); + CellNode newnode; + newnode.Element = newcell; + newnode.Score = Get_Spread_Score(swamp_origin, newcell, spread_step); data.SpreadID = WorkingRegionID; marker_index++; - queue->Insert(*newnode++); + queue.Insert(newnode); } } } spread_step++; - node = queue->Extract_Min(); + node = queue.Extract_Min(); } int i; @@ -5256,8 +5250,7 @@ bool MapGeneratorClass::Seed_Arctic_Lake(const Cell & cell) return(false); } - CellNode *nodes = new CellNode[std::max((2 * spread_limit) + 2, 100)]; - PriorityQueueClass *queue = new PriorityQueueClass(std::max((2 * spread_limit) + 2, 100)); + PriorityQueueClass queue(std::max((2 * spread_limit) + 2, 100)); Map.Reset_Iterator(); CellClass *cptr = Map.Iterate(); @@ -5300,20 +5293,20 @@ bool MapGeneratorClass::Seed_Arctic_Lake(const Cell & cell) int spread_count = (int)Sample_Truncated_Normal(mean, scale, 75.0, (double)max_spread); - queue->Clear(); + queue.Clear(); - int marker_index = 1; - nodes[0].Element = seed_cell; - nodes[0].Score = 0.0f; + CellNode seed_node; + seed_node.Element = seed_cell; + seed_node.Score = 0.0f; Set_Cell_Data_Spread(seed_cell, WorkingRegionID); - queue->Insert(nodes[0]); + queue.Insert(seed_node); - CellNode *node = queue->Extract_Min(); + std::optional node = queue.Extract_Min(); DynamicVectorClass *ice_cells = new DynamicVectorClass(); ice_cells->Set_Growth_Step(spread_count + 1); - while (seeded_count < spread_count && node != NULL) { + while (seeded_count < spread_count && node) { Set_Cell_Data_Region(node->Element, WorkingRegionID); CellClass *node_cellptr = &Map[node->Element]; @@ -5322,7 +5315,6 @@ bool MapGeneratorClass::Seed_Arctic_Lake(const Cell & cell) node_cellptr->IsIceGrowthAllowed = true; ice_cells->Add(node_cellptr->Fetch_CellID()); - CellNode *newnode = &nodes[marker_index]; for (FacingType dir = FACING_FIRST; dir < FACING_COUNT; dir = FacingType(dir + FACING_90)) { Cell newcell = Adjacent_Cell(node->Element, dir); @@ -5333,11 +5325,11 @@ bool MapGeneratorClass::Seed_Arctic_Lake(const Cell & cell) if (!data.RegionID && data.SpreadID != WorkingRegionID) { CellClass *newcellptr = &Map[newcell]; if (newcellptr->Is_Tile_Clear()) { - newnode->Element = newcell; - newnode->Score = Get_Spread_Score(seed_cell, newcell, seeded_count); + CellNode newnode; + newnode.Element = newcell; + newnode.Score = Get_Spread_Score(seed_cell, newcell, seeded_count); Set_Cell_Data_Spread(newcell, WorkingRegionID); - marker_index++; - queue->Insert(*newnode++); + queue.Insert(newnode); } } } @@ -5345,11 +5337,11 @@ bool MapGeneratorClass::Seed_Arctic_Lake(const Cell & cell) } seeded_count++; - node = queue->Extract_Min(); + node = queue.Extract_Min(); } - CellNode *remaining = queue->Extract_Min(); - while (remaining != NULL) { + std::optional remaining = queue.Extract_Min(); + while (remaining) { if (!success) { break; } @@ -5367,15 +5359,13 @@ bool MapGeneratorClass::Seed_Arctic_Lake(const Cell & cell) ice_cells->Add(cellptr->CellID); } - remaining = queue->Extract_Min(); + remaining = queue.Extract_Min(); seeded_count++; } Seed_Ice(*ice_cells, (IsometricTileType)(IsometricTileTypeClass::Ice1Set + ICE_CRACKED)); Seed_Ice(*ice_cells, (IsometricTileType)(IsometricTileTypeClass::Ice1Set + ICE_EDGE)); - delete queue; - delete[] nodes; delete ice_cells; if (success) { @@ -5402,8 +5392,7 @@ void MapGeneratorClass::Seed_Ice(DynamicVectorClass & cells, IsometricTile int seed_count = Pick_Random_UInt(0, 15); - CellNode * nodes = new CellNode[std::max(cells.Count() * 2, 64)]; - PriorityQueueClass * queue = new PriorityQueueClass(std::max(cells.Count() * 2, 64)); + PriorityQueueClass queue(std::max(cells.Count() * 2, 64)); for (int i = cells.Count() - 1; i >= 0; i--) { MapRegionClass::Get_Cell_Data(cells[i]).Iced = false; @@ -5413,47 +5402,44 @@ void MapGeneratorClass::Seed_Ice(DynamicVectorClass & cells, IsometricTile int spread_step = 0; int spread_limit = std::max(4, cells.Count() / 20); int spread_count = Pick_Random_UInt(3, spread_limit); - queue->Clear(); + queue.Clear(); int seed_index = Pick_Random_UInt(0, cells.Count() - 1); - int node_count = 1; Cell seed_cell = cells[seed_index]; - nodes[0].Element = seed_cell; - nodes[0].Score = 0.0f; + CellNode seed_node; + seed_node.Element = seed_cell; + seed_node.Score = 0.0f; MapRegionClass::Get_Cell_Data(seed_cell).Iced = true; - queue->Insert(nodes[0]); + queue.Insert(seed_node); - CellNode * node = queue->Extract_Min(); - while (spread_step < spread_count && node != NULL) { + std::optional node = queue.Extract_Min(); + while (spread_step < spread_count && node) { Map[node->Element].ITType = (IsometricTileType)last; - CellNode * newnode = &nodes[node_count]; for (FacingType dir = FACING_FIRST; dir < FACING_COUNT; dir = FacingType(dir + FACING_90)) { Cell newcell = Adjacent_Cell(node->Element, dir); if (My_In_Radar(newcell)) { CellClass * newcellptr = &Map[newcell]; MapRegionClass::CellData & data = MapRegionClass::Get_Cell_Data(newcell); if (newcellptr->Is_Tile_Ice() && !data.Iced) { - newnode->Element = newcell; - newnode->Score = Get_Ice_Score(seed_cell, newcell, rand_scale); + CellNode newnode; + newnode.Element = newcell; + newnode.Score = Get_Ice_Score(seed_cell, newcell, rand_scale); data.Iced = true; - node_count++; - queue->Insert(*newnode++); + queue.Insert(newnode); } } } spread_step++; - node = queue->Extract_Min(); + node = queue.Extract_Min(); } seed_count--; } - delete[] nodes; - delete queue; } @@ -6632,10 +6618,8 @@ DynamicVectorClass *MapGeneratorClass::Build_Region_Border_Cell_List(int i /// True if the region grew without colliding with another region. bool MapGeneratorClass::Grow_Water_Region(int region_id, float spread_scale, Rect const & bounds, Cell const & origin, bool claim_frontier) { - CellNode * nodes = new CellNode[std::max(2 * LocalHeight * LocalWidth, 100)]; - PriorityQueueClass * queue = new PriorityQueueClass(std::max(2 * LocalHeight * LocalWidth, 100)); + PriorityQueueClass queue(std::max(2 * LocalHeight * LocalWidth, 100)); - int marker_index = 0; bool result_flag = true; /* @@ -6663,16 +6647,15 @@ bool MapGeneratorClass::Grow_Water_Region(int region_id, float spread_scale, Rec int spread_count = 0; if (seed_count > 0) { - CellNode * newnode = nodes; do { Cell seed = (*seed_cells)[spread_count]; if (seed.X >= bounds.X && seed.X < bounds.X + bounds.Width && seed.Y >= bounds.Y && seed.Y < bounds.Y + bounds.Height) { - newnode->Element = seed; - newnode->Score = Get_Angle_Score(origin, seed, angle); - marker_index++; - queue->Insert(*newnode++); + CellNode newnode; + newnode.Element = seed; + newnode.Score = Get_Angle_Score(origin, seed, angle); + queue.Insert(newnode); } } while (++spread_count < seed_count); } @@ -6690,12 +6673,12 @@ bool MapGeneratorClass::Grow_Water_Region(int region_id, float spread_scale, Rec spread_count = (int)(1.0f / ((float)(1.0 / scale) * spread_scale) * 0.5f); spread_count += Pick_Random_UInt(0, spread_count / 2); - CellNode * node = queue->Extract_Min(); + std::optional node = queue.Extract_Min(); int spread_step = 0; if (spread_count > 0) { while (true) { - if (!result_flag || node == NULL) { + if (!result_flag || !node) { break; } @@ -6707,7 +6690,6 @@ bool MapGeneratorClass::Grow_Water_Region(int region_id, float spread_scale, Rec } } - CellNode * newnode = &nodes[marker_index]; for (FacingType dir = FACING_FIRST; dir < FACING_COUNT; dir = FacingType(dir + FACING_90)) { Cell newcell = Adjacent_Cell(node->Element, dir); if (My_In_Radar(newcell)) { @@ -6719,11 +6701,11 @@ bool MapGeneratorClass::Grow_Water_Region(int region_id, float spread_scale, Rec newcell.Y >= bounds.Y && newcell.Y < bounds.Y + bounds.Height && Map[newcell].Is_Tile_Clear()) { - newnode->Element = newcell; - newnode->Score = Get_Angle_Score(origin, newcell, angle); + CellNode newnode; + newnode.Element = newcell; + newnode.Score = Get_Angle_Score(origin, newcell, angle); Set_Cell_Data_Spread(newcell, region_id); - marker_index++; - queue->Insert(*newnode++); + queue.Insert(newnode); continue; } other_region = MapRegionClass::Get_Cell_Data(newcell).RegionID; @@ -6742,7 +6724,7 @@ bool MapGeneratorClass::Grow_Water_Region(int region_id, float spread_scale, Rec spread_step++; angle += Sample_Normal(0, M_PI_4); - node = queue->Extract_Min(); + node = queue.Extract_Min(); if (spread_step >= spread_count) { break; } @@ -6753,8 +6735,8 @@ bool MapGeneratorClass::Grow_Water_Region(int region_id, float spread_scale, Rec * Optionally drain the remaining frontier and assign it to the region. */ if (claim_frontier) { - CellNode * remaining = queue->Extract_Min(); - while (remaining != NULL) { + std::optional remaining = queue.Extract_Min(); + while (remaining) { if (!result_flag) { break; } @@ -6774,12 +6756,10 @@ bool MapGeneratorClass::Grow_Water_Region(int region_id, float spread_scale, Rec } } - remaining = queue->Extract_Min(); + remaining = queue.Extract_Min(); } } - delete[] nodes; - delete queue; return(result_flag); } @@ -6970,22 +6950,21 @@ bool MapGeneratorClass::Init_Start_Points(void) while (true) { Cell seed = Scen->Get_Waypoint_Cell((WAYPOINT)player_index); - CellNode * nodes = new CellNode[800]; - PriorityQueueClass * queue = new PriorityQueueClass(800); + PriorityQueueClass queue(800); Clear_Cell_Data_Spreads(); - queue->Clear(); + queue.Clear(); int patch_id = player_index + 1; - int node_count = 1; int spread_count = 0; - nodes[0].Element = seed; - nodes[0].Score = 0.0f; + CellNode seed_node; + seed_node.Element = seed; + seed_node.Score = 0.0f; MapRegionClass::Get_Cell_Data(seed).SpreadID = patch_id; - queue->Insert(nodes[0]); + queue.Insert(seed_node); - CellNode * node = queue->Extract_Min(); - if (node == NULL) { + std::optional node = queue.Extract_Min(); + if (!node) { break; } @@ -6996,25 +6975,24 @@ bool MapGeneratorClass::Init_Start_Points(void) MapRegionClass::Get_Cell_Data(node->Element).Inviolate = true; - CellNode * newnode = &nodes[node_count]; for (FacingType dir = FACING_FIRST; dir < FACING_COUNT; dir++) { Cell ncell = Adjacent_Cell(node->Element, dir); if (My_In_Radar(ncell) && MapRegionClass::Get_Cell_Data(ncell).SpreadID == 0) { if (Map[ncell].Is_Tile_Clear()) { - newnode->Element = ncell; + CellNode newnode; + newnode.Element = ncell; int dx = ncell.X - seed.X; int dy = ncell.Y - seed.Y; - newnode->Score = std::sqrt((double)(dx * dx + dy * dy)); + newnode.Score = std::sqrt((double)(dx * dx + dy * dy)); MapRegionClass::Get_Cell_Data(ncell).SpreadID = patch_id; - node_count++; - queue->Insert(*newnode++); + queue.Insert(newnode); } } } spread_count++; - node = queue->Extract_Min(); - if (node == NULL) { + node = queue.Extract_Min(); + if (!node) { break; } } @@ -7023,8 +7001,6 @@ bool MapGeneratorClass::Init_Start_Points(void) break; } - delete[] nodes; - delete queue; player_index = patch_id; if (patch_id >= SeedData.NumPlayers) { @@ -7157,9 +7133,21 @@ void MapGeneratorClass::Create_Tiberium_Patch(Cell const &cell, int count, int p "TIBTRE03" }; + /* + * A slot in the node pool below, ordered by the score that slot carries. The queue holds + * these rather than the nodes themselves so that the spread keeps reading its cell out + * of the pool, which is what the restart further down depends on. + */ + struct PoolSlot { + int Index; + float Score; + + bool operator<(PoolSlot const & other) const { return((double)Score < (double)other.Score); } + }; + int placed_count = 0; - CellNode *nodes = new CellNode[10 * count]; - PriorityQueueClass *queue = new PriorityQueueClass(10 * count); + std::vector nodes(10 * count); + PriorityQueueClass queue(10 * count); bool queue_was_reset_for_spread = false; Cell spread_origin = cell; @@ -7175,8 +7163,8 @@ void MapGeneratorClass::Create_Tiberium_Patch(Cell const &cell, int count, int p int wildlife_trigger = Pick_Random_UInt(0, count); HouseClass *neutral = House_From_HousesType(HouseTypeClass::From_Name("Neutral")); - queue->Clear(); - CellNode *node = NULL; + queue.Clear(); + int node_index = -1; int marker_index = 0; int visited = 1; @@ -7185,8 +7173,8 @@ void MapGeneratorClass::Create_Tiberium_Patch(Cell const &cell, int count, int p break; } - if (node == NULL) { - queue->Clear(); + if (node_index < 0) { + queue.Clear(); Map.Reset_Iterator(); CellClass *iter = Map.Iterate(); @@ -7200,27 +7188,28 @@ void MapGeneratorClass::Create_Tiberium_Patch(Cell const &cell, int count, int p nodes[0].Element = cell; nodes[0].Score = 0; Set_Cell_Data_Spread(cell, patch_id); - queue->Insert(nodes[0]); - node = queue->Extract_Min(); + queue.Insert(PoolSlot{0, nodes[0].Score}); + + std::optional taken = queue.Extract_Min(); + node_index = taken ? taken->Index : -1; queue_was_reset_for_spread = false; restart_count++; spread_origin = cell; } /* - * Every use below reads node->Element THROUGH the node pointer, and the loop - * re-reads it on every pass. This is load-bearing: the neighbor writes go - * to nodes[marker_index], and marker_index is reset to 0 in the block below - * while node still points at nodes[0], so the first neighbor stored OVERWRITES - * node->Element and the remaining directions are taken from the new cell. - * Caching the cell in a local would silently scan the true 8-neighborhood and - * generate different tiberium fields. + * Every use below reads the cell back out of its pool slot, and the loop re-reads + * it on every pass. This is load-bearing: the neighbor writes go to + * nodes[marker_index], and marker_index is reset to 0 in the block below while the + * slot in hand is still nodes[0], so the first neighbor stored OVERWRITES that cell + * and the remaining directions are taken from the new one. Holding the cell in a + * local would silently scan the true 8-neighborhood and generate different fields. */ - CellClass *cellptr = &Map[node->Element]; - if (MapRegionClass::CellData::Is_Not_Inviolate(node->Element)) { + CellClass *cellptr = &Map[nodes[node_index].Element]; + if (MapRegionClass::CellData::Is_Not_Inviolate(nodes[node_index].Element)) { if (!queue_was_reset_for_spread) { - spread_origin = node->Element; - queue->Clear(); + spread_origin = nodes[node_index].Element; + queue.Clear(); marker_index = 0; queue_was_reset_for_spread = true; } @@ -7242,7 +7231,7 @@ void MapGeneratorClass::Create_Tiberium_Patch(Cell const &cell, int count, int p } if (first_placed_cell == CELL_NONE) { - first_placed_cell = node->Element; + first_placed_cell = nodes[node_index].Element; } if (placed_count == wildlife_trigger && wildlife_count > 0 && spawn_wildlife) { @@ -7255,7 +7244,7 @@ void MapGeneratorClass::Create_Tiberium_Patch(Cell const &cell, int count, int p } if (foot != NULL) { - if (!foot->Unlimbo(node->Element.As_Coord(), DIR_N)) { + if (!foot->Unlimbo(nodes[node_index].Element.As_Coord(), DIR_N)) { delete foot; } } @@ -7269,7 +7258,7 @@ void MapGeneratorClass::Create_Tiberium_Patch(Cell const &cell, int count, int p CellNode *newnode = &nodes[marker_index]; for (int dir = FACING_FIRST; dir < FACING_COUNT; dir++) { - Cell newcell = Adjacent_Cell(node->Element, (FacingType)dir); + Cell newcell = Adjacent_Cell(nodes[node_index].Element, (FacingType)dir); if (Map.In_Local_Radar(newcell, true)) { CellClass *newcellptr = &Map[newcell]; if (newcellptr->Is_Tile_Clear()) { @@ -7279,13 +7268,15 @@ void MapGeneratorClass::Create_Tiberium_Patch(Cell const &cell, int count, int p newnode->Score = Get_Tiberium_Score(spread_origin, newcell); Set_Cell_Data_Spread(newcell, patch_id); marker_index++; - queue->Insert(*newnode++); + queue.Insert(PoolSlot{(int)(newnode - nodes.data()), newnode->Score}); + newnode++; } } } } - node = queue->Extract_Min(); + std::optional taken = queue.Extract_Min(); + node_index = taken ? taken->Index : -1; } if (place_tree) { @@ -7295,8 +7286,6 @@ void MapGeneratorClass::Create_Tiberium_Patch(Cell const &cell, int count, int p } } - delete[] nodes; - delete queue; } @@ -7786,21 +7775,20 @@ void MapGeneratorClass::Generate_Arctic_Vegetation(void) /// The chance, from 0 to 1, that a cell reached is given a tree. void MapGeneratorClass::Place_Forest(CellClass const * cellptr, int count, double density) { - CellNode * nodes = new CellNode[10 * count]; - PriorityQueueClass * queue = new PriorityQueueClass(10 * count); + PriorityQueueClass queue(10 * count); int i = 0; - queue->Clear(); + queue.Clear(); - int node_count = 1; Cell cell = cellptr->Fetch_CellID(); - nodes->Element = cell; - nodes->Score = 0; + CellNode seed_node; + seed_node.Element = cell; + seed_node.Score = 0; MapRegionClass::Get_Cell_Data(cellptr->Fetch_CellID()).Forested = true; - queue->Insert(*nodes); + queue.Insert(seed_node); - CellNode * node = queue->Extract_Min(); + std::optional node = queue.Extract_Min(); int min_tree = 1; int max_tree = 25; @@ -7812,7 +7800,7 @@ void MapGeneratorClass::Place_Forest(CellClass const * cellptr, int count, doubl min_tree = 21; } - while (i < count && node != NULL) { + while (i < count && node) { CellClass * cptr = &Map[node->Element]; if (cptr->Is_Tile_Clear() && cptr->Cell_Occupier() == NULL && cptr->Overlay == OVERLAY_NONE && cptr->Land_Type() != LAND_ROCK) { if (Random_Fraction() < density) { @@ -7823,27 +7811,24 @@ void MapGeneratorClass::Place_Forest(CellClass const * cellptr, int count, doubl } } - CellNode * newnode = &nodes[node_count]; for (FacingType dir = FACING_FIRST; dir < FACING_COUNT; dir++) { Cell newcell = Adjacent_Cell(node->Element, dir); if (My_In_Radar(newcell)) { MapRegionClass::CellData & data = MapRegionClass::Get_Cell_Data(newcell); if (!data.Forested && !data.Inviolate) { - newnode->Element = newcell; - newnode->Score = Get_Forest_Score(cellptr->Fetch_CellID(), newcell); + CellNode newnode; + newnode.Element = newcell; + newnode.Score = Get_Forest_Score(cellptr->Fetch_CellID(), newcell); data.Forested = true; - node_count++; - queue->Insert(*newnode++); + queue.Insert(newnode); } } } i++; - node = queue->Extract_Min(); + node = queue.Extract_Min(); } - delete[] nodes; - delete queue; } @@ -7880,48 +7865,44 @@ double MapGeneratorClass::Get_Forest_Score(const Cell & cell1, const Cell & cell /// May the patch spread over pavement as well as open ground? void MapGeneratorClass::Place_Tile_Patch(CellClass * cellptr, IsometricTileType ittype, int count, int origin_id, bool on_pavement) { - CellNode * nodes = new CellNode[10 * count]; - PriorityQueueClass * queue = new PriorityQueueClass(10 * count); + PriorityQueueClass queue(10 * count); int i = 0; - queue->Clear(); + queue.Clear(); - int node_count = 1; Cell cell = cellptr->Fetch_CellID(); - nodes->Element = cell; - nodes->Score = 0; + CellNode seed_node; + seed_node.Element = cell; + seed_node.Score = 0; MapRegionClass::Get_Cell_Data(cellptr->Fetch_CellID()).SpreadID = origin_id; - queue->Insert(*nodes); + queue.Insert(seed_node); - CellNode * node = queue->Extract_Min(); - while (i < count && node != NULL) { + std::optional node = queue.Extract_Min(); + while (i < count && node) { CellClass * cptr = &Map[node->Element]; cptr->ITType = ittype; - CellNode * newnode = &nodes[node_count]; for (FacingType dir = FACING_FIRST; dir < FACING_COUNT; dir++) { Cell newcell = Adjacent_Cell(node->Element, dir); if (My_In_Radar(newcell)) { CellClass * newcellptr = &Map[newcell]; if (newcellptr->Is_Tile_Clear() || on_pavement && (newcellptr->Is_Tile_Pavement() || newcellptr->Is_Tile_Misc_Pavement())) { if (MapRegionClass::Get_Cell_Data(newcell).SpreadID != origin_id && newcellptr->Ramp == RAMP_NONE && newcellptr->Overlay == OVERLAY_NONE && newcellptr->Cell_Occupier() == NULL) { - newnode->Element = newcell; - newnode->Score = Get_Tile_Patch_Score(cellptr->Fetch_CellID(), newcell); + CellNode newnode; + newnode.Element = newcell; + newnode.Score = Get_Tile_Patch_Score(cellptr->Fetch_CellID(), newcell); MapRegionClass::Get_Cell_Data(newcell).SpreadID = origin_id; - node_count++; - queue->Insert(*newnode++); + queue.Insert(newnode); } } } } i++; - node = queue->Extract_Min(); + node = queue.Extract_Min(); } - delete[] nodes; - delete queue; } @@ -7977,24 +7958,23 @@ void MapGeneratorClass::Generate_Mold(void) int spread_count = Pick_Random_UInt(30, 150); int heap_size = std::max(100, 6 * spread_count); - CellNode * nodes = new CellNode[heap_size]; - PriorityQueueClass * queue = new PriorityQueueClass(heap_size); - queue->Clear(); + PriorityQueueClass queue(heap_size); + queue.Clear(); int marker_index = 1; - nodes[0].Element = mold_origin; - nodes[0].Score = 0; - queue->Insert(nodes[0]); + CellNode seed_node; + seed_node.Element = mold_origin; + seed_node.Score = 0; + queue.Insert(seed_node); - CellNode * node = queue->Extract_Min(); + std::optional node = queue.Extract_Min(); int spread_step = 0; - while (spread_step < spread_count && node != NULL) { + while (spread_step < spread_count && node) { CellClass * cptr = &Map[node->Element]; cptr->ITType = IsometricTileTypeClass::BlueMoldTile; cptr->SubTile = 0; cells.Add(cptr->CellID); - CellNode * newnode = &nodes[marker_index]; for (FacingType dir = FACING_FIRST; dir < FACING_COUNT; dir = FacingType(dir + FACING_90)) { Cell newcell = Adjacent_Cell(node->Element, dir); if (My_In_Radar(newcell)) { @@ -8011,17 +7991,18 @@ void MapGeneratorClass::Generate_Mold(void) } if (can_spread && !data.Inviolate && marker_index < heap_size) { - newnode->Element = newcell; - newnode->Score = Get_Spread_Score(mold_origin, newcell, spread_step); + CellNode newnode; + newnode.Element = newcell; + newnode.Score = Get_Spread_Score(mold_origin, newcell, spread_step); data.SpreadID = WorkingRegionID; marker_index++; - queue->Insert(*newnode++); + queue.Insert(newnode); } } } spread_step++; - node = queue->Extract_Min(); + node = queue.Extract_Min(); } int i; @@ -8062,8 +8043,6 @@ void MapGeneratorClass::Generate_Mold(void) DebugString("Mold heap size: %d -- Marker Index: %d\n", heap_size, marker_index); - delete queue; - delete[] nodes; } @@ -8132,27 +8111,28 @@ void MapGeneratorClass::Generate_Crystals(const Cell & cell) heap_size = 100; } - CellNode *nodes = new CellNode[heap_size]; - PriorityQueueClass *queue = new PriorityQueueClass(heap_size); - queue->Clear(); + PriorityQueueClass queue(heap_size); + queue.Clear(); int marker_index = 0; if (on_cliff) { - nodes[0].Element = cliff_seed; - nodes[0].Score = 0; + CellNode seed_node; + seed_node.Element = cliff_seed; + seed_node.Score = 0; marker_index = 1; - queue->Insert(nodes[0]); + queue.Insert(seed_node); } - CellNode *seed = &nodes[marker_index++]; - seed->Element = crystal_origin; - seed->Score = 0; - queue->Insert(*seed); + CellNode seed; + seed.Element = crystal_origin; + seed.Score = 0; + marker_index++; + queue.Insert(seed); - CellNode *node = queue->Extract_Min(); + std::optional node = queue.Extract_Min(); int spread_step = 0; while (spread_step < spread_count) { - if (node == NULL) { + if (!node) { break; } CellClass *cptr = &Map[node->Element]; @@ -8160,23 +8140,23 @@ void MapGeneratorClass::Generate_Crystals(const Cell & cell) cptr->SubTile = 0; cells.Add(cptr->CellID); - CellNode *newnode = &nodes[marker_index]; for (FacingType dir = FACING_FIRST; dir < FACING_COUNT; dir = FacingType(dir + FACING_90)) { Cell newcell = Adjacent_Cell(node->Element, dir); if (My_In_Radar(newcell)) { MapRegionClass::CellData & data = MapRegionClass::Get_Cell_Data(newcell); if (Map[newcell].Is_Tile_Clear() && Map[newcell].Overlay == OVERLAY_NONE && marker_index < heap_size) { - newnode->Element = newcell; - newnode->Score = Get_Spread_Score(crystal_origin, newcell, spread_step); + CellNode newnode; + newnode.Element = newcell; + newnode.Score = Get_Spread_Score(crystal_origin, newcell, spread_step); data.SpreadID = WorkingRegionID; marker_index++; - queue->Insert(*newnode++); + queue.Insert(newnode); } } } spread_step++; - node = queue->Extract_Min(); + node = queue.Extract_Min(); } int i; @@ -8212,8 +8192,6 @@ void MapGeneratorClass::Generate_Crystals(const Cell & cell) DebugString("Mold heap size: %d -- Marker Index: %d\n", heap_size, marker_index); - delete queue; - delete[] nodes; } double ConditionRedChances[BIOME_COUNT] = {0.5, 0.2, 0.2, 0.6, 0.8}; @@ -8288,9 +8266,8 @@ void MapGeneratorClass::Generate_Urban_Areas(void) /// The collected urban area cells, or NULL if the area is too small or sparse. DynamicVectorClass * MapGeneratorClass::Create_Urban_Area(Cell const & cell, int size) { - CellNode * nodes = new CellNode[10 * size]; - PriorityQueueClass * queue = new PriorityQueueClass(10 * size); - queue->Clear(); + PriorityQueueClass queue(10 * size); + queue.Clear(); int processed = 0; @@ -8302,22 +8279,21 @@ DynamicVectorClass * MapGeneratorClass::Create_Urban_Area(Cell const & cel cptr = Map.Iterate(); } - nodes[0].Element = (Cell &)cell; - nodes[0].Score = 0; + CellNode seed_node; + seed_node.Element = (Cell &)cell; + seed_node.Score = 0; Set_Cell_Data_Spread(cell, 1); - int node_count = 1; - queue->Insert(nodes[0]); + queue.Insert(seed_node); - CellNode * node = queue->Extract_Min(); + std::optional node = queue.Extract_Min(); DynamicVectorClass * cells = new DynamicVectorClass; cells->Set_Growth_Step(size + 2); - while (size > processed && node != NULL) { + while (size > processed && node) { Map[node->Element].ITType = IsometricTileTypeClass::PaveTile; cells->Add(node->Element); - CellNode * newnode = &nodes[node_count]; for (FacingType dir = FACING_FIRST; dir < FACING_COUNT; dir++) { Cell newcell = Adjacent_Cell(node->Element, dir); if (My_In_Radar(newcell)) { @@ -8325,22 +8301,20 @@ DynamicVectorClass * MapGeneratorClass::Create_Urban_Area(Cell const & cel if (newcellptr->Is_Tile_Clear() && newcellptr->Overlay == OVERLAY_NONE) { MapRegionClass::CellData & data = MapRegionClass::Get_Cell_Data(newcell); if (data.SpreadID != 1 && !data.Inviolate) { - newnode->Element = newcell; - newnode->Score = Get_Urban_Score(cell, newcell); + CellNode newnode; + newnode.Element = newcell; + newnode.Score = Get_Urban_Score(cell, newcell); Set_Cell_Data_Spread(newcell, 1); - node_count++; - queue->Insert(*newnode++); + queue.Insert(newnode); } } } } processed++; - node = queue->Extract_Min(); + node = queue.Extract_Min(); } - delete[] nodes; - delete queue; Rect bounds = Get_Cell_Bounding_Rect(*cells); diff --git a/code/mapgen.h b/code/mapgen.h index 7b3f0da0e..28d88eb17 100644 --- a/code/mapgen.h +++ b/code/mapgen.h @@ -15,8 +15,6 @@ class CellClass; class MapPreviewClass; -struct CellNode; -template class PriorityQueueClass; #define RANDOM_MAP_FILE_NAME "RandMap.Sed" @@ -533,7 +531,7 @@ class MapGeneratorClass bool Grow_Water_Region(int region_id, float spread_scale, Rect const & bounds, Cell const & origin, bool claim_frontier); bool Place_Waterfall(int region_id, Cell const & cell1, Cell const & cell2, int direction, bool & placed, double & head_x, double & head_y); int Get_Target_Water_Amount(void); - void Generate_Swamp(DynamicVectorClass &cells, int last, CellNode *nodes, PriorityQueueClass *queue); + void Generate_Swamp(DynamicVectorClass & cells, int last); void Seed_Ice(DynamicVectorClass &cells, IsometricTileType last); void Smooth_Ice(void); diff --git a/code/mixfile.h b/code/mixfile.h index af937829f..221bcd6e3 100644 --- a/code/mixfile.h +++ b/code/mixfile.h @@ -50,6 +50,7 @@ class MixFileClass : public Node int operator > (const SubBlock & two) const {return(CRC > two.CRC);}; int operator == (const SubBlock & two) const {return(CRC == two.CRC);}; }; + static_assert(sizeof(SubBlock) == 12, "a MIX directory entry is 12 bytes on disk"); private: static MixFileClass * Finder(char const * filename); @@ -84,9 +85,9 @@ class MixFileClass : public Node std::int16_t count; std::int32_t size; }; + static_assert(sizeof(FileHeader) == 6, "the MIX header is 6 bytes on disk"); #pragma pack() - static_assert(sizeof(FileHeader) == 6, "Mixfile header layout changed"); static_assert(offsetof(FileHeader, size) == 2, "Mixfile header layout changed"); /* diff --git a/code/mouse.cpp b/code/mouse.cpp index 5cfacad41..69e65ba60 100644 --- a/code/mouse.cpp +++ b/code/mouse.cpp @@ -47,7 +47,6 @@ #include "builtype.h" #include "cell.h" #include "data.h" -#include "hashtable.h" #include "isotype.h" #include "mixfile.h" #include "overtype.h" @@ -60,6 +59,8 @@ #include "terrtype.h" #include "xmouse.h" +#include + #define MOUSE_HOTSPOT_MIN 0 #define MOUSE_HOTSPOT_CENTER 12345 @@ -413,25 +414,24 @@ bool MouseClass::Load(SaveStreamClass & stream) */ Free_Cells(); - delete CellSubzones; + delete [] CellSubzones; CellSubzones = NULL; - delete CellZones; + delete [] CellZones; CellZones = NULL; - delete ZoneAdjacency; - ZoneAdjacency = NULL; + ZoneAdjacency.clear(); for (i = 0; i < SUBZONE_COUNT; i++) { SubzoneTracking[i].Clear(); + SubzoneTrackingEntryCount[i] = 0; } for (i = 0; i < MZONE_COUNT; i++) { - delete Zones[i]; + delete [] Zones[i]; Zones[i] = NULL; } for (i = 0; i < SUBZONE_COUNT; i++) { - delete SubzoneConnectionHashTable[i]; - SubzoneConnectionHashTable[i] = NULL; + SubzoneConnectionStaging[i].clear(); } Array.Clear(); @@ -452,39 +452,30 @@ bool MouseClass::Load(SaveStreamClass & stream) */ Init_Cells(); - CellSubzones = NULL; - CellZones = NULL; - Set_Map_Dimensions(PlayRect, 1, 0, false); - if (CellSubzones) { - delete CellSubzones; - CellSubzones = NULL; - } - if (CellZones) { - delete CellZones; - CellZones = NULL; - } - CellSubzones = new CellSubzoneStruct[CellZoneCount]; CellZones = new CellZoneStruct[CellZoneCount]; - ZoneAdjacency = new ZONE_PAIR_HASH_SET(20, 256, SubzoneHash); for (i = 0; i < SUBZONE_COUNT; i++) { int v = (1 << (i + 1)); SubzoneTracking[i].Clear(); + SubzoneTrackingEntryCount[i] = 0; SubzoneTracking[i].Set_Growth_Step((4 * PlayRect.Width * PlayRect.Height) / (v * v)); - SubzoneConnectionHashTable[i] = new SUBZONE_CONNECTION_HASH_SET(20, 256, SubzoneHash); } + /* + * These blocks are read raw, so a file whose records are a different size would drag + * the rest of the stream out of step. + */ stream.Serialize_Bytes(CellZones, (int)(sizeof(*CellZones) * CellZoneCount)); if (stream.Was_Error()) { return(false); } for (i = 0; i < MZONE_COUNT; i++) { - Zones[i] = new unsigned short[ZoneCount]; - stream.Serialize_Bytes(Zones[i], (int)(sizeof(unsigned short) * ZoneCount)); + Zones[i] = new int[ZoneCount]; + stream.Serialize_Bytes(Zones[i], (int)(sizeof(*Zones[i]) * ZoneCount)); if (stream.Was_Error()) { return(false); } @@ -505,9 +496,13 @@ bool MouseClass::Load(SaveStreamClass & stream) return(false); } for (i = 0; i < count; i++) { - if (Load_Object(stream) == NULL) { + std::unique_ptr cell = Load_Object_As(stream); + if (cell == nullptr) { return(false); } + // The cell put itself into the map's array as it finished loading, and the map + // is what deletes it from here on. + cell.release(); } TerrainTypeClass::Init(Scen->Theater); @@ -561,7 +556,7 @@ bool MouseClass::Save(SaveStreamClass & stream) } for (i = 0; i < MZONE_COUNT; i++) { - stream.Serialize_Bytes(Zones[i], (int)(sizeof(unsigned short) * ZoneCount)); + stream.Serialize_Bytes(Zones[i], (int)(sizeof(*Zones[i]) * ZoneCount)); if (stream.Was_Error()) { return(false); } @@ -596,8 +591,11 @@ bool MouseClass::Save(SaveStreamClass & stream) } cptr = Iterate(); } + // The count was written before the cells, so a second pass that disagrees with it + // has already written a map no load can read back. if (count != 0) { - return(result); + stream.Fail(); + return(false); } result = true; diff --git a/code/nodes.h b/code/nodes.h index 8ad8e9015..7f0ac490e 100644 --- a/code/nodes.h +++ b/code/nodes.h @@ -31,17 +31,33 @@ struct CellNode { Element = cell; } + CellNode(Cell const & cell, float score) : Element(cell), Score(score) {} + bool operator==(const CellNode & other) const { return((double)Score == (double)other.Score); } bool operator!=(const CellNode & other) const { return((double)Score != (double)other.Score); } bool operator<(const CellNode & other) const { return((double)Score < (double)other.Score); } bool operator>(const CellNode & other) const { return((double)Score > (double)other.Score); } bool operator<=(const CellNode & other) const { return((double)Score <= (double)other.Score); } bool operator>=(const CellNode & other) const { return((double)Score >= (double)other.Score); } + + // Carries the node to or from a save game. + template + void Serialize(S & stream) + { + stream.Serialize(Element); + stream.Serialize(Score); + } }; struct AStarHierarchicalNode { + /* + * This is the node's own slot in the pool the search hands nodes out from, and it is + * what the nodes reached through this one record as their ParentIndex. + */ + int PoolIndex; + /* * This is the index within the node pool of the node that this one was reached from, * or -1 for the node the search started at. When the destination is reached, the diff --git a/code/options.cpp b/code/options.cpp index 942da8a17..b05eeae96 100644 --- a/code/options.cpp +++ b/code/options.cpp @@ -61,6 +61,7 @@ #include "options.h" #include "_command.h" +#include "_deploymentconfig.h" #include "_map.h" #include "_rules.h" #include "audio/audioengine.h" @@ -68,6 +69,7 @@ #include "ccrand.h" #include "command.h" #include "dbgprint.h" +#include "deploymentconfig.h" #include "dsurface.h" #include "globals.h" #include "init.h" @@ -358,7 +360,7 @@ static char const * Scale_Mode_Name(int mode) *=============================================================================================*/ void OptionsClass::Load_Settings(void) { - DebugString("--------- Loading SUN.INI settings ---------------\n"); + DebugString("--------- Loading %s settings ---------------\n", DeploymentConfig.SettingsFile.c_str()); /* ** Read in the Options values @@ -465,7 +467,7 @@ void OptionsClass::Load_Settings(void) *=============================================================================================*/ void OptionsClass::Save_Settings (void) { - CCFileClass file(CONFIG_FILE_NAME); + CCFileClass file(DeploymentConfig.SettingsFile.c_str()); DebugString("Saving game settings\n"); diff --git a/code/pcx.h b/code/pcx.h index 4b9b85415..7dfb3bb8e 100644 --- a/code/pcx.h +++ b/code/pcx.h @@ -44,6 +44,7 @@ struct RGB { unsigned char green; unsigned char blue; }; +static_assert(sizeof(RGB) == 3, "a PCX palette entry is 3 bytes on disk"); struct PCX_HEADER { @@ -66,10 +67,9 @@ struct PCX_HEADER short vert_screen_size; char filler[54]; }; +static_assert(sizeof(PCX_HEADER) == 128, "the PCX header is 128 bytes on disk"); #pragma pack(pop) -static_assert(sizeof(RGB) == 3, "PCX palette entry layout changed"); -static_assert(sizeof(PCX_HEADER) == 128, "PCX file header layout changed"); static_assert(offsetof(PCX_HEADER, x) == 4, "PCX file header layout changed"); static_assert(offsetof(PCX_HEADER, ega_palette) == 16, "PCX file header layout changed"); static_assert(offsetof(PCX_HEADER, byte_per_line) == 66, "PCX file header layout changed"); diff --git a/code/preview.h b/code/preview.h index 2b30c3f1c..b1bb1739b 100644 --- a/code/preview.h +++ b/code/preview.h @@ -50,6 +50,7 @@ class MapPreviewClass int Width; int Height; }; + static_assert(sizeof(Header) == 8, "the preview header is 8 bytes on disk"); private: diff --git a/code/priority.h b/code/priority.h index 0873ee6fc..8308b7e0e 100644 --- a/code/priority.h +++ b/code/priority.h @@ -9,283 +9,182 @@ #pragma once -#include "dbgprint.h" - -#include -#include -#include - - -#define PARENT(index) (index >> 1) -#define LEFT_CHILD(index) ((index << 1)) -#define RIGHT_CHILD(index) ((index << 1) + 1) -#define SWAP(left, right) \ -{ \ - T * temp = Heap[left]; \ - Heap[left] = Heap[right]; \ - Heap[right] = temp; \ -} +#include +#include +#include +#include +/* + * A binary min heap ordered by the Score each element carries. + * + * The sift is spelled out here because the order equal scores come out in is part of the + * simulation, and the standard heap algorithms may settle such a tie either way. + * tests/priorityqueue holds that order. + */ template class PriorityQueueClass { - friend class AStarClass; public: - PriorityQueueClass(int size); - ~PriorityQueueClass(void); + // The size is how much room to take up front, not a limit on what the queue holds. + explicit PriorityQueueClass(int size = 0); - void Clear(void); + void Clear(void) { Heap.clear(); } + void Reserve(int size) { if (size > 0) { Heap.reserve((std::size_t)size); } } + int Count(void) const { return((int)Heap.size()); } - bool Insert(T & node); - T * Extract_Min(void); - T * Replace_Root(T & node); + void Insert(T node); + std::optional Extract_Min(void); + T Replace_Root(T node); bool Remove_Matching(T const & item); - void Heapify(int index); - - int Count(void) const { return(ActiveCount); } /* - * Carries the queue to or from a save game. The heap holds pointers into the - * caller's node array, so each slot travels as its index into that array and the - * array has to be handed back in on the way in. + * Carries the queue to or from a save game as its length followed by its elements, + * so the element type has to describe its own members to the stream. The elements + * travel in heap order rather than score order, because draining the queue to sort + * them would build a different heap and reorder the ties within it. */ template - void Serialize(S & stream, T * nodes); + void Serialize(S & stream) { stream.Serialize(Heap); } private: - /* - * This is the number of nodes currently in the queue, which is also the index of the - * last of them. - */ - int ActiveCount; + void Heapify(std::size_t index); - /* - * This is the number of slots the queue was created with. An insert that would run - * past the last slot is refused, since the queue never grows. - */ - int Size; + static std::size_t Parent(std::size_t index) { return((index - 1) / 2); } + static std::size_t Left_Child(std::size_t index) { return((2 * index) + 1); } + static std::size_t Right_Child(std::size_t index) { return((2 * index) + 2); } /* - * This points to the array of node pointers that makes up the heap. Slot zero is not - * used -- the lowest scoring node sits at slot one, and the children of any node lie - * at twice its index. + * The lowest scoring element sits at slot zero, and the children of the element at + * any slot lie at twice its index plus one and plus two. */ - T ** Heap; - - /// Unused - uintptr_t MaxNodePointer; - uintptr_t MinNodePointer; + std::vector Heap; }; template PriorityQueueClass::PriorityQueueClass(int size) { - MaxNodePointer = 0; - MinNodePointer = UINTPTR_MAX; - ActiveCount = 0; - Size = size; - Heap = new T * [size + 1](); - for (int index = 0; index <= Size; index++) { - Heap[index] = NULL; - } -} - - -template -PriorityQueueClass::~PriorityQueueClass(void) -{ - delete[] Heap; -} - - -template -void PriorityQueueClass::Clear(void) -{ - for (int index = 0; index <= ActiveCount; index++) { - Heap[index] = NULL; - } - ActiveCount = 0; + Reserve(size); } template -inline bool PriorityQueueClass::Insert(T & node) +inline void PriorityQueueClass::Insert(T node) { - unsigned index = ActiveCount + 1; - unsigned parent_index = PARENT(index); float score = node.Score; + std::size_t index = Heap.size(); - if (index >= (unsigned)Size) { - return(false); - } + Heap.emplace_back(); - while (index > 1) { - if (Heap[parent_index]->Score <= score) { + while (index > 0) { + std::size_t parent_index = Parent(index); + if (Heap[parent_index].Score <= score) { break; } Heap[index] = Heap[parent_index]; index = parent_index; - parent_index = PARENT(index); } - Heap[index] = &node; - ActiveCount++; - - if ((uintptr_t)&node > MaxNodePointer) { - MaxNodePointer = (uintptr_t)&node; - } - - if ((uintptr_t)&node < MinNodePointer) { - MinNodePointer = (uintptr_t)&node; - } - - return(true); + Heap[index] = node; } -template -inline T * PriorityQueueClass ::Extract_Min(void) +template +inline std::optional PriorityQueueClass::Extract_Min(void) { - if (ActiveCount == 0) { - return(NULL); + if (Heap.empty()) { + return(std::nullopt); } - T * min = Heap[1]; - Heap[1] = Heap[ActiveCount]; - Heap[ActiveCount] = NULL; - ActiveCount--; + T min = Heap.front(); - Heapify(1); + Heap.front() = Heap.back(); + Heap.pop_back(); + + Heapify(0); return(min); } -template -inline T * PriorityQueueClass::Replace_Root(T & node) +template +inline T PriorityQueueClass::Replace_Root(T node) { - if (ActiveCount == 0) { - return(&node); + if (Heap.empty()) { + return(node); } - T * old_root = Heap[1]; - - if (node < *old_root) { - return(&node); + if (node < Heap.front()) { + return(node); } - Heap[1] = &node; + T old_root = Heap.front(); + Heap.front() = node; - Heapify(1); + Heapify(0); return(old_root); } -template -inline bool PriorityQueueClass ::Remove_Matching(T const & item) +template +inline bool PriorityQueueClass::Remove_Matching(T const & item) { - for (int index = 1; index <= ActiveCount; index++) { - if (Heap[index]->Element == item.Element) { - if (index == ActiveCount) { - ActiveCount--; - } else { - T * last = Heap[ActiveCount]; - float last_score = last->Score; - int parent = PARENT(index); - ActiveCount--; - if (index != 1 && Heap[parent]->Score >= last_score) { - while ((unsigned)index > 1) { - if (Heap[parent]->Score <= last_score) break; - Heap[index] = Heap[parent]; - index = parent; - parent = PARENT(index); - } - Heap[index] = last; - } else { - Heap[index] = last; - Heapify(index); - } - } + for (std::size_t index = 0; index < Heap.size(); index++) { + if (!(Heap[index].Element == item.Element)) { + continue; + } - DebugString("Exiting Remove_Matching\n"); + if (index + 1 == Heap.size()) { + Heap.pop_back(); return(true); } - } - - DebugString("Exiting Remove_Matching\n"); - return(false); -} - - -template -inline void PriorityQueueClass::Heapify(int index) -{ - int smallest = index; - - int left = LEFT_CHILD(index); - int right = RIGHT_CHILD(index); - - smallest = left <= Count() && *Heap[left] < *Heap[index] ? left : index; - smallest = right <= Count() && *Heap[right] < *Heap[smallest] ? right : smallest; - - while (smallest != index) { - - SWAP(index, smallest) - index = smallest; - - left = LEFT_CHILD(index); - right = RIGHT_CHILD(index); + T last = Heap.back(); + float last_score = last.Score; + Heap.pop_back(); + + // A parent scoring exactly what the replacement scores leaves it where it stands, + // never sifted back down. That is what the extraction order was built on. + if (index != 0 && Heap[Parent(index)].Score >= last_score) { + std::size_t hole = index; + while (hole > 0) { + std::size_t parent = Parent(hole); + if (Heap[parent].Score <= last_score) { + break; + } + Heap[hole] = Heap[parent]; + hole = parent; + } + Heap[hole] = last; + } else { + Heap[index] = last; + Heapify(index); + } - smallest = left <= Count() && *Heap[left] < *Heap[index] ? left : index; - smallest = right <= Count() && *Heap[right] < *Heap[smallest] ? right : smallest; + return(true); } + + return(false); } template -template -void PriorityQueueClass::Serialize(S & stream, T * nodes) +inline void PriorityQueueClass::Heapify(std::size_t index) { - int count = ActiveCount; - int size = Size; - - stream.Serialize(count); - stream.Serialize(size); - - /* - * The heap was sized when the queue was built and never grows, so a save describing - * a different one, or more nodes than fit, cannot be read into this queue. - */ - if (stream.Is_Loading()) { - if (size != Size || count < 0 || count > Size) { - stream.Fail(); - return; - } - ActiveCount = count; - } - - for (int slot = 0; slot < Size; slot++) { - int index = stream.Is_Saving() ? (int)(Heap[slot] - nodes) : 0; - stream.Serialize(index); + for (;;) { + std::size_t left = Left_Child(index); + std::size_t right = Right_Child(index); - if (stream.Is_Loading()) { - Heap[slot] = &nodes[index]; + std::size_t smallest = left < Heap.size() && Heap[left] < Heap[index] ? left : index; + smallest = right < Heap.size() && Heap[right] < Heap[smallest] ? right : smallest; - if ((uintptr_t)Heap[slot] > MaxNodePointer) { - MaxNodePointer = (uintptr_t)Heap[slot]; - } - if ((uintptr_t)Heap[slot] < MinNodePointer) { - MinNodePointer = (uintptr_t)Heap[slot]; - } + if (smallest == index) { + break; } + + std::swap(Heap[index], Heap[smallest]); + index = smallest; } } - -#undef PARENT -#undef LEFT_CHILD -#undef RIGHT_CHILD -#undef SWAP diff --git a/code/radar.cpp b/code/radar.cpp index 0077423de..03858b0e1 100644 --- a/code/radar.cpp +++ b/code/radar.cpp @@ -108,15 +108,6 @@ #include -/// -/// Should this tracked object go at the head of its hash bucket? -/// The radar draws only the first object it finds on any given pixel, so the local player's -/// own units are placed at the front of the bucket and win the blip. -/// -/// bool; Does the tracked object belong to the local player? -inline bool RadarTrackingStruct::Use_Head(void) const { return(Object->House == PlayerPtr); } - - RadarClass::RTacticalClass RadarClass::RadarButton; void const * RadarClass::RadarAnim = NULL; @@ -155,7 +146,6 @@ RadarClass::RadarClass(void) : RadarCellWidth(0), RadarCellHeight(0), CellRedrawRect(0,0,0,0), - RadarTrackingTable(0), PixelFlags(0), ZoomFactor(0), RadarScale(1), @@ -178,7 +168,7 @@ RadarClass::RadarClass(void) : /// -/// Destroys the radar map, releasing its surfaces and object tracking table. +/// Destroys the radar map, releasing its surfaces and dropping its object tracking. /// RadarClass::~RadarClass(void) { @@ -1341,7 +1331,7 @@ void RadarClass::Plot_Radar_Background(void) /// -/// Adds an object to the radar tracking table. +/// Adds an object to the radar tracking. /// This routine registers the object at a radar pixel so that it will be drawn as a blip on /// the next radar render. A unit that lands outside the radar surface is pulled back to the /// nearest edge pixel; a building in that position is simply not tracked. @@ -1369,12 +1359,9 @@ void RadarClass::Radar_Track(TechnoClass * techno, Point2D point) techno->RadarPos = point; } - RADAR_HASH_TABLE::ObjectType track; - track.Key.Position = point; - track.Key.Object = techno; - track.Value = techno; - - if (RadarTrackingTable->Add_Object(track, true)) { + // The radar draws only the head of a pixel's list, so the local player's own objects go + // there and win the blip. + if (RadarTracking.Track(point, techno, techno->House == PlayerPtr)) { Radar_Pixel(point); IsToRedraw = true; } @@ -1382,7 +1369,7 @@ void RadarClass::Radar_Track(TechnoClass * techno, Point2D point) /// -/// Removes an object from the radar tracking table. +/// Removes an object from the radar tracking. /// This routine is called when a tracked object moves off a radar pixel or leaves the game. /// The vacated pixel is flagged for redraw so that whatever lies beneath it shows through /// again. @@ -1390,10 +1377,7 @@ void RadarClass::Radar_Track(TechnoClass * techno, Point2D point) /// The radar pixel the object was being tracked at. void RadarClass::Radar_Untrack(TechnoClass * techno, Point2D point) { - RadarTrackingStruct track; - track.Object = techno; - track.Position = point; - if (RadarTrackingTable->Remove_Object(track, techno)) { + if (RadarTracking.Untrack(point, techno)) { Radar_Pixel(point); IsToRedraw = true; } @@ -1428,38 +1412,75 @@ Point2D RadarClass::Coord_To_Radar_Pixel(Coord const & coord, bool clip) } -int RadarTrackingStruct::Hash_Old(RadarTrackingStruct const & s) +bool RadarTrackingClass::Track(Point2D const & pixel, TechnoClass * object, bool head) +{ + std::vector & objects = Blips[std::pair(pixel.X, pixel.Y)]; + + if (std::find(objects.begin(), objects.end(), object) != objects.end()) { + return(false); + } + + if (head) { + objects.insert(objects.begin(), object); + } else { + objects.push_back(object); + } + return(true); +} + + +bool RadarTrackingClass::Untrack(Point2D const & pixel, TechnoClass * object) { - return((int)((uintptr_t)s.Object + 251 * s.Position.X)); + auto found = Blips.find(std::pair(pixel.X, pixel.Y)); + if (found == Blips.end()) { + return(false); + } + + std::vector & objects = found->second; + auto entry = std::find(objects.begin(), objects.end(), object); + if (entry == objects.end()) { + return(false); + } + + objects.erase(entry); + if (objects.empty()) { + Blips.erase(found); + } + return(true); } -int RadarTrackingStruct::Hash2(RadarTrackingStruct const & s) + +TechnoClass * RadarTrackingClass::First(Point2D const & pixel) const { - return(s.Position.X + 251 * s.Position.Y); + auto found = Blips.find(std::pair(pixel.X, pixel.Y)); + return(found == Blips.end() ? NULL : found->second.front()); +} + + +TechnoClass * RadarTrackingClass::Last(Point2D const & pixel) const +{ + auto found = Blips.find(std::pair(pixel.X, pixel.Y)); + return(found == Blips.end() ? NULL : found->second.back()); } /// -/// Creates the radar's object tracking table. +/// Empties the radar's object tracking. /// void RadarClass::Init_Radar(void) { - RadarTrackingTable = new RADAR_HASH_TABLE(10, 256, RadarTrackingStruct::Hash2); + RadarTracking.Clear(); } /// /// Resets the radar map back to a blank slate. -/// This routine throws the object tracking table away and rebuilds the radar image from the +/// This routine throws the object tracking away and rebuilds the radar image from the /// current local map bounds. Every object is marked as untracked so that it registers itself /// again on its next logic pass. /// void RadarClass::Reset_Radar(void) { - if (RadarTrackingTable != NULL) { - delete RadarTrackingTable; - } - Init_Radar(); Map.Set_Local_Dimensions(Map.LocalRect); Compute_Radar_Image(); @@ -1487,10 +1508,7 @@ void RadarClass::Clear_Radar(void) delete BackgroundColors; BackgroundColors = NULL; } - if (RadarTrackingTable != NULL) { - delete RadarTrackingTable; - RadarTrackingTable = NULL; - } + RadarTracking.Clear(); if (PixelFlags != NULL) { delete [] PixelFlags; PixelFlags = NULL; @@ -1500,7 +1518,7 @@ void RadarClass::Clear_Radar(void) /// /// Handles the radar repairs needed after a save game is loaded. -/// The radar's surfaces and object tracking table are never saved, so this routine releases +/// The radar's surfaces and object tracking are never saved, so this routine releases /// the ones the previous scenario left behind and builds fresh ones from the loaded map. /// Every object is marked as untracked so that it registers itself again as the game resumes. /// @@ -1555,22 +1573,7 @@ void RadarClass::Plot_Radar_Pixel(Point2D const & point) coord.Z = Map.Get_Height_GL(coord); bool shadow = (MainWindow && Map.Is_Shrouded(coord)); - RadarTrackingStruct track; - track.Position = point; - track.Object = 0; - - TechnoClass *tech = NULL; - - RADAR_HASH_TABLE::BucketType &bucket = RadarTrackingTable->Buckets[track.Hash()]; - int count = bucket.Count(); - - for (int index = 0; index < count; index++) { - TechnoClass *candidate = bucket[index].Key.Object; - if (bucket[index].Key == track) { - tech = candidate; - break; - } - } + TechnoClass *tech = RadarTracking.First(point); if (tech != NULL) { HouseClass *house = tech->House; @@ -1616,48 +1619,36 @@ void RadarClass::Render_Tracked_Objects(void) { memset(PixelFlags, 0, RadarSurface->Get_Width() * RadarSurface->Get_Height() / 8 + 1); - for (int b = 0; b < 256; b++) { - - RADAR_HASH_TABLE::BucketType &bucket = RadarTrackingTable->Buckets[b]; - - int count = bucket.Count(); - - RADAR_HASH_TABLE::ObjectType *ptr = &bucket[0]; - - for (int t = 0; t < count; t++) { - RadarTrackingStruct *track = &ptr[t].Key; - TechnoClass *tech = track->Object; - - int id = track->Position.X + track->Position.Y * RadarSurface->Get_Width(); - int i = id >> 3; - int bit = 1 << (id & 7); - - if ((PixelFlags[i] & bit) == 0) { - PixelFlags[i] |= bit; + RadarTracking.For_Each_Pixel([this](Point2D const & pixel, TechnoClass * tech) { + int id = pixel.X + pixel.Y * RadarSurface->Get_Width(); + int i = id >> 3; + int bit = 1 << (id & 7); - ColorScheme *scheme = ColorSchemes[tech->House->Scheme]; + if ((PixelFlags[i] & bit) == 0) { + PixelFlags[i] |= bit; - if (tech->RTTI == RTTI_INFANTRY) { - InfantryClass *inf = (InfantryClass *)tech; - if (inf->Class->IsDisguised) { - scheme = ColorSchemes[PlayerPtr->Scheme]; - } - } + ColorScheme *scheme = ColorSchemes[tech->House->Scheme]; - int color = scheme->Bright; - ConvertClass *drawer = scheme->Converter; - if (drawer->Bytes_Per_Pixel() == 1) { - unsigned char *translator = (unsigned char *)drawer->Get_Translate_Table(); - color = translator[color]; - } else { - unsigned short *translator = (unsigned short *)drawer->Get_Translate_Table(); - color = translator[color]; + if (tech->RTTI == RTTI_INFANTRY) { + InfantryClass *inf = (InfantryClass *)tech; + if (inf->Class->IsDisguised) { + scheme = ColorSchemes[PlayerPtr->Scheme]; } + } - RadarSurface->Put_Pixel(track->Position, color); + int color = scheme->Bright; + ConvertClass *drawer = scheme->Converter; + if (drawer->Bytes_Per_Pixel() == 1) { + unsigned char *translator = (unsigned char *)drawer->Get_Translate_Table(); + color = translator[color]; + } else { + unsigned short *translator = (unsigned short *)drawer->Get_Translate_Table(); + color = translator[color]; } + + RadarSurface->Put_Pixel(pixel, color); } - } + }); } @@ -1830,15 +1821,9 @@ void RadarClass::Resolve_Radar_Point(Point2D const & point, Cell & cell, ObjectC { Point2D pt = point - RadarRect.TopLeft; - RadarTrackingStruct track; - track.Object = NULL; - track.Position = pt; - - /* - * The lookup key carries no object, so Get matches on position alone (via the - * non-const, position-only RadarTrackingStruct::operator==). - */ - RadarTrackingTable->Get(track, (TechnoClass *&)object); + // A click takes the object at the tail of the pixel's list, which is the last one to have + // been tracked there that did not go to the head. + object = RadarTracking.Last(pt); if (object != NULL) { cell = object->Destination_Coord(); @@ -1851,7 +1836,7 @@ void RadarClass::Resolve_Radar_Point(Point2D const & point, Cell & cell, ObjectC /// /// Lists the members the radar map holds. /// Only the pending update lists and the state of the radar display itself travel. The radar -/// surfaces, the object tracking table and the picture geometry are all rebuilt from the +/// surfaces, the object tracking and the picture geometry are all rebuilt from the /// loaded map by Post_Load_Radar_Fixup. /// /// The stream carrying the members. @@ -1880,7 +1865,7 @@ void RadarClass::Serialize(SaveStreamClass & stream) // RadarCellWidth -- measured again while the radar background is resampled. // RadarCellHeight // CellRedrawRect - // RadarTrackingTable -- rebuilt by Post_Load_Radar_Fixup, which also marks every object + // RadarTracking -- rebuilt by Post_Load_Radar_Fixup, which also marks every object // untracked so that it registers itself again. stream.Serialize(PixelStack); diff --git a/code/radar.h b/code/radar.h index 0339fb0b0..7c0db29f3 100644 --- a/code/radar.h +++ b/code/radar.h @@ -34,10 +34,13 @@ #include "coord.h" #include "display.h" -#include "hashtable.h" #include "stimer.h" #include "timer.h" +#include +#include +#include + #include "bsize.hh" class DSurface; @@ -46,44 +49,39 @@ template class DynamicVectorClass; typedef DynamicVectorClass FOUNDATION_LIST; -struct RadarTrackingStruct { - - RadarTrackingStruct(TechnoClass * object = NULL, int x = 0, int y = 0) : Object(object), Position(x, y) {} - RadarTrackingStruct(RadarTrackingStruct const & that) : Object(that.Object), Position(that.Position) {} - - /* - * This is the object that appears as a blip at the tracked position. A building covers - * several radar pixels, so it is tracked once for every pixel of its radar foundation. - */ - TechnoClass * Object; - - /* - * This is the radar pixel that the object occupies. The tracking table is hashed on this - * alone, so the radar can find whichever object sits on a given pixel. - */ - Point2D Position; - - bool operator==(const RadarTrackingStruct & that) const { return(Object == that.Object && Position == that.Position); } - bool operator!=(const RadarTrackingStruct & that) const { return(Object != that.Object || Position != that.Position); } - - /* - * The non-const overloads compare Position only. A lookup key is built with - * Object == NULL and relies on these to match on position alone (see Get), - * while Add and Remove use the const (Object + Position) overloads above. - */ - bool operator==(RadarTrackingStruct & that) { return(Position == that.Position); } - bool operator!=(RadarTrackingStruct & that) { return(Position != that.Position); } - - int Hash(void) const { return((Position.X - 5 * Position.Y) & 0xFF); } - - bool Use_Head(void) const; +/* + * The objects that show up as blips on the radar, listed by the radar pixel each one occupies. + * A building covers several pixels and is tracked once for each of them, and several objects + * can stand on one pixel, so a pixel holds a list rather than a single object. The radar draws + * only the object at the head of that list, and a click on the radar takes the one at its tail. + */ +class RadarTrackingClass +{ + public: + // Places the object at the head of the pixel's list when head is set. One already + // tracked at that pixel keeps its place. + bool Track(Point2D const & pixel, TechnoClass * object, bool head); + bool Untrack(Point2D const & pixel, TechnoClass * object); + + // The object the radar draws at a pixel, and the one a click there resolves to. + TechnoClass * First(Point2D const & pixel) const; + TechnoClass * Last(Point2D const & pixel) const; + + void Clear(void) { Blips.clear(); } + + // Visits each occupied pixel once, with the object the radar draws on it. + template + void For_Each_Pixel(T visit) const + { + for (auto const & [pixel, objects] : Blips) { + visit(Point2D(pixel.first, pixel.second), objects.front()); + } + } - static int Hash_Old(RadarTrackingStruct const & s); - static int Hash2(RadarTrackingStruct const & s); + private: + std::map, std::vector> Blips; }; -typedef HashTableClass RADAR_HASH_TABLE; - class RadarClass: public DisplayClass { typedef DisplayClass BASECLASS; @@ -255,12 +253,9 @@ class RadarClass: public DisplayClass int RadarCellHeight; Rect CellRedrawRect; - /* - * This is the table of objects that show up as blips on the radar, keyed by the radar - * pixel each one occupies. It lets the radar draw its blips without walking every - * object in the game, and lets a click on the radar find the object underneath. - */ - RADAR_HASH_TABLE * RadarTrackingTable; + // Kept so that drawing the blips and resolving a radar click never walk every object + // in the game. + RadarTrackingClass RadarTracking; /* ** This is the list of radar pixels that need to be updated. Only a partial diff --git a/code/reinf.cpp b/code/reinf.cpp index 4cfb87095..d27e1be6d 100644 --- a/code/reinf.cpp +++ b/code/reinf.cpp @@ -47,10 +47,10 @@ #include "airctype.h" #include "building.h" #include "cell.h" +#include "classids.h" #include "foot.h" #include "globals.h" #include "house.h" -#include "classids.h" #include "incdec.h" #include "inline.h" #include "mouse.h" diff --git a/code/revent.cpp b/code/revent.cpp index dedf27157..ff4c07403 100644 --- a/code/revent.cpp +++ b/code/revent.cpp @@ -391,17 +391,20 @@ bool RadarEventClass::Save(SaveStreamClass & stream) /// bool; Were the events read successfully? bool RadarEventClass::Load(SaveStreamClass & stream) { + // The destructor takes the event off the list, so the list drains as they are deleted. for (int i = RadarEvents.Count() - 1; i >= 0; i--) { delete RadarEvents[i]; - RadarEvents.Delete_Index(i); } stream.Set_Context("RadarEventClass"); int count = 0; stream.Serialize(count); + if (!stream.Fits(count, 1)) { + return(false); + } - for (int index = 0; index < count; index++) { + for (int index = 0; index < count && !stream.Was_Error(); index++) { RadarEventClass * event = new RadarEventClass(RADAREVENT_NONE, Cell(0, 0)); event->Serialize(stream); } diff --git a/code/revent.h b/code/revent.h index 7f1aee6e5..e27232e87 100644 --- a/code/revent.h +++ b/code/revent.h @@ -18,7 +18,6 @@ #include "revent.hh" -class SaveStreamClass; class SaveStreamClass; template class DynamicVectorClass; diff --git a/code/rgb.h b/code/rgb.h index b17332ba7..d06bdd2af 100644 --- a/code/rgb.h +++ b/code/rgb.h @@ -51,10 +51,9 @@ struct RGBStruct unsigned char Green; unsigned char Blue; }; +static_assert(sizeof(RGBStruct) == 3, "a palette entry is 3 bytes on disk"); #pragma pack() -static_assert(sizeof(RGBStruct) == 3, "Palette entry layout changed"); - /* ** Each color entry is represented by this class. It holds the values for the color diff --git a/code/rules.cpp b/code/rules.cpp index 30ae73fea..f3cf6a7cb 100644 --- a/code/rules.cpp +++ b/code/rules.cpp @@ -51,6 +51,7 @@ #include "rules.h" #include "_bench.h" +#include "_deploymentconfig.h" #include "_palette.h" #include "_rules.h" #include "_warhead.h" @@ -66,6 +67,7 @@ #include "conquer.h" #include "convert.h" #include "dbgprint.h" +#include "deploymentconfig.h" #include "findmake.h" #include "globals.h" #include "house.h" @@ -691,7 +693,7 @@ void RulesClass::Initialize(CCINIClass const & ini) Load_Art_INI(); if (Addon_Enabled(ADDON_FIRESTORM) == true) { - CCFileClass artfsfile("ARTFS.INI"); + CCFileClass artfsfile(DeploymentConfig.ArtExpansionFile.c_str()); if (artfsfile.Is_Available() == true) { ArtINI.Load(artfsfile, false); } @@ -700,7 +702,7 @@ void RulesClass::Initialize(CCINIClass const & ini) Heap_Maximums(ini); Addition(ini); - CCFileClass langfile("LANGRULE.INI"); + CCFileClass langfile(DeploymentConfig.LanguageRulesFile.c_str()); if (langfile.Is_Available() == true) { CCINIClass langini; if (langini.Load(langfile, true) > 1) { @@ -714,7 +716,7 @@ void RulesClass::Initialize(CCINIClass const & ini) Addition(FSRuleINI); } - CCFileClass langfsfile("LANGFS.INI"); + CCFileClass langfsfile(DeploymentConfig.LanguageRulesExpansionFile.c_str()); if (langfsfile.Is_Available() == true) { CCINIClass langfsini; langfsini.Load(langfsfile, false); @@ -2994,7 +2996,7 @@ int RulesClass::Get_Art_Unique_ID(void) { int id = ArtINI.Get_Unique_ID(); if (Addon_Enabled(ADDON_FIRESTORM) == true) { - CCFileClass artfs("ARTFS.INI"); + CCFileClass artfs(DeploymentConfig.ArtExpansionFile.c_str()); if (artfs.Is_Available() == true) { CCINIClass artfsini; artfsini.Load(artfs, false); @@ -3031,6 +3033,6 @@ int RulesClass::Get_AI_Unique_ID(void) void RulesClass::Load_Art_INI(void) { ArtINI.Clear(); - CCFileClass art("ART.INI"); + CCFileClass art(DeploymentConfig.ArtFile.c_str()); ArtINI.Load(art, false); } diff --git a/code/savefile.cpp b/code/savefile.cpp index e8e3738f5..ae47dfcb7 100644 --- a/code/savefile.cpp +++ b/code/savefile.cpp @@ -9,12 +9,12 @@ #include "savefile.h" +#include "crc.h" + #include -#include +#include #include -#include -#include #include #include @@ -22,36 +22,36 @@ namespace { unsigned char const Signature[4] = { 'O', 'T', 'S', 'V' }; -constexpr unsigned int FLAG_LZO = 0x0001; -constexpr unsigned int FIELD_HEADER_SIZE = 8; -constexpr unsigned int MAX_FIELD_LENGTH = 0x10000; +constexpr std::uint32_t FLAG_LZO = 0x0001; +constexpr std::uint32_t FIELD_HEADER_SIZE = 8; +constexpr std::uint32_t MAX_FIELD_LENGTH = 0x10000; // No game state comes near this, and a header asking for more is asking for memory. -constexpr unsigned int MAX_CONTENT_LENGTH = 0x10000000; +constexpr std::uint32_t MAX_CONTENT_LENGTH = 0x10000000; // A listing is a dozen short fields; a table beyond this is not one. -constexpr unsigned int MAX_TABLE_LENGTH = 0x100000; +constexpr std::uint32_t MAX_TABLE_LENGTH = 0x100000; -unsigned int Get_U16(unsigned char const * from) +std::uint32_t Get_U16(unsigned char const * from) { - return((unsigned int)from[0] | ((unsigned int)from[1] << 8)); + return((std::uint32_t)from[0] | ((std::uint32_t)from[1] << 8)); } -unsigned int Get_U32(unsigned char const * from) +std::uint32_t Get_U32(unsigned char const * from) { - return((unsigned int)from[0] | ((unsigned int)from[1] << 8) - | ((unsigned int)from[2] << 16) | ((unsigned int)from[3] << 24)); + return((std::uint32_t)from[0] | ((std::uint32_t)from[1] << 8) + | ((std::uint32_t)from[2] << 16) | ((std::uint32_t)from[3] << 24)); } -void Put_U16(unsigned char * into, unsigned int value) +void Put_U16(unsigned char * into, std::uint32_t value) { into[0] = (unsigned char)(value & 0xFF); into[1] = (unsigned char)((value >> 8) & 0xFF); } -void Put_U32(unsigned char * into, unsigned int value) +void Put_U32(unsigned char * into, std::uint32_t value) { into[0] = (unsigned char)(value & 0xFF); into[1] = (unsigned char)((value >> 8) & 0xFF); @@ -60,7 +60,7 @@ void Put_U32(unsigned char * into, unsigned int value) } -void Append(std::vector & into, void const * data, unsigned int length) +void Append(std::vector & into, void const * data, std::size_t length) { unsigned char const * bytes = (unsigned char const *)data; into.insert(into.end(), bytes, bytes + length); @@ -81,66 +81,52 @@ bool Reserve(std::vector & buffer, std::size_t length) } -bool Read_Range(std::FILE * file, void * into, unsigned int length) +bool Read_Range(HANDLE file, void * into, std::uint32_t length) { unsigned char * cursor = (unsigned char *)into; while (length > 0) { - std::size_t const got = std::fread(cursor, 1, length, file); - if (got == 0) return(false); + DWORD got = 0; + if (!ReadFile(file, cursor, length, &got, nullptr) || got == 0) return(false); cursor += got; - length -= (unsigned int)got; + length -= got; } return(true); } -bool Write_Range(std::FILE * file, void const * data, unsigned int length) +bool Write_Range(HANDLE file, void const * data, std::uint32_t length) { unsigned char const * cursor = (unsigned char const *)data; while (length > 0) { - unsigned int const block = (length > 0x100000) ? 0x100000 : length; - std::size_t const written = std::fwrite(cursor, 1, block, file); - if (written != block) return(false); + DWORD const block = (length > 0x100000) ? 0x100000 : length; + DWORD written = 0; + if (!WriteFile(file, cursor, block, &written, nullptr) || written != block) return(false); cursor += written; - length -= block; + length -= written; } return(true); } -/// -/// Reports the length of an open file without moving the caller's read position. -/// -/// The length in bytes, or -1. -long File_Length(std::FILE * file) -{ - long const here = std::ftell(file); - if (here < 0 || std::fseek(file, 0, SEEK_END) != 0) return(-1); - long const end = std::ftell(file); - std::fseek(file, here, SEEK_SET); - return(end); -} - - struct HeaderType { - unsigned int Version; - unsigned int Flags; - unsigned int TableLength; - unsigned int ContentOffset; - unsigned int StoredLength; - unsigned int ContentLength; - unsigned int ContentCRC; - unsigned int HeaderCRC; + std::uint32_t Version; + std::uint32_t Flags; + std::uint32_t TableLength; + std::uint32_t ContentOffset; + std::uint32_t StoredLength; + std::uint32_t ContentLength; + std::uint32_t ContentCRC; + std::uint32_t HeaderCRC; }; // The header checksum continues over the field table, so a listing can verify what it // reads without touching the content. -unsigned int Header_CRC(unsigned char const * header, unsigned char const * table, unsigned int length) +std::uint32_t Header_CRC(unsigned char const * header, unsigned char const * table, std::uint32_t length) { return(SaveFileClass::Checksum(table, length, SaveFileClass::Checksum(header, SaveFileClass::HEADER_SIZE - 4))); } @@ -148,7 +134,7 @@ unsigned int Header_CRC(unsigned char const * header, unsigned char const * tabl // Decides everything the first 32 bytes can decide, in the order a caller wants to // hear about it: not ours, a version we do not read, or damage. -SaveFileClass::ResultType Parse_Header(unsigned char const * bytes, unsigned int available, HeaderType & header) +SaveFileClass::ResultType Parse_Header(unsigned char const * bytes, std::uint32_t available, HeaderType & header) { if (available < sizeof(Signature) || memcmp(bytes, Signature, sizeof(Signature)) != 0) { return(SaveFileClass::RESULT_NOT_A_SAVE); @@ -193,28 +179,9 @@ SaveFileClass::SaveFileClass(void) } -unsigned int SaveFileClass::Checksum(unsigned char const * data, unsigned int length, unsigned int seed) +std::uint32_t SaveFileClass::Checksum(unsigned char const * data, std::uint32_t length, std::uint32_t seed) { - static unsigned int table[256]; - static bool ready = false; - - if (!ready) { - for (unsigned int index = 0; index < 256; index++) { - unsigned int value = index; - for (int bit = 0; bit < 8; bit++) { - value = (value & 1) ? (0xEDB88320u ^ (value >> 1)) : (value >> 1); - } - table[index] = value; - } - ready = true; - } - - unsigned int crc = ~seed; - for (unsigned int index = 0; index < length; index++) { - crc = table[(crc ^ data[index]) & 0xFF] ^ (crc >> 8); - } - - return(~crc); + return(CRC::Memory(data, length, seed)); } @@ -239,11 +206,11 @@ SaveFileClass::FieldType const * SaveFileClass::Find(int id, int kind) const for (FieldType const & field : Fields) { if (field.ID == id && field.Kind == kind) return(&field); } - return(NULL); + return(nullptr); } -void SaveFileClass::Set(int id, int kind, void const * data, unsigned int length) +void SaveFileClass::Set(int id, int kind, void const * data, std::size_t length) { for (FieldType & field : Fields) { if (field.ID == id && field.Kind == kind) { @@ -262,15 +229,15 @@ void SaveFileClass::Set(int id, int kind, void const * data, unsigned int length void SaveFileClass::Set_String(int id, char const * text) { - if (text == NULL) text = ""; - Set(id, FIELD_STRING, text, (unsigned int)strlen(text)); + if (text == nullptr) text = ""; + Set(id, FIELD_STRING, text, strlen(text)); } void SaveFileClass::Set_Int(int id, int value) { unsigned char bytes[4]; - Put_U32(bytes, (unsigned int)value); + Put_U32(bytes, (std::uint32_t)value); Set(id, FIELD_INT, bytes, sizeof(bytes)); } @@ -287,18 +254,18 @@ void SaveFileClass::Set_Time(int id, FILETIME const & time) // A string that does not fit is truncated to what does; the result is always terminated. bool SaveFileClass::Get_String(int id, char * text, int size) const { - if (text == NULL || size <= 0) return(false); + if (text == nullptr || size <= 0) return(false); FieldType const * const field = Find(id, FIELD_STRING); - if (field == NULL) { + if (field == nullptr) { text[0] = '\0'; return(false); } - unsigned int length = (unsigned int)field->Bytes.size(); - if (length > (unsigned int)(size - 1)) { + std::size_t length = field->Bytes.size(); + if (length > (std::size_t)(size - 1)) { // A cut never splits a UTF-8 sequence, so a shortened description stays text. - length = (unsigned int)(size - 1); + length = (std::size_t)(size - 1); while (length > 0 && (field->Bytes[length] & 0xC0) == 0x80) length--; } memcpy(text, field->Bytes.data(), length); @@ -311,9 +278,9 @@ bool SaveFileClass::Get_String(int id, char * text, int size) const bool SaveFileClass::Get_Int(int id, int * value) const { FieldType const * const field = Find(id, FIELD_INT); - if (field == NULL || field->Bytes.size() != 4) return(false); + if (field == nullptr || field->Bytes.size() != 4) return(false); - if (value != NULL) *value = (int)Get_U32(field->Bytes.data()); + if (value != nullptr) *value = (int)Get_U32(field->Bytes.data()); return(true); } @@ -321,9 +288,9 @@ bool SaveFileClass::Get_Int(int id, int * value) const bool SaveFileClass::Get_Time(int id, FILETIME * time) const { FieldType const * const field = Find(id, FIELD_TIME); - if (field == NULL || field->Bytes.size() != 8) return(false); + if (field == nullptr || field->Bytes.size() != 8) return(false); - if (time != NULL) { + if (time != nullptr) { time->dwLowDateTime = Get_U32(field->Bytes.data()); time->dwHighDateTime = Get_U32(field->Bytes.data() + 4); } @@ -343,27 +310,27 @@ void SaveFileClass::Serialize_Fields(std::vector & table) const for (FieldType const & field : Fields) { unsigned char head[FIELD_HEADER_SIZE]; - Put_U16(head, (unsigned int)field.ID); - Put_U16(head + 2, (unsigned int)field.Kind); - Put_U32(head + 4, (unsigned int)field.Bytes.size()); + Put_U16(head, (std::uint32_t)field.ID); + Put_U16(head + 2, (std::uint32_t)field.Kind); + Put_U32(head + 4, (std::uint32_t)field.Bytes.size()); Append(table, head, sizeof(head)); - Append(table, field.Bytes.data(), (unsigned int)field.Bytes.size()); + Append(table, field.Bytes.data(), field.Bytes.size()); } } -SaveFileClass::ResultType SaveFileClass::Parse_Fields(unsigned char const * table, unsigned int length) +SaveFileClass::ResultType SaveFileClass::Parse_Fields(unsigned char const * table, std::uint32_t length) { Fields.clear(); - unsigned int offset = 0; + std::uint32_t offset = 0; while (offset < length) { if (length - offset < FIELD_HEADER_SIZE) return(RESULT_CORRUPT); FieldType field; field.ID = (int)Get_U16(table + offset); field.Kind = (int)Get_U16(table + offset + 2); - unsigned int const bytes = Get_U32(table + offset + 4); + std::uint32_t const bytes = Get_U32(table + offset + 4); offset += FIELD_HEADER_SIZE; if (bytes > MAX_FIELD_LENGTH || bytes > length - offset) return(RESULT_CORRUPT); @@ -381,7 +348,7 @@ SaveFileClass::ResultType SaveFileClass::Parse_Fields(unsigned char const * tabl // interrupted at any point leaves the previous file untouched. SaveFileClass::ResultType SaveFileClass::Write(char const * path) const { - if (path == NULL) return(RESULT_WRITE_FAILED); + if (path == nullptr) return(RESULT_WRITE_FAILED); // The reader's limits bind the writer too, so a save this build writes is one it reads, // and one it cannot write leaves the file on disk alone. @@ -394,58 +361,59 @@ SaveFileClass::ResultType SaveFileClass::Write(char const * path) const Serialize_Fields(table); if (table.size() > MAX_TABLE_LENGTH) return(RESULT_TOO_LARGE); - std::vector stored; - unsigned int flags = 0; + // The compressed block is kept only when it is smaller than the content; otherwise + // the content is written where it already sits, rather than copied to be written. + std::vector compressed; + unsigned char const * payload = Content.data(); + std::uint32_t payload_length = (std::uint32_t)Content.size(); + std::uint32_t flags = 0; if (!Content.empty()) { - std::vector work(LZO1X_MEM_COMPRESS); - stored.resize(Content.size() + Content.size() / 16 + 64 + 3); + std::vector work; + if (!Reserve(work, LZO1X_MEM_COMPRESS) + || !Reserve(compressed, Content.size() + Content.size() / 16 + 64 + 3)) { + return(RESULT_NO_MEMORY); + } lzo_uint packed = 0; int const status = lzo1x_1_compress(Content.data(), (lzo_uint)Content.size(), - stored.data(), &packed, work.data()); + compressed.data(), &packed, work.data()); if (status == LZO_E_OK && packed < Content.size()) { - stored.resize((std::size_t)packed); + payload = compressed.data(); + payload_length = (std::uint32_t)packed; flags |= FLAG_LZO; - } else { - stored = Content; } } - std::vector image(HEADER_SIZE); - unsigned char * const header = image.data(); + unsigned char header[HEADER_SIZE]; memcpy(header, Signature, sizeof(Signature)); Put_U16(header + 4, FORMAT_VERSION); Put_U16(header + 6, flags); - Put_U32(header + 8, (unsigned int)table.size()); - Put_U32(header + 12, HEADER_SIZE + (unsigned int)table.size()); - Put_U32(header + 16, (unsigned int)stored.size()); - Put_U32(header + 20, (unsigned int)Content.size()); - Put_U32(header + 24, Checksum(stored.data(), (unsigned int)stored.size())); - Put_U32(header + 28, Header_CRC(header, table.data(), (unsigned int)table.size())); - - image.insert(image.end(), table.begin(), table.end()); - image.insert(image.end(), stored.begin(), stored.end()); + Put_U32(header + 8, (std::uint32_t)table.size()); + Put_U32(header + 12, HEADER_SIZE + (std::uint32_t)table.size()); + Put_U32(header + 16, payload_length); + Put_U32(header + 20, (std::uint32_t)Content.size()); + Put_U32(header + 24, Checksum(payload, payload_length)); + // The header checksum covers everything before itself, so it is filled in last. + Put_U32(header + 28, Header_CRC(header, table.data(), (std::uint32_t)table.size())); std::string const temporary = std::string(path) + ".tmp"; - std::FILE * const file = std::fopen(temporary.c_str(), "wb"); - if (file == NULL) return(RESULT_WRITE_FAILED); + HANDLE const file = CreateFileA(temporary.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) return(RESULT_WRITE_FAILED); - bool ok = Write_Range(file, image.data(), (unsigned int)image.size()); - if (ok) ok = (std::fflush(file) == 0); - if (std::fclose(file) != 0) ok = false; + bool ok = Write_Range(file, header, HEADER_SIZE); + if (ok && !table.empty()) ok = Write_Range(file, table.data(), (std::uint32_t)table.size()); + if (ok && payload_length > 0) ok = Write_Range(file, payload, payload_length); + if (ok) ok = (FlushFileBuffers(file) != FALSE); + if (!CloseHandle(file)) ok = false; - if (ok) { - std::error_code error; - std::filesystem::rename(temporary, path, error); - ok = !error; - } + if (ok) ok = (MoveFileExA(temporary.c_str(), path, MOVEFILE_REPLACE_EXISTING) != FALSE); if (!ok) { - std::error_code error; - std::filesystem::remove(temporary, error); + DeleteFileA(temporary.c_str()); return(RESULT_WRITE_FAILED); } @@ -458,24 +426,24 @@ SaveFileClass::ResultType SaveFileClass::Read(char const * path) Fields.clear(); Content.clear(); - if (path == NULL) return(RESULT_MISSING); + if (path == nullptr) return(RESULT_MISSING); - std::FILE * const file = std::fopen(path, "rb"); - if (file == NULL) return(RESULT_MISSING); + HANDLE const file = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) return(RESULT_MISSING); // The header is judged before anything the file's size could ask for is allocated. unsigned char head[HEADER_SIZE]; - unsigned int const got = (unsigned int)std::fread(head, 1, HEADER_SIZE, file); - bool const ok = !std::ferror(file); + DWORD got = 0; + bool const ok = (ReadFile(file, head, HEADER_SIZE, &got, nullptr) != FALSE); HeaderType header; ResultType result = ok ? Parse_Header(head, got, header) : RESULT_CORRUPT; std::vector image; if (result == RESULT_OK) { - long const length = File_Length(file); - unsigned int const size = (unsigned int)length; - if (length < 0 || size != header.ContentOffset + header.StoredLength) { + DWORD const size = GetFileSize(file, nullptr); + if (size == INVALID_FILE_SIZE || size != header.ContentOffset + header.StoredLength) { result = RESULT_CORRUPT; } else if (!Reserve(image, size)) { result = RESULT_NO_MEMORY; @@ -484,7 +452,7 @@ SaveFileClass::ResultType SaveFileClass::Read(char const * path) if (!Read_Range(file, image.data() + HEADER_SIZE, size - HEADER_SIZE)) result = RESULT_CORRUPT; } } - std::fclose(file); + CloseHandle(file); if (result != RESULT_OK) return(result); if (Header_CRC(image.data(), image.data() + HEADER_SIZE, header.TableLength) != header.HeaderCRC) { @@ -508,7 +476,7 @@ SaveFileClass::ResultType SaveFileClass::Read(char const * path) lzo_uint unpacked = (lzo_uint)Content.size(); int const status = lzo1x_decompress_safe(stored, (lzo_uint)header.StoredLength, - Content.data(), &unpacked, NULL); + Content.data(), &unpacked, nullptr); if (status != LZO_E_OK || unpacked != header.ContentLength) { Fields.clear(); @@ -538,23 +506,23 @@ SaveFileClass::ResultType SaveFileClass::Read_Fields(char const * path) Fields.clear(); Content.clear(); - if (path == NULL) return(RESULT_MISSING); + if (path == nullptr) return(RESULT_MISSING); - std::FILE * const file = std::fopen(path, "rb"); - if (file == NULL) return(RESULT_MISSING); + HANDLE const file = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) return(RESULT_MISSING); unsigned char head[HEADER_SIZE]; - unsigned int const got = (unsigned int)std::fread(head, 1, HEADER_SIZE, file); - bool ok = !std::ferror(file); + DWORD got = 0; + bool ok = (ReadFile(file, head, HEADER_SIZE, &got, nullptr) != FALSE); HeaderType header; ResultType result = ok ? Parse_Header(head, got, header) : RESULT_CORRUPT; std::vector table; if (result == RESULT_OK && header.TableLength > 0) { - long const length = File_Length(file); - unsigned int const size = (unsigned int)length; - if (length < 0 || header.TableLength > size - HEADER_SIZE) { + DWORD const size = GetFileSize(file, nullptr); + if (size == INVALID_FILE_SIZE || header.TableLength > size - HEADER_SIZE) { result = RESULT_CORRUPT; } else if (!Reserve(table, header.TableLength)) { result = RESULT_NO_MEMORY; @@ -562,10 +530,10 @@ SaveFileClass::ResultType SaveFileClass::Read_Fields(char const * path) if (!Read_Range(file, table.data(), header.TableLength)) result = RESULT_CORRUPT; } } - std::fclose(file); + CloseHandle(file); if (result != RESULT_OK) return(result); - if (Header_CRC(head, table.data(), (unsigned int)table.size()) != header.HeaderCRC) return(RESULT_CORRUPT); + if (Header_CRC(head, table.data(), (std::uint32_t)table.size()) != header.HeaderCRC) return(RESULT_CORRUPT); - return(Parse_Fields(table.data(), (unsigned int)table.size())); + return(Parse_Fields(table.data(), (std::uint32_t)table.size())); } diff --git a/code/savefile.h b/code/savefile.h index f6b3afa69..2a25968e0 100644 --- a/code/savefile.h +++ b/code/savefile.h @@ -14,6 +14,7 @@ #endif #include +#include #include // The file a saved game is kept in: a fixed header, a table of listing fields, and one @@ -52,7 +53,7 @@ class SaveFileClass ResultType Read_Fields(char const * path); static char const * Result_Text(ResultType result); - static unsigned int Checksum(unsigned char const * data, unsigned int length, unsigned int seed = 0); + static std::uint32_t Checksum(unsigned char const * data, std::uint32_t length, std::uint32_t seed = 0); std::vector Content; @@ -70,9 +71,9 @@ class SaveFileClass }; FieldType const * Find(int id, int kind) const; - void Set(int id, int kind, void const * data, unsigned int length); + void Set(int id, int kind, void const * data, std::size_t length); void Serialize_Fields(std::vector & table) const; - ResultType Parse_Fields(unsigned char const * table, unsigned int length); + ResultType Parse_Fields(unsigned char const * table, std::uint32_t length); std::vector Fields; }; diff --git a/code/saveload.cpp b/code/saveload.cpp index ed692b3b3..df3ff9062 100644 --- a/code/saveload.cpp +++ b/code/saveload.cpp @@ -46,6 +46,7 @@ #include "saveload.h" +#include "_deploymentconfig.h" #include "_logic.h" #include "_map.h" #include "_rect.h" @@ -69,6 +70,7 @@ #include "classfactory.h" #include "data.h" #include "dbgprint.h" +#include "deploymentconfig.h" #include "empulse.h" #include "enviro.h" #include "factory.h" @@ -143,28 +145,10 @@ #include "objheaps.hh" #include +#include +#include #include -#include -#include - -/// The save record stamps its times in the Windows epoch: hundred nanosecond ticks since the -/// start of 1601. The host clock counts from 1970, so the difference between the two is added. -static void Fetch_System_File_Time(FILETIME* result) -{ -#ifdef _WIN32 - GetSystemTimeAsFileTime(result); -#else - unsigned long long const epoch_difference = 116444736000000000ULL; - unsigned long long const now = (unsigned long long)std::chrono::duration_cast>>( - std::chrono::system_clock::now().time_since_epoch()).count(); - unsigned long long const ticks = now + epoch_difference; - result->dwLowDateTime = (DWORD)(ticks & 0xFFFFFFFFULL); - result->dwHighDateTime = (DWORD)(ticks >> 32); -#endif -} - - //#define SAVE_BLOCK_SIZE 512 #define SAVE_BLOCK_SIZE 4096 //#define SAVE_BLOCK_SIZE 1024 @@ -176,15 +160,16 @@ unsigned int ExpectedGameVersion = LoadOptionsClass::GAMEVER_OPENTS; /// -/// Writes one object to the save stream as a record of its own. -/// The record is the class identifier, the length of what follows, and whatever the -/// object's Save writes; a reader that does not consume exactly that length has read a -/// record of a different shape than was written. +/// Writes one object to the save stream as a record of its own. A reader that does not +/// consume exactly the record's length has read a record of another shape than was +/// written, and a missing object fails the stream rather than leaving a gap where the +/// reader expects one. /// /// bool; Was the record written whole? bool Save_Object(SaveStreamClass & stream, IPersistent * persist) { - if (persist == NULL) { + if (persist == nullptr) { + stream.Fail(); return(false); } @@ -209,7 +194,8 @@ bool Save_Object(SaveStreamClass & stream, IPersistent * persist) bool Save_Object(SaveStreamClass & stream, ILocomotion * locomotion) { IPersistent * const persist = dynamic_cast(locomotion); - if (persist == NULL) { + if (persist == nullptr) { + stream.Fail(); return(false); } return(Save_Object(stream, persist)); @@ -217,61 +203,75 @@ bool Save_Object(SaveStreamClass & stream, ILocomotion * locomotion) /// -/// Recreates one object from the save stream. -/// The object is created through the class registered for the identifier the record -/// carries, and reattaches itself to its own heap as it is constructed. +/// Recreates one object from the save stream. It reattaches itself to its own heap as it +/// is constructed, so the caller is handed it only to keep or to refuse. /// -/// The object, or NULL with the stream failed when the identifier names no -/// registered class, the object could not read its record, or the record's length does -/// not match what the object consumed. -IPersistent * Load_Object(SaveStreamClass & stream) +/// Asked whether the object is of the class the caller expects, once +/// the record has been read and before the object takes its place. May be null when any +/// class will do. +/// The object, owned by the caller, or nothing with the stream failed when the +/// identifier names no registered class, the object could not read its record, the record's +/// length does not match what the object consumed, or the class is not the one asked +/// for. +std::unique_ptr Load_Object(SaveStreamClass & stream, bool (*accepts)(IPersistent const * object)) { ClassID classid; unsigned int length = 0; stream.Serialize_Bytes(&classid, sizeof(classid)); stream.Serialize(length); if (stream.Was_Error()) { - return(NULL); + return(nullptr); } unsigned int const start = stream.Offset(); if (length > stream.Size() - start) { DebugString("Save record at %u claims %u bytes, past the end of the save\n", start, length); stream.Fail(); - return(NULL); + return(nullptr); } SwizzleManagerClass::MarkType const mark = Swizzler.Mark(); - std::unique_ptr persist(Create_Object(classid)); - if (persist == NULL) { + std::unique_ptr persist = Create_Object(classid); + if (persist == nullptr) { DebugString("Save record at %u names a class this build does not register\n", start); stream.Fail(); - return(NULL); + return(nullptr); } - bool ok = persist->Load(stream); + bool ok; + { + SaveStreamClass::BoundScope const bound(stream, start + length); + ok = persist->Load(stream); + } if (ok && stream.Offset() != start + length) { DebugString("Save record of %s at %u is %u bytes but %u were read\n", typeid(*persist).name(), start, length, stream.Offset() - start); ok = false; } + if (ok && accepts != nullptr && !accepts(persist.get())) { + DebugString("Save record of %s at %u is not the class expected there\n", + typeid(*persist).name(), start); + ok = false; + } if (!ok) { Swizzler.Abandon(mark); stream.Fail(); - return(NULL); + return(nullptr); } persist->Post_Load(); - return(persist.release()); + return(persist); } /// /// Loads a vector of persistent objects from the save game stream. /// The objects are not handed back -- each one reattaches itself to its own heap as it is -/// constructed, which is what refills the game's vectors. +/// constructed, which is what refills the game's vectors. A record naming any class other +/// than the heap's fails the load, since nothing else belongs in that heap. /// /// bool; Was the record read whole? +template static bool Load_Vector(SaveStreamClass & stream) { int count = 0; @@ -280,13 +280,18 @@ static bool Load_Vector(SaveStreamClass & stream) return(false); } if (count < 0) { + stream.Fail(); return(false); } for (int index = 0; index < count; index++) { - if (Load_Object(stream) == NULL) { + std::unique_ptr object = Load_Object_As(stream); + if (object == nullptr) { return(false); } + // The object attached itself to its own heap as it was constructed, and the heap + // is what deletes it from here on. + object.release(); } return(true); } @@ -768,7 +773,7 @@ static bool Get_All(SaveStreamClass & stream, bool save_net) RulesClass::Load_Art_INI(); if (Addon_Enabled(ADDON_FIRESTORM) == true) { - CCFileClass artfs("ARTFS.INI"); + CCFileClass artfs(DeploymentConfig.ArtExpansionFile.c_str()); if (artfs.Is_Available() == true) { ArtINI.Load(artfs, false); } @@ -781,13 +786,15 @@ static bool Get_All(SaveStreamClass & stream, bool save_net) return(false); } - if (!Load_Vector(stream)) { /// AnimTypes + if (!Load_Vector(stream)) { /// AnimTypes return(false); } - Map.Load(stream); + if (!Map.Load(stream)) { + return(false); + } - if (!Load_Vector(stream)) { /// Tubes + if (!Load_Vector(stream)) { /// Tubes return(false); } @@ -796,170 +803,167 @@ static bool Get_All(SaveStreamClass & stream, bool save_net) } Map.Reset_All_Subzones(); - Logic.Load(stream); + if (!Logic.Load(stream)) { + return(false); + } if (TacticalMap != NULL) { delete TacticalMap; TacticalMap = NULL; } - SwizzleManagerClass::MarkType const mark = Swizzler.Mark(); - IPersistent * const object = Load_Object(stream); - Tactical * const old_tactical = dynamic_cast(object); - if (object != NULL && old_tactical == NULL) { - DebugString("Save record of %s at %u is not the tactical map\n", typeid(*object).name(), stream.Offset()); - Swizzler.Abandon(mark); - delete object; - stream.Fail(); - } - if (old_tactical == NULL) { + std::unique_ptr tactical = Load_Object_As(stream); + if (tactical == nullptr) { return(false); } + // The map installed itself in TacticalMap as it was constructed, and that global is + // what deletes it from here on. + tactical.release(); - if (!Load_Vector(stream)) { /// HouseTypes + if (!Load_Vector(stream)) { /// HouseTypes return(false); } - if (!Load_Vector(stream)) { /// Houses + if (!Load_Vector(stream)) { /// Houses return(false); } - if (!Load_Vector(stream)) { /// Units + if (!Load_Vector(stream)) { /// Units return(false); } - if (!Load_Vector(stream)) { /// UnitTypes + if (!Load_Vector(stream)) { /// UnitTypes return(false); } - if (!Load_Vector(stream)) { /// InfantryTypes + if (!Load_Vector(stream)) { /// InfantryTypes return(false); } - if (!Load_Vector(stream)) { /// Infantry + if (!Load_Vector(stream)) { /// Infantry return(false); } - if (!Load_Vector(stream)) { /// BuildingTypes + if (!Load_Vector(stream)) { /// BuildingTypes return(false); } - if (!Load_Vector(stream)) { /// Buildings + if (!Load_Vector(stream)) { /// Buildings return(false); } - if (!Load_Vector(stream)) { /// AircraftTypes + if (!Load_Vector(stream)) { /// AircraftTypes return(false); } - if (!Load_Vector(stream)) { /// Aircraft + if (!Load_Vector(stream)) { /// Aircraft return(false); } - if (!Load_Vector(stream)) { /// Anims + if (!Load_Vector(stream)) { /// Anims return(false); } - if (!Load_Vector(stream)) { /// TaskForces + if (!Load_Vector(stream)) { /// TaskForces return(false); } - if (!Load_Vector(stream)) { /// TeamTypes + if (!Load_Vector(stream)) { /// TeamTypes return(false); } - if (!Load_Vector(stream)) { /// Teams + if (!Load_Vector(stream)) { /// Teams return(false); } - if (!Load_Vector(stream)) { /// ScriptTypes + if (!Load_Vector(stream)) { /// ScriptTypes return(false); } - if (!Load_Vector(stream)) { /// Scripts + if (!Load_Vector(stream)) { /// Scripts return(false); } - if (!Load_Vector(stream)) { /// TagTypes + if (!Load_Vector(stream)) { /// TagTypes return(false); } - if (!Load_Vector(stream)) { /// Tags + if (!Load_Vector(stream)) { /// Tags return(false); } - if (!Load_Vector(stream)) { /// TriggerTypes + if (!Load_Vector(stream)) { /// TriggerTypes return(false); } - if (!Load_Vector(stream)) { /// Triggers + if (!Load_Vector(stream)) { /// Triggers return(false); } - if (!Load_Vector(stream)) { /// AITriggerTypes + if (!Load_Vector(stream)) { /// AITriggerTypes return(false); } - if (!Load_Vector(stream)) { /// Actions + if (!Load_Vector(stream)) { /// Actions return(false); } - if (!Load_Vector(stream)) { /// Events + if (!Load_Vector(stream)) { /// Events return(false); } - if (!Load_Vector(stream)) { /// Factories + if (!Load_Vector(stream)) { /// Factories return(false); } - if (!Load_Vector(stream)) { /// VoxelAnimTypes + if (!Load_Vector(stream)) { /// VoxelAnimTypes return(false); } - if (!Load_Vector(stream)) { /// VoxelAnims + if (!Load_Vector(stream)) { /// VoxelAnims return(false); } - if (!Load_Vector(stream)) { /// Warheads + if (!Load_Vector(stream)) { /// Warheads return(false); } - if (!Load_Vector(stream)) { /// Weapons + if (!Load_Vector(stream)) { /// Weapons return(false); } - if (!Load_Vector(stream)) { /// ParticleTypes + if (!Load_Vector(stream)) { /// ParticleTypes return(false); } - if (!Load_Vector(stream)) { /// Particles + if (!Load_Vector(stream)) { /// Particles return(false); } - if (!Load_Vector(stream)) { /// ParticleSystemTypes + if (!Load_Vector(stream)) { /// ParticleSystemTypes return(false); } - if (!Load_Vector(stream)) { /// ParticleSystems + if (!Load_Vector(stream)) { /// ParticleSystems return(false); } - if (!Load_Vector(stream)) { /// BulletTypes + if (!Load_Vector(stream)) { /// BulletTypes return(false); } - if (!Load_Vector(stream)) { /// Bullets + if (!Load_Vector(stream)) { /// Bullets return(false); } - if (!Load_Vector(stream)) { /// WaypointPaths + if (!Load_Vector(stream)) { /// WaypointPaths return(false); } - if (!Load_Vector(stream)) { /// SmudgeTypes + if (!Load_Vector(stream)) { /// SmudgeTypes return(false); } - if (!Load_Vector(stream)) { /// OverlayTypes + if (!Load_Vector(stream)) { /// OverlayTypes return(false); } - if (!Load_Vector(stream)) { /// LightSources + if (!Load_Vector(stream)) { /// LightSources return(false); } - if (!Load_Vector(stream)) { /// BuildingLights + if (!Load_Vector(stream)) { /// BuildingLights return(false); } - if (!Load_Vector(stream)) { /// Sides + if (!Load_Vector(stream)) { /// Sides return(false); } - if (!Load_Vector(stream)) { /// Tiberiums + if (!Load_Vector(stream)) { /// Tiberiums return(false); } - if (!Load_Vector(stream)) { /// EMPulseClass::EMPulses + if (!Load_Vector(stream)) { /// EMPulseClass::EMPulses return(false); } - if (!Load_Vector(stream)) { /// SuperWeaponTypes + if (!Load_Vector(stream)) { /// SuperWeaponTypes return(false); } - if (!Load_Vector(stream)) { /// SuperWeapons + if (!Load_Vector(stream)) { /// SuperWeapons return(false); } - if (!Load_Vector(stream)) { /// TerrainTypes + if (!Load_Vector(stream)) { /// TerrainTypes return(false); } - if (!Load_Vector(stream)) { /// Terrains + if (!Load_Vector(stream)) { /// Terrains return(false); } - if (!Load_Vector(stream)) { /// FoggedObjectClass::FoggyObjects + if (!Load_Vector(stream)) { /// FoggedObjectClass::FoggyObjects return(false); } - if (!Load_Vector(stream)) { /// AlphaShapes + if (!Load_Vector(stream)) { /// AlphaShapes return(false); } - if (!Load_Vector(stream)) { /// Waves + if (!Load_Vector(stream)) { /// Waves return(false); } if (!VeinholeMonsterClass::Load_All(stream)) { @@ -1038,7 +1042,7 @@ bool Save_Game(const char *file_name, char const * descr) info.Set_Game_Type(Session.Type); FILETIME FileTime; - Fetch_System_File_Time(&FileTime); + GetSystemTimeAsFileTime(&FileTime); info.Set_Last_Time(FileTime); info.Set_Start_Time(FileTime); info.Set_Play_Time(FileTime); @@ -1115,16 +1119,9 @@ bool Load_Game(const char *file_name) { DebugString("\nLOADING GAME [%s]\n", file_name); - SaveVersionInfo info; - if (!Get_Savefile_Info(file_name, &info)) { - return(false); - } - if (info.Get_Internal_Version() != ExpectedGameVersion) { - return(false); - } - // The whole file is checked before the running game is torn down, so a damaged - // save costs nothing. + // save costs nothing. The listing fields come back with it, so the version this + // build will not read is judged on the same read rather than on a second one. SaveFileClass file; SaveFileClass::ResultType const result = file.Read(Saved_Game_Name(file_name).c_str()); if (result != SaveFileClass::RESULT_OK) { @@ -1132,18 +1129,37 @@ bool Load_Game(const char *file_name) return(false); } + SaveVersionInfo info; + if (!info.Load(file)) { + return(false); + } + if (info.Get_Internal_Version() != ExpectedGameVersion) { + return(false); + } + LoadedSaveVersion = info.Get_Internal_Version(); Session.Type = (GameType)info.Get_Game_Type(); Swizzler.Discard(); SaveStreamClass stream(file.Content, SaveStreamClass::MODE_LOAD); - bool res = Get_All(stream, false); + bool res = false; + // The catch sits here rather than around the whole routine because what was already + // loaded still has to be abandoned below. Both of the ways a count read from the file + // can end an allocation are refused here; anything else still raises. + try { + res = Get_All(stream, false); + } catch (std::bad_alloc const &) { + DebugString("\t***** FAILED! (out of memory at %u of %u bytes)\n", stream.Offset(), stream.Size()); + } catch (std::length_error const &) { + DebugString("\t***** FAILED! (a count no container can hold at %u of %u bytes)\n", + stream.Offset(), stream.Size()); + } if (!res) { DebugString("\t***** FAILED! (at %u of %u bytes)\n", stream.Offset(), stream.Size()); - // What was loaded stays in the heaps until the next teardown, which must not - // follow the identities still sitting in its pointer slots. - Swizzler.Abandon(); + // What was loaded stays in the heaps until the next teardown, so the requests it + // registered must not be answered into it once the game that follows has moved on. + Swizzler.Discard(); return(false); } if (stream.Offset() != stream.Size()) { @@ -1290,7 +1306,7 @@ int Load_Misc_Values(SaveStreamClass & stream) *=========================================================================*/ bool Get_Savefile_Info(char const * name, SaveVersionInfo * info) { - if (name == NULL || info == NULL) { + if (name == nullptr || info == nullptr) { return(false); } diff --git a/code/saveload.h b/code/saveload.h index bab276bf6..556fe04d9 100644 --- a/code/saveload.h +++ b/code/saveload.h @@ -16,6 +16,7 @@ #include "persist.h" #include +#include class SaveStreamClass; class SaveVersionInfo; @@ -27,11 +28,34 @@ struct ILocomotion; int Load_Misc_Values(SaveStreamClass & stream); int Save_Misc_Values(SaveStreamClass & stream); -// An object travels as its class identifier, the length of its record, and the record. -// A locomotor loaded this way is handed back unowned; the caller takes it. +// A loaded object is handed back owned; one that belongs to a heap is released there by +// the caller that puts it in one. docs/SAVE-FORMAT.md records what a record holds. bool Save_Object(SaveStreamClass & stream, IPersistent * object); bool Save_Object(SaveStreamClass & stream, ILocomotion * locomotion); -IPersistent * Load_Object(SaveStreamClass & stream); +std::unique_ptr Load_Object(SaveStreamClass & stream, + bool (*accepts)(IPersistent const * object) = nullptr); + +/// +/// Loads the record next in the stream and requires it to be of the class asked for. +/// +/// The object, owned by the caller, or nothing with the stream failed when the +/// record holds another class. A record of the wrong class is destroyed before it can take +/// its place, so the test happens while the object is still only the reader's. +template +std::unique_ptr Load_Object_As(SaveStreamClass & stream) +{ + std::unique_ptr object = Load_Object(stream, [](IPersistent const * candidate) { + return(dynamic_cast(candidate) != nullptr); + }); + + // The record was accepted only if it holds a T, so this cast answers for what was loaded. + T * const wanted = dynamic_cast(object.get()); + if (wanted != nullptr) { + object.release(); + } + return(std::unique_ptr(wanted)); +} + bool Get_Savefile_Info(char const * name, SaveVersionInfo * info); bool Save_Game(const char *file_name, char const * descr); bool Load_Game(const char *file_name); diff --git a/code/savestream.cpp b/code/savestream.cpp index a37a2e55b..ca5e7d1d3 100644 --- a/code/savestream.cpp +++ b/code/savestream.cpp @@ -28,6 +28,7 @@ unsigned int LoadedSaveVersion = 0; SaveStreamClass::SaveStreamClass(std::vector & buffer, ModeType mode) : Buffer(&buffer), Cursor(mode == MODE_SAVE ? (unsigned int)buffer.size() : 0), + Limit((unsigned int)buffer.size()), Mode(mode), Failed(false), FormatVersion(mode == MODE_LOAD ? LoadedSaveVersion : ExpectedGameVersion), @@ -39,9 +40,7 @@ SaveStreamClass::SaveStreamClass(std::vector & buffer, ModeType m void SaveStreamClass::Fail(void) { - if (!Failed) { - Failed = true; - } + Failed = true; } @@ -70,7 +69,10 @@ void SaveStreamClass::Serialize_Bytes(void * data, int length) Buffer->insert(Buffer->end(), bytes, bytes + length); Cursor = (unsigned int)Buffer->size(); } else { - if ((unsigned int)length > Buffer->size() - Cursor) { + // The bound is the record being read rather than the whole stream, so a member that + // reads more than its own is refused instead of quietly spending the bytes of the + // record after it. + if ((unsigned int)length > Limit - Cursor) { Failed = true; return; } diff --git a/code/savestream.h b/code/savestream.h index 4ccf359b3..9655cbde3 100644 --- a/code/savestream.h +++ b/code/savestream.h @@ -15,7 +15,9 @@ #include "win.h" #include +#include #include +#include #include #include #include @@ -114,6 +116,32 @@ class SaveStreamClass * Where the next byte goes or comes from, so a record can be framed by its length. */ unsigned int Offset(void) const {return(Cursor);} + + /* + * Holds a load to one record while it is read, so that a member reading more than + * its record holds is refused rather than spending the bytes of the record after + * it. A record nested in another leaves the outer one bounded as it was. + */ + class BoundScope + { + public: + BoundScope(SaveStreamClass & stream, unsigned int end) + : Stream(stream), Previous(stream.Limit) + { + if (end <= Stream.Limit) { + Stream.Limit = end; + } + } + + ~BoundScope(void) {Stream.Limit = Previous;} + + BoundScope(BoundScope const &) = delete; + BoundScope & operator=(BoundScope const &) = delete; + + private: + SaveStreamClass & Stream; + unsigned int Previous; + }; unsigned int Size(void) const {return((unsigned int)Buffer->size());} void Overwrite_Bytes(unsigned int offset, void const * data, int length); @@ -127,7 +155,7 @@ class SaveStreamClass bool Fits(int count, std::size_t each) { if (Is_Loading()) { - std::size_t const room = (std::size_t)(Buffer->size() - Cursor) / (each > 0 ? each : 1); + std::size_t const room = (std::size_t)(Limit - Cursor) / (each > 0 ? each : 1); if (count < 0 || (std::size_t)count > room) { Fail(); return(false); @@ -136,6 +164,31 @@ class SaveStreamClass return(true); } + /* + * Sizes a container the count asked for, failing the pass rather than throwing when + * the process cannot hold it. A count within the bytes remaining still asks for that + * many elements, which is more memory than the stream itself occupies, and for a + * wide element more than the container itself will hold. + */ + template + bool Reserve(C & container, int count) + { + if (count < 0 || (std::size_t)count > container.max_size()) { + Fail(); + return(false); + } + + try { + container.clear(); + container.resize((std::size_t)count); + } catch (std::bad_alloc const &) { + container.clear(); + Fail(); + return(false); + } + return(true); + } + /* * Numbers and enumerations travel as their declared width. */ @@ -195,6 +248,39 @@ class SaveStreamClass } } + // How much room a buffer keeps for its text is this build's business rather than + // the file's, so only the text travels. The last byte stays the terminator, since + // every reader of these buffers treats them as C strings. This claims every + // char[N], so one holding bytes rather than text would be cut at its first zero. + template + void Serialize(char (&value)[N], std::source_location const & = std::source_location::current()) + { + int count = 0; + + if (Is_Saving()) { + while (count < N - 1 && value[count] != '\0') { + count++; + } + } + + Serialize(count); + + if (Is_Loading() && (count < 0 || count >= N)) { + Fail(); + return; + } + + if (count > 0) { + Serialize_Bytes(value, count); + } + + if (Is_Loading()) { + for (int index = count; index < N; index++) { + value[index] = '\0'; + } + } + } + /* * The standard library's fixed size array travels as the built in one does. */ @@ -226,8 +312,9 @@ class SaveStreamClass if (!Fits(count, (std::is_arithmetic_v || std::is_enum_v) ? sizeof(T) : 1)) { return; } - value.clear(); - value.resize(count); + if (!Reserve(value, count)) { + return; + } } if constexpr (std::is_arithmetic_v || std::is_enum_v) { @@ -276,8 +363,9 @@ class SaveStreamClass if (!Fits(count, 1)) { return; } - value.clear(); - value.resize(count); + if (!Reserve(value, count)) { + return; + } } for (int index = 0; index < count; index++) { @@ -296,10 +384,9 @@ class SaveStreamClass Serialize(count); if (Is_Loading()) { - if (!Fits(count, 1)) { + if (!Fits(count, 1) || !Reserve(value, count)) { return; } - value.assign((std::size_t)count, false); } for (int index = 0; index < count; index++) { @@ -318,10 +405,9 @@ class SaveStreamClass Serialize(count); if (Is_Loading()) { - if (!Fits(count, 1)) { + if (!Fits(count, 1) || !Reserve(value, count)) { return; } - value.resize(count); } if (count > 0) { @@ -356,6 +442,9 @@ class SaveStreamClass std::vector * Buffer; unsigned int Cursor; + + // A read is judged against the record being loaded rather than the whole stream. + unsigned int Limit; ModeType Mode; bool Failed; unsigned int FormatVersion; diff --git a/code/savever.cpp b/code/savever.cpp index 50d67725e..68424fb9b 100644 --- a/code/savever.cpp +++ b/code/savever.cpp @@ -31,8 +31,6 @@ SaveVersionInfo::SaveVersionInfo(void) : { ScenarioDescription[0] = '\0'; PlayerHouse[0] = '\0'; - UnknownString[0] = '\0'; - PlayerName[0] = '\0'; ExecutableName[0] = '\0'; StartTime.dwLowDateTime = 0; @@ -175,50 +173,6 @@ int SaveVersionInfo::Get_Scenario_Number(void) } -/// -/// Records the spare string kept with the save information. -/// The string is truncated if it will not fit the buffer it is kept in. -/// -void SaveVersionInfo::Set_Unknown_String(const char * str) -{ - UnknownString[sizeof(UnknownString) - 1] = 0; - strncpy(UnknownString, str, sizeof(UnknownString) - 1); -} - - -/// -/// Fetches the spare string kept with the save information. -/// Neither the save nor the load routine records this string, so it only ever holds what -/// the current session put there. -/// -/// Returns with the string most recently set. -const char * SaveVersionInfo::Get_Unknown_String(void) -{ - return(UnknownString); -} - - -/// -/// Records the name of the player making the save. -/// The name is truncated if it will not fit the buffer it is kept in. -/// -void SaveVersionInfo::Set_Player_Name(const char * name) -{ - PlayerName[sizeof(PlayerName) - 1] = 0; - strncpy(PlayerName, name, sizeof(PlayerName) - 1); -} - - -/// -/// Fetches the name of the player who made the save. -/// -/// Returns with the player name recorded in the save. -const char * SaveVersionInfo::Get_Player_Name(void) -{ - return(PlayerName); -} - - /// /// Records the name of the program writing the save. /// The name is truncated if it will not fit the buffer it is kept in. @@ -330,8 +284,6 @@ void SaveVersionInfo::Save(SaveFileClass & file) const file.Set_Time(PIDSI_LAST_SAVE_TIME, LastSaveTime); file.Set_Time(PIDSI_G_PLAY_TIME, PlayTime); file.Set_String(PIDSI_EXEC_NAME, ExecutableName); - file.Set_String(PIDSI_PLAYER_NAME1, PlayerName); - file.Set_String(PIDSI_PLAYER_NAME2, PlayerName); file.Set_Int(PIDSI_SCENARIO_NUM, ScenarioNumber); file.Set_Int(PIDSI_CAMPAIGN_NUM, CampaignNumber); file.Set_Int(PIDSI_GAME_TYPE, GameType); @@ -351,7 +303,6 @@ bool SaveVersionInfo::Load(SaveFileClass const & file) file.Get_Time(PIDSI_LAST_SAVE_TIME, &LastSaveTime); file.Get_Time(PIDSI_G_PLAY_TIME, &PlayTime); file.Get_String(PIDSI_EXEC_NAME, ExecutableName, sizeof(ExecutableName)); - file.Get_String(PIDSI_PLAYER_NAME1, PlayerName, sizeof(PlayerName)); file.Get_Int(PIDSI_SCENARIO_NUM, &ScenarioNumber); file.Get_Int(PIDSI_CAMPAIGN_NUM, &CampaignNumber); file.Get_Int(PIDSI_GAME_TYPE, &GameType); diff --git a/code/savever.h b/code/savever.h index e6b923015..337ef0eb6 100644 --- a/code/savever.h +++ b/code/savever.h @@ -16,6 +16,8 @@ class SaveFileClass; enum { PIDSI_SCEN_DESCRIP = 2, PIDSI_PLAYER_HOUSE = 3, + // Nothing writes a player name; the identifiers stay reserved because the property set + // this table replaced numbered them. PIDSI_PLAYER_NAME1 = 4, PIDSI_PLAYER_NAME2 = 8, PIDSI_G_VERSION = 9, @@ -53,12 +55,6 @@ class SaveVersionInfo void Set_Scenario_Number(int num); int Get_Scenario_Number(void); - void Set_Unknown_String(const char * name); - const char * Get_Unknown_String(void); - - void Set_Player_Name(const char * name); - const char * Get_Player_Name(void); - void Set_Executable_Name(const char * name); const char * Get_Executable_Name(void); @@ -108,18 +104,6 @@ class SaveVersionInfo int CampaignNumber; int ScenarioNumber; - /* - * This is a spare string carried with the save information, reachable only through - * its own accessors. Neither the save nor the load routine records it. - */ - char UnknownString[260]; - - /* - * This is the name of the player who made the save, which is recorded separately - * from the house so that the person and the side are both known. - */ - char PlayerName[64]; - /* * This is the name of the program that wrote the save, so a file can be traced back * to what produced it rather than merely to a version number. @@ -139,4 +123,3 @@ class SaveVersionInfo */ int GameType; }; - diff --git a/code/session.cpp b/code/session.cpp index 93f2dc122..23a979b3a 100644 --- a/code/session.cpp +++ b/code/session.cpp @@ -48,6 +48,7 @@ #include "session.h" +#include "_deploymentconfig.h" #include "_keyboar.h" #include "_map.h" #include "_rules.h" @@ -56,6 +57,7 @@ #include "conquer.h" #include "data.h" #include "dbgprint.h" +#include "deploymentconfig.h" #include "gamedirs.h" // for Search_Files. #include "globals.h" #include "hostclock.h" @@ -641,7 +643,7 @@ bool SessionClass::Log_To_File(FILE *out) *=========================================================================*/ void SessionClass::Write_MultiPlayer_Settings(void) { - CDFileClass file(CONFIG_FILE_NAME); + CDFileClass file(DeploymentConfig.SettingsFile.c_str()); { // Save the player's last-used Handle & Color ConfigINI.Put_Int("MultiPlayer", "Color", (int)PrefColor); diff --git a/code/shapeset.h b/code/shapeset.h index 2e58df3b0..2885a5833 100644 --- a/code/shapeset.h +++ b/code/shapeset.h @@ -169,10 +169,10 @@ class ShapeSet void Flag_RLE_Compressed(void) {Flags |= SFLAG_RLE;} void Set_Size(short size) {Size = size;} }; + static_assert(sizeof(ShapeRecord) == 24, "a SHP frame record is 24 bytes on disk"); // A shape file is cast straight onto this class, so the frame records that follow the // header keep their file widths and offsets. - static_assert(sizeof(ShapeRecord) == 24, "Shape frame record layout changed"); static_assert(offsetof(ShapeRecord, Width) == 4, "Shape frame record layout changed"); static_assert(offsetof(ShapeRecord, Color) == 12, "Shape frame record layout changed"); static_assert(offsetof(ShapeRecord, Data) == 20, "Shape frame record layout changed"); @@ -194,11 +194,9 @@ class ShapeSet ShapeSet(ShapeSet const & rvalue); ShapeSet const & operator = (ShapeSet const & rvalue); }; +static_assert(sizeof(ShapeSet) == 8, "the SHP header is 8 bytes on disk"); #pragma pack(pop) -// A shape file is cast straight onto this header, so its four fields keep their file widths. -static_assert(sizeof(ShapeSet) == 8, "Shape file header layout changed"); - /*********************************************************************************************** * ShapeSet::Get_Data -- Fetches pointer to raw shape data. * diff --git a/code/srfcache.cpp b/code/srfcache.cpp index 6c88236bd..b67767f60 100644 --- a/code/srfcache.cpp +++ b/code/srfcache.cpp @@ -36,6 +36,7 @@ struct MSBitmap BITMAPFILEHEADER filehead; BITMAPINFO info; }; +static_assert(sizeof(MSBitmap) == 58, "the file header, info header and one colour occupy 58 bytes on disk"); #pragma pack(pop) static_assert(sizeof(BITMAPFILEHEADER) == 14, "Bitmap file header layout changed"); diff --git a/code/startup.cpp b/code/startup.cpp index dcfbea5bb..e7a3502f0 100644 --- a/code/startup.cpp +++ b/code/startup.cpp @@ -508,7 +508,7 @@ int CALLBACK WinMain ( HINSTANCE instance , HINSTANCE , char * , int command_sho // anywhere for a player's files to go. Naming it again settles it where it belongs. Session.RecordFile.Set_Name("RECORD.BIN"); - CDFileClass *cfile = new CDFileClass(CONFIG_FILE_NAME); + CDFileClass *cfile = new CDFileClass(DeploymentConfig.SettingsFile.c_str()); ConfigINI.Load(*cfile, false); Options.ScreenWidth = ConfigINI.Get_Int("Video", "ScreenWidth", Options.ScreenWidth); diff --git a/code/sun.h b/code/sun.h index d5b7885df..f0f06b943 100644 --- a/code/sun.h +++ b/code/sun.h @@ -64,7 +64,6 @@ ** Filenames of the data files it can create at run time. */ #define FAME_FILE_NAME "HALLFAME.DAT" -#define CONFIG_FILE_NAME "SUN.INI" /********************************************************************** diff --git a/code/swizzle.cpp b/code/swizzle.cpp index 3ac2a94de..421128df5 100644 --- a/code/swizzle.cpp +++ b/code/swizzle.cpp @@ -230,15 +230,13 @@ void SwizzleManagerClass::Resolve(void) /// -/// Takes back everything registered since the mark, clearing each pointer slot it covers. -/// A slot that was registered still holds the identity read from the file, which is not -/// an address; clearing it lets the object it belongs to be destroyed safely. +/// Takes back everything registered since the mark. +/// The slots those requests name were left null by Swizzle and nothing has filled them, +/// since Resolve does not run until the load has succeeded, so dropping the requests is +/// all it takes to let the objects holding them be destroyed. /// void SwizzleManagerClass::Abandon(MarkType const & mark) { - for (std::size_t index = mark.Requests; index < RequestTable.size(); index++) { - *(void **)RequestTable[index].Pointer = NULL; - } RequestTable.resize(mark.Requests); PointerTable.resize(mark.Pointers); } diff --git a/code/swizzle.h b/code/swizzle.h index 293f31431..6c536fb8b 100644 --- a/code/swizzle.h +++ b/code/swizzle.h @@ -88,7 +88,6 @@ class SwizzleManagerClass }; MarkType Mark(void) const {return(MarkType{RequestTable.size(), PointerTable.size()});} void Abandon(MarkType const & mark); - void Abandon(void) {Abandon(MarkType{0, 0});} private: /* diff --git a/code/taction.cpp b/code/taction.cpp index fa9b68a7d..598eb788c 100644 --- a/code/taction.cpp +++ b/code/taction.cpp @@ -40,7 +40,6 @@ * ActionChoiceClass::Draw_It -- Display the action choice as part of a list box. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ -#include "hostclock.h" #include "always.h" #include "taction.h" @@ -62,6 +61,7 @@ #include "empulse.h" #include "foot.h" #include "globals.h" +#include "hostclock.h" #include "house.h" #include "houstype.h" #include "incdec.h" diff --git a/code/techtype.cpp b/code/techtype.cpp index 27ae39bfe..2c018778a 100644 --- a/code/techtype.cpp +++ b/code/techtype.cpp @@ -24,10 +24,10 @@ #include "bullet.h" #include "bullettype.h" #include "cell.h" +#include "classids.h" #include "combat.h" #include "findmake.h" #include "globals.h" -#include "classids.h" #include "infatype.h" #include "mixfile.h" #include "psystype.h" diff --git a/code/tiberium.cpp b/code/tiberium.cpp index 3b01431c8..59de5b97f 100644 --- a/code/tiberium.cpp +++ b/code/tiberium.cpp @@ -28,6 +28,7 @@ #include "tracker.h" #include +#include #define MAX_SPREAD_DELAY 50 #define MAX_GROWTH_DELAY 50 @@ -60,12 +61,10 @@ TiberiumClass::TiberiumClass(char const * ininame) : SpreadCount(0), SpreadQueue(NULL), SpreadState(NULL), - SpreadNodes(NULL), SpreadTimer(), GrowthCount(0), GrowthQueue(NULL), GrowthState(NULL), - GrowthNodes(NULL), GrowthTimer() { HeapID = (TiberiumType)Tiberiums.Count(); @@ -238,15 +237,13 @@ void TiberiumClass::Serialize(SaveStreamClass & stream) stream.Serialize(Variety); stream.Serialize(RampVariety); stream.Serialize(SpreadCount); - // SpreadQueue -- pools sized to the map rather than saved state; Load drops them and the + // SpreadQueue -- sized to the map rather than to saved state; Load drops these and the // tiberium systems build them again from the map itself. // SpreadState - // SpreadNodes stream.Serialize(SpreadTimer); stream.Serialize(GrowthCount); - // GrowthQueue -- the growth pools, dropped and rebuilt the same way. + // GrowthQueue -- the growth records, dropped and rebuilt the same way. // GrowthState - // GrowthNodes stream.Serialize(GrowthTimer); } @@ -323,30 +320,44 @@ void TiberiumClass::Deinit_Tiberium_Spread_System(void) /// void TiberiumClass::Spread_AI(void) { - if (SpreadQueue && SpreadQueue->Count() && SpreadPercentage > 0.00001) { + if (SpreadQueue.Count() && SpreadPercentage > 0.00001) { /* * The amount we spread depends on how many spreads are enqueued. * Randomize it so that it feels more natural. */ - int count = std::min(25, std::max(5, (int)(SpreadQueue->Count() * SpreadPercentage))); + int count = std::min(25, std::max(5, (int)(SpreadQueue.Count() * SpreadPercentage))); count = (abs(Scen->RandomNumber()) % count) + 1; /* * SpreadQueue does not recycle its entries. * When space runs low, we need to clear and recalculate it. */ - if (SpreadQueue->Count() > Map_Cell_Count() - 20) { + if (SpreadQueue.Count() > Map_Cell_Count() - 20) { Recalc_Spread(); } int index = 0; - CellNode * node = SpreadQueue->Extract_Min(); + std::optional node = SpreadQueue.Extract_Min(); - while (index < count && node != NULL) { + while (index < count && node) { CellClass * cellptr = &Map[node->Element]; int possible_spreads = 0; + /* + * A cell that has since lost its tiberium seeds nothing, and re-enqueuing it at + * score zero would bring it back ahead of every other entry on every later pass. + * Drop it instead, without charging it against this pass. + */ + if (!cellptr->Can_Tiberium_Spread()) { + SpreadState[Map_Cell_Index(cellptr->CellID)] = false; + + if (index < count) { + node = SpreadQueue.Extract_Min(); + } + continue; + } + /* * Count how many neighbors we can spread Tiberium to. */ @@ -365,9 +376,8 @@ void TiberiumClass::Spread_AI(void) * If there's more than one possibility, then re-enqueue this cell to spread again later. */ if (possible_spreads > 1) { - SpreadNodes[SpreadCount].Element = cellptr->CellID; - SpreadNodes[SpreadCount].Score = 0; - SpreadQueue->Insert(SpreadNodes[SpreadCount++]); + SpreadQueue.Insert(CellNode(cellptr->CellID, 0.0f)); + SpreadCount++; SpreadState[Map_Cell_Index(cellptr->CellID)] = true; } } else { @@ -375,7 +385,7 @@ void TiberiumClass::Spread_AI(void) } if (index < count) { - node = SpreadQueue->Extract_Min(); + node = SpreadQueue.Extract_Min(); } } } @@ -391,9 +401,8 @@ void TiberiumClass::Init_Spread(void) { Clear_Spread(); - SpreadNodes = new CellNode[Map_Cell_Count()]; SpreadState = new bool [Map_Cell_Count()]; - SpreadQueue = new PriorityQueueClass(Map_Cell_Count()); + SpreadQueue.Reserve(Map_Cell_Count()); Recalc_Spread(); } @@ -407,7 +416,7 @@ void TiberiumClass::Init_Spread(void) void TiberiumClass::Recalc_Spread(void) { SpreadCount = 0; - SpreadQueue->Clear(); + SpreadQueue.Clear(); for (int i = Map_Cell_Count() - 1; i >= 0; i--) { SpreadState[i] = false; @@ -418,9 +427,8 @@ void TiberiumClass::Recalc_Spread(void) while (iter) { if (iter->Tiberium_Type_Here() == HeapID && iter->Can_Tiberium_Spread()) { - SpreadNodes[SpreadCount].Element = iter->CellID; - SpreadNodes[SpreadCount].Score = 0.0; - SpreadQueue->Insert(SpreadNodes[SpreadCount++]); + SpreadQueue.Insert(CellNode(iter->CellID, 0.0f)); + SpreadCount++; SpreadState[Map_Cell_Index(iter->CellID)] = true; } @@ -436,16 +444,7 @@ void TiberiumClass::Recalc_Spread(void) /// void TiberiumClass::Clear_Spread(void) { - if (SpreadQueue) { - SpreadQueue->Clear(); - delete SpreadQueue; - SpreadQueue = NULL; - } - - if (SpreadNodes) { - delete [] SpreadNodes; - SpreadNodes = NULL; - } + SpreadQueue.Clear(); if (SpreadState) { delete [] SpreadState; @@ -488,9 +487,8 @@ void TiberiumClass::Queue_Spread(Cell const & cell) Recalc_Spread(); } - SpreadNodes[SpreadCount].Element = cell; - SpreadNodes[SpreadCount].Score = float(Frame + abs(Scen->RandomNumber()) % MAX_SPREAD_DELAY); - SpreadQueue->Insert(SpreadNodes[SpreadCount++]); + SpreadQueue.Insert(CellNode(cell, float(Frame + abs(Scen->RandomNumber()) % MAX_SPREAD_DELAY))); + SpreadCount++; SpreadState[Map_Cell_Index(cell)] = true; } } @@ -549,27 +547,27 @@ void TiberiumClass::Deinit_Tiberium_Growth_System(void) /// void TiberiumClass::Growth_AI(void) { - if (GrowthQueue && GrowthQueue->Count() && GrowthPercentage > 0.00001) { + if (GrowthQueue.Count() && GrowthPercentage > 0.00001) { /* * The amount we grow depends on how many growths are enqueued. * Randomize it so that it feels more natural. */ - int count = std::min(50, std::max(5, (int)(GrowthQueue->Count() * GrowthPercentage))); + int count = std::min(50, std::max(5, (int)(GrowthQueue.Count() * GrowthPercentage))); count = (abs(Scen->RandomNumber()) % count) + 1; /* * GrowthQueue does not recycle its entries. * When space runs low, we need to clear and recalculate it. */ - if (GrowthQueue->Count() > Map_Cell_Count() - 2 * count) { + if (GrowthQueue.Count() > Map_Cell_Count() - 2 * count) { Recalc_Growth(); } int index = 0; - CellNode * node = GrowthQueue->Extract_Min(); + std::optional node = GrowthQueue.Extract_Min(); - while (index < count && node != NULL) { + while (index < count && node) { CellClass * cellptr = &Map[node->Element]; if (cellptr->Tiberium_Type_Here() == HeapID) { @@ -580,19 +578,21 @@ void TiberiumClass::Growth_AI(void) * Also, take this opportunity to queue this cell to spread, if possible. */ if (cellptr->OverlayData < MAX_GROWTH_STAGE) { - GrowthNodes[GrowthCount].Element = node->Element; - GrowthNodes[GrowthCount].Score = float(Frame + abs(Scen->RandomNumber() % MAX_GROWTH_DELAY)); + float score = float(Frame + abs(Scen->RandomNumber() % MAX_GROWTH_DELAY)); GrowthState[Map_Cell_Index(node->Element)] = true; - GrowthQueue->Insert(GrowthNodes[GrowthCount++]); + GrowthQueue.Insert(CellNode(node->Element, score)); + GrowthCount++; Queue_Spread(node->Element); } else { GrowthState[Map_Cell_Index(node->Element)] = false; } + } else { + GrowthState[Map_Cell_Index(node->Element)] = false; } index++; if (index < count) { - node = GrowthQueue->Extract_Min(); + node = GrowthQueue.Extract_Min(); } } } @@ -608,9 +608,8 @@ void TiberiumClass::Init_Growth(void) { Clear_Growth(); - GrowthNodes = new CellNode[Map_Cell_Count()]; GrowthState = new bool [Map_Cell_Count()]; - GrowthQueue = new PriorityQueueClass(Map_Cell_Count()); + GrowthQueue.Reserve(Map_Cell_Count()); Recalc_Growth(); } @@ -624,7 +623,7 @@ void TiberiumClass::Init_Growth(void) void TiberiumClass::Recalc_Growth(void) { GrowthCount = 0; - GrowthQueue->Clear(); + GrowthQueue.Clear(); for (int i = Map_Cell_Count() - 1; i >= 0; i--) { GrowthState[i] = false; @@ -634,9 +633,8 @@ void TiberiumClass::Recalc_Growth(void) CellClass * iter = Map.Iterate(); while (iter) { if (iter->Tiberium_Type_Here() == HeapID && iter->Can_Tiberium_Grow()) { - GrowthNodes[GrowthCount].Element = iter->CellID; - GrowthNodes[GrowthCount].Score = 0.0; - GrowthQueue->Insert(GrowthNodes[GrowthCount++]); + GrowthQueue.Insert(CellNode(iter->CellID, 0.0f)); + GrowthCount++; GrowthState[Map_Cell_Index(iter->CellID)] = true; } iter = Map.Iterate(); @@ -651,16 +649,7 @@ void TiberiumClass::Recalc_Growth(void) /// void TiberiumClass::Clear_Growth(void) { - if (GrowthQueue) { - GrowthQueue->Clear(); - delete GrowthQueue; - GrowthQueue = NULL; - } - - if (GrowthNodes) { - delete [] GrowthNodes; - GrowthNodes = NULL; - } + GrowthQueue.Clear(); if (GrowthState) { delete [] GrowthState; @@ -679,7 +668,7 @@ void TiberiumClass::Clear_Growth(void) void TiberiumClass::Queue_Growth(Cell const & cell) { int cellindex = Map_Cell_Index(cell); - if (Map[cell].OverlayData < MAX_GROWTH_STAGE) { + if (Map[cell].OverlayData < MAX_GROWTH_STAGE && !GrowthState[cellindex]) { /* * GrowthQueue does not recycle its entries. @@ -689,9 +678,8 @@ void TiberiumClass::Queue_Growth(Cell const & cell) Recalc_Growth(); } - GrowthNodes[GrowthCount].Element = cell; - GrowthNodes[GrowthCount].Score = float(Frame + abs(Scen->RandomNumber()) % MAX_GROWTH_DELAY); - GrowthQueue->Insert(GrowthNodes[GrowthCount++]); + GrowthQueue.Insert(CellNode(cell, float(Frame + abs(Scen->RandomNumber()) % MAX_GROWTH_DELAY))); + GrowthCount++; GrowthState[cellindex] = true; } } diff --git a/code/tiberium.h b/code/tiberium.h index 7e02a6f8d..c7f7e8888 100644 --- a/code/tiberium.h +++ b/code/tiberium.h @@ -156,8 +156,9 @@ class TiberiumClass : public AbstractTypeClass int RampVariety; /* - * This is the number of records handed out of the SpreadNodes pool so far. The pool is - * never recycled, so the queue is rebuilt from the map once the pool runs low. + * This is the number of cells enqueued to seed since the queue was last rebuilt. + * Nothing takes a stale or duplicate entry back out, so the queue is rebuilt from the + * map once this approaches the map's cell count. */ int SpreadCount; @@ -165,7 +166,7 @@ class TiberiumClass : public AbstractTypeClass * This is the queue of cells waiting to seed their neighbors, ordered by the game * frame at which each becomes due. */ - PriorityQueueClass * SpreadQueue; + PriorityQueueClass SpreadQueue; /* * This is one flag per map cell, true while the cell is sitting in the SpreadQueue. It @@ -173,20 +174,15 @@ class TiberiumClass : public AbstractTypeClass */ bool * SpreadState; - /* - * This is the pool of queue records the SpreadQueue is built out of -- one per map - * cell, handed out in order by the SpreadCount cursor. - */ - CellNode * SpreadNodes; - /* * This counts down the frames remaining until this tiberium's next spread pass. */ CDTimerClass SpreadTimer; /* - * This is the number of records handed out of the GrowthNodes pool so far. The pool is - * never recycled, so the queue is rebuilt from the map once the pool runs low. + * This is the number of cells enqueued to grow since the queue was last rebuilt. + * Nothing takes a stale or duplicate entry back out, so the queue is rebuilt from the + * map once this approaches the map's cell count. */ int GrowthCount; @@ -194,7 +190,7 @@ class TiberiumClass : public AbstractTypeClass * This is the queue of cells waiting to ripen, ordered by the game frame at which each * becomes due. */ - PriorityQueueClass * GrowthQueue; + PriorityQueueClass GrowthQueue; /* * This is one flag per map cell, true while the cell is sitting in the GrowthQueue. It @@ -202,12 +198,6 @@ class TiberiumClass : public AbstractTypeClass */ bool * GrowthState; - /* - * This is the pool of queue records the GrowthQueue is built out of -- one per map - * cell, handed out in order by the GrowthCount cursor. - */ - CellNode * GrowthNodes; - /* * This counts down the frames remaining until this tiberium's next growth pass. */ diff --git a/code/unit.cpp b/code/unit.cpp index 1012a1309..9673d37de 100644 --- a/code/unit.cpp +++ b/code/unit.cpp @@ -120,13 +120,13 @@ #include "bullettype.h" #include "ccrand.h" #include "cell.h" +#include "classids.h" #include "combat.h" #include "conquer.h" #include "draw.h" #include "fog.h" #include "house.h" #include "houstype.h" -#include "classids.h" #include "incdec.h" #include "infantry.h" #include "infatype.h" @@ -2107,7 +2107,7 @@ void UnitClass::Per_Cell_Process(PCPType why) Cell whom_center = whom->Center_Coord(); if (Center_Coord().As_Cell() == whom->Center_Coord().As_Cell() && whom->RTTI == RTTI_BUILDING) { ClassID const clsid = Locomotion_Class_ID(Locomotion.get()); - if (clsid == ClassID_HoverLocomotion && static_cast(whom)->Class->IsCanUnitRepair && NavCom == NULL) { + if (clsid == ClassID_HoverLocomotion && static_cast(whom)->Class->IsCanUnitRepair && NavCom == nullptr) { NavCom = whom; } if (whom == NavCom) { @@ -4492,8 +4492,7 @@ FacingType UnitClass::Desired_Load_Dir(ObjectClass * passenger, Cell & moveto) c FacingType face = FACING_N; FacingType faceto; if (passenger != NULL) { - DirType direction = direction.Direction(Center_Coord(), passenger->Center_Coord()); - faceto = (FacingType)direction.As_Dir256(); + faceto = (FacingType)Direction(passenger).As_Dir256(); } else { faceto = (FacingType)(PrimaryFacing.Current().Right_180()).As_Dir256(); } @@ -5371,7 +5370,7 @@ void UnitClass::Assign_Destination(AbstractClass * target, bool immediate) walk->Link_To_Object(this); piggy = Piggyback_Of(walk.get()); if (piggy != NULL) { - piggy->Begin_Piggyback(std::move(Locomotion)); + piggy->Begin_Piggyback(Locomotion); Locomotion = std::move(walk); Locomotion->Force_New_Slope(Map[Get_Coord()].Ramp); } diff --git a/code/vein.cpp b/code/vein.cpp index 678663606..5db7f3fd4 100644 --- a/code/vein.cpp +++ b/code/vein.cpp @@ -49,6 +49,7 @@ #include "ramp.hh" #include +#include DynamicVectorClass VeinholeMonsterClass::VeinholeMonsters; @@ -64,8 +65,6 @@ static bool * GlobalGrowthState = NULL; VeinholeMonsterClass::VeinholeMonsterClass(void) : BASECLASS(), GrowthCount(0), - GrowthQueue(NULL), - GrowthNodes(NULL), GrowthTimer(0), GrowthState(NULL), CurrentState(IDLE), @@ -79,9 +78,8 @@ VeinholeMonsterClass::VeinholeMonsterClass(void) : VeinCount(0) { VeinholeMonsters.Add(this); - GrowthNodes = new CellNode[Rule->MaxVeinholeGrowth]; - GrowthQueue = new PriorityQueueClass(Rule->MaxVeinholeGrowth); - GrowthQueue->Clear(); + GrowthQueue.Clear(); + GrowthQueue.Reserve(Rule->MaxVeinholeGrowth); if (GlobalGrowthState == NULL) { GlobalGrowthState = new bool [Map_Cell_Count()]; } @@ -99,8 +97,6 @@ VeinholeMonsterClass::VeinholeMonsterClass(void) : VeinholeMonsterClass::VeinholeMonsterClass(Cell const & cell) : BASECLASS(), GrowthCount(0), - GrowthQueue(NULL), - GrowthNodes(NULL), GrowthTimer(Rule->VeinholeGrowthRate), GrowthState(NULL), CurrentState(IDLE), @@ -144,9 +140,8 @@ VeinholeMonsterClass::VeinholeMonsterClass(Cell const & cell) : Control.Set_Step(0); Control.Set_Stage(-1); - GrowthNodes = new CellNode[Rule->MaxVeinholeGrowth]; - GrowthQueue = new PriorityQueueClass(Rule->MaxVeinholeGrowth); - GrowthQueue->Clear(); + GrowthQueue.Clear(); + GrowthQueue.Reserve(Rule->MaxVeinholeGrowth); if (GlobalGrowthState == NULL) { GlobalGrowthState = new bool[Map_Cell_Count()]; @@ -525,16 +520,14 @@ void VeinholeMonsterClass::Grow(void) { static const int _mod = 5; - if (GrowthQueue && GrowthCount <= Rule->MaxVeinholeGrowth - 40 && VeinCount <= Rule->MaxVeinholeGrowth - 100 && Scen->IsVeinGrowth) { - - double amount = GrowthQueue->Count(); + if (GrowthCount <= Rule->MaxVeinholeGrowth - 40 && VeinCount <= Rule->MaxVeinholeGrowth - 100 && Scen->IsVeinGrowth) { int index = 0; int count = (abs(Scen->RandomNumber) % _mod) + 1; - CellNode * node = GrowthQueue->Extract_Min(); + std::optional node = GrowthQueue.Extract_Min(); - while (index < count && node != NULL) { + while (index < count && node) { CellClass & cell = Map[node->Element]; if (cell.OverlayData < OVERLAYDATA_FIRST_SOLID_VEIN) { @@ -559,10 +552,10 @@ void VeinholeMonsterClass::Grow(void) int cindex = Map_Cell_Index(adjacent); if (cindex >= 0 && cindex < Map_Cell_Count()) { if (adj_cell.Can_Place_Veins() && !GlobalGrowthState[cindex] && GrowthCount < Rule->MaxVeinholeGrowth) { - GrowthNodes[GrowthCount].Element = adjacent; - GrowthNodes[GrowthCount].Score = float(Frame / 50 + abs(Scen->RandomNumber() % 50) + 1); + float score = float(Frame / 50 + abs(Scen->RandomNumber() % 50) + 1); GlobalGrowthState[cindex] = true; - GrowthQueue->Insert(GrowthNodes[GrowthCount++]); + GrowthQueue.Insert(CellNode(adjacent, score)); + GrowthCount++; } GrowthState[cindex] = true; } @@ -573,7 +566,7 @@ void VeinholeMonsterClass::Grow(void) index++; if (index < count) { - node = GrowthQueue->Extract_Min(); + node = GrowthQueue.Extract_Min(); } } } @@ -588,17 +581,15 @@ void VeinholeMonsterClass::Shrink(void) { static const int _mod = 4; - if (GrowthQueue != NULL) { - int index = 0; - int count = abs(Scen->RandomNumber) % _mod + 1; - CellNode * node = GrowthQueue->Extract_Min(); + int index = 0; + int count = abs(Scen->RandomNumber) % _mod + 1; + std::optional node = GrowthQueue.Extract_Min(); - while (index < count && node != NULL) { - Reduce_Veins_At(&Map[Map[node->Element].CellID]); - index++; - if (index < count) { - node = GrowthQueue->Extract_Min(); - } + while (index < count && node) { + Reduce_Veins_At(&Map[Map[node->Element].CellID]); + index++; + if (index < count) { + node = GrowthQueue.Extract_Min(); } } } @@ -620,11 +611,10 @@ void VeinholeMonsterClass::Init_Vein_Growth_System(bool clear) Deinit_Vein_Growth_System(); for (i = VeinholeMonsters.Count() - 1; i >= 0; i--) { - VeinholeMonsters[i]->GrowthNodes = new CellNode[Rule->MaxVeinholeGrowth]; - VeinholeMonsters[i]->GrowthQueue = new PriorityQueueClass(Rule->MaxVeinholeGrowth); + VeinholeMonsters[i]->GrowthQueue.Reserve(Rule->MaxVeinholeGrowth); VeinholeMonsters[i]->GrowthState = new bool[cell_count]; memset(VeinholeMonsters[i]->GrowthState, 0, cell_count); - VeinholeMonsters[i]->GrowthQueue->Clear(); + VeinholeMonsters[i]->GrowthQueue.Clear(); } } @@ -686,7 +676,7 @@ void VeinholeMonsterClass::Build_Growth_Queue(void) GrowthCount = 0; VeinCount = 0; - GrowthQueue->Clear(); + GrowthQueue.Clear(); for (int i = Map_Cell_Count() - 1; i >= 0; i--) { GrowthState[i] = false; @@ -706,9 +696,8 @@ void VeinholeMonsterClass::Build_Growth_Queue(void) VeinCount++; } else { if (GrowthCount < Rule->MaxVeinholeGrowth) { - GrowthNodes[GrowthCount].Element = cellptr->CellID; - GrowthNodes[GrowthCount].Score = 0; - GrowthQueue->Insert(GrowthNodes[GrowthCount++]); + GrowthQueue.Insert(CellNode(cellptr->CellID, 0.0f)); + GrowthCount++; } } } @@ -730,9 +719,8 @@ void VeinholeMonsterClass::Build_Growth_Queue(void) VeinCount++; } else { if (GrowthCount < Rule->MaxVeinholeGrowth) { - GrowthNodes[GrowthCount].Element = adjacent->CellID; - GrowthNodes[GrowthCount].Score = 0; - GrowthQueue->Insert(GrowthNodes[GrowthCount++]); + GrowthQueue.Insert(CellNode(adjacent->CellID, 0.0f)); + GrowthCount++; if (adjacent->OverlayData >= OVERLAYDATA_FIRST_SOLID_VEIN) { cells.Add(adjacent->CellID); } @@ -760,7 +748,7 @@ void VeinholeMonsterClass::Build_Growth_Queue(void) void VeinholeMonsterClass::Build_Shrinking_Queue(void) { GrowthCount = 0; - GrowthQueue->Clear(); + GrowthQueue.Clear(); VeinCount = 0; Map.Reset_Iterator(); @@ -770,9 +758,9 @@ void VeinholeMonsterClass::Build_Shrinking_Queue(void) int index = Map_Cell_Index(iter->CellID); if (index >= 0 && index < Map_Cell_Count() && GrowthState[index]) { if (GrowthCount < Rule->MaxVeinholeGrowth) { - GrowthNodes[GrowthCount].Element = iter->CellID; - GrowthNodes[GrowthCount].Score = 1000 - Point2D(iter->CellID.X, iter->CellID.Y).Distance_To(Point2D(CellID.X, CellID.Y)); - GrowthQueue->Insert(GrowthNodes[GrowthCount++]); + float score = 1000 - Point2D(iter->CellID.X, iter->CellID.Y).Distance_To(Point2D(CellID.X, CellID.Y)); + GrowthQueue.Insert(CellNode(iter->CellID, score)); + GrowthCount++; VeinCount++; } } @@ -799,19 +787,10 @@ void VeinholeMonsterClass::Clear_Global_Data(void) /// void VeinholeMonsterClass::Clear_Growth(void) { - if (GrowthQueue) { - GrowthQueue->Clear(); - delete GrowthQueue; - GrowthQueue = NULL; - } - - if (GrowthNodes) { - delete GrowthNodes; - GrowthNodes = NULL; - } + GrowthQueue.Clear(); if (GrowthState) { - delete GrowthState; + delete [] GrowthState; GrowthState = NULL; } @@ -866,7 +845,7 @@ void VeinholeMonsterClass::Destroy_Monster(void) void VeinholeMonsterClass::Remove_Dead(void) { for (int i = VeinholeMonsters.Count() - 1; i >= 0; i--) { - if (VeinholeMonsters[i]->IsDead && VeinholeMonsters[i]->GrowthQueue->Count() == 0) { + if (VeinholeMonsters[i]->IsDead && VeinholeMonsters[i]->GrowthQueue.Count() == 0) { delete VeinholeMonsters[i]; } } @@ -906,7 +885,7 @@ bool VeinholeMonsterClass::Load_All(SaveStreamClass & stream) */ VeinholeMonsterClass * monster = new VeinholeMonsterClass(); - SwizzleIDType id = 0; + SwizzleIDType id; stream.Serialize(id); if (stream.Was_Error()) { return(false); @@ -925,16 +904,6 @@ bool VeinholeMonsterClass::Load_All(SaveStreamClass & stream) return(false); } - stream.Serialize_Bytes(monster->GrowthNodes, (int)(sizeof(CellNode) * Rule->MaxVeinholeGrowth)); - if (stream.Was_Error()) { - return(false); - } - - monster->GrowthQueue->Serialize(stream, monster->GrowthNodes); - if (stream.Was_Error()) { - return(false); - } - TargetTracker.Add_Index(monster->Fetch_ID(), monster); } @@ -951,12 +920,9 @@ void VeinholeMonsterClass::Serialize(SaveStreamClass & stream) BASECLASS::Serialize(stream); stream.Serialize(GrowthCount); - // GrowthQueue -- pools sized to the map and the growth limit in the rules. The monster - // allocates them for itself, and Load_All and Save_All carry their contents alongside this - // record. - // GrowthNodes + GrowthQueue.Serialize(stream); stream.Serialize(GrowthTimer); - // GrowthState -- part of the same set of pools. + // GrowthState -- one flag per map cell, carried beside this record by Load_All and Save_All. stream.Serialize(CurrentState); stream.Serialize(DesiredState); stream.Serialize(Control); @@ -1006,15 +972,6 @@ bool VeinholeMonsterClass::Save_All(SaveStreamClass & stream) return(false); } - stream.Serialize_Bytes(VeinholeMonsters[i]->GrowthNodes, (int)(sizeof(CellNode) * Rule->MaxVeinholeGrowth)); - if (stream.Was_Error()) { - return(false); - } - - VeinholeMonsters[i]->GrowthQueue->Serialize(stream, VeinholeMonsters[i]->GrowthNodes); - if (stream.Was_Error()) { - return(false); - } } return(true); @@ -1062,7 +1019,7 @@ void VeinholeMonsterClass::Reduce_Veins_At(CellClass * cellptr) } if (facingbools[facing / 2] == true && !IsDead) { - GrowthQueue->Remove_Matching(CellNode(adjacent->CellID)); + GrowthQueue.Remove_Matching(CellNode(adjacent->CellID)); } } } @@ -1076,9 +1033,9 @@ void VeinholeMonsterClass::Reduce_Veins_At(CellClass * cellptr) GlobalGrowthState[cindex] = false; } else if (!IsDead) { if (GrowthCount < Rule->MaxVeinholeGrowth) { - GrowthNodes[GrowthCount].Element = cell; - GrowthNodes[GrowthCount].Score = float(Frame / 50 + abs(Scen->RandomNumber() % 50) + 1); - GrowthQueue->Insert(GrowthNodes[GrowthCount++]); + float score = float(Frame / 50 + abs(Scen->RandomNumber() % 50) + 1); + GrowthQueue.Insert(CellNode(cell, score)); + GrowthCount++; GrowthState[cindex] = true; GlobalGrowthState[cindex] = true; } diff --git a/code/vein.h b/code/vein.h index 85c456eed..dba7355a7 100644 --- a/code/vein.h +++ b/code/vein.h @@ -74,9 +74,9 @@ class VeinholeMonsterClass : public ObjectClass public: /* - * This is the number of nodes handed out of the GrowthNodes pool. It never falls - * until the queue is rebuilt, so it also serves as the check that stops a monster - * spreading past the vein limit in the rules. + * This is the number of cells enqueued since the queue was last rebuilt. It never + * falls until then, so it is also the check that stops a monster spreading past the + * vein limit in the rules. */ int GrowthCount; @@ -86,14 +86,7 @@ class VeinholeMonsterClass : public ObjectClass * with the veins it already owns, farthest out first, so the patch withers back from * its edges. */ - PriorityQueueClass * GrowthQueue; - - /* - * This is the block of nodes the GrowthQueue's entries live in, sized to the vein - * limit in the rules. Holding them in one array is what lets the queue be saved as - * indices into it. - */ - CellNode * GrowthNodes; + PriorityQueueClass GrowthQueue; /* * This is the countdown to the next growth step, restarted with a jittered copy of diff --git a/code/voxel.h b/code/voxel.h index 08395d6e1..246509aa1 100644 --- a/code/voxel.h +++ b/code/voxel.h @@ -35,6 +35,7 @@ struct VPLHeaderStruct /// Unused int Unused; }; +static_assert(sizeof(VPLHeaderStruct) == 16, "the VPL header is 16 bytes on disk"); Matrix3D Get_Isometric_View_Matrix(void); void Init_Voxel_Matrices(void); diff --git a/code/voxel.hh b/code/voxel.hh index 3ccc745dd..b7f0cc817 100644 --- a/code/voxel.hh +++ b/code/voxel.hh @@ -82,6 +82,7 @@ struct VoxelHeaderStruct */ int DataSize; }; +static_assert(sizeof(VoxelHeaderStruct) == 32, "the VXL header before the remap bytes and palette is 32 bytes on disk"); /* @@ -105,6 +106,7 @@ struct VoxelLayerHeaderStruct int Unused1; unsigned char Unused2; }; +static_assert(sizeof(VoxelLayerHeaderStruct) == 28, "the VXL section header is 28 bytes on disk; the last byte is padded out to a word"); /* @@ -164,6 +166,7 @@ struct VoxelLayerInfoStruct */ unsigned char NormalType; }; +static_assert(sizeof(VoxelLayerInfoStruct) == 92, "the VXL section tailer is 92 bytes on disk"); /* @@ -189,3 +192,4 @@ struct VoxelAnimFileHeaderStruct */ int LayerCount; }; +static_assert(sizeof(VoxelAnimFileHeaderStruct) == 24, "the HVA header is 24 bytes on disk"); diff --git a/code/vqalib/loader.cpp b/code/vqalib/loader.cpp index 8bc8ac659..c4f359793 100644 --- a/code/vqalib/loader.cpp +++ b/code/vqalib/loader.cpp @@ -308,9 +308,9 @@ struct VQASN2J { std::int16_t index2; std::int32_t predicted2; }; +static_assert(sizeof(VQASN2J) == 12, "the SN2J chunk is 12 bytes on disk"); #pragma pack(pop) -static_assert(sizeof(VQASN2J) == 12, "SN2J chunk layout changed"); static_assert(offsetof(VQASN2J, predicted2) == 8, "SN2J chunk layout changed"); @@ -3535,10 +3535,9 @@ long Load_SN2J(VQAHandleP *vqap, unsigned long iffsize) unsigned short wIndex2; unsigned int dwPredicted2; } data; + static_assert(sizeof(SNJ2Struct) == 12, "the SN2J chunk is 12 bytes on disk"); #pragma pack(pop) - static_assert(sizeof(data) == 12, "SN2J chunk layout changed"); - #if(VQAVOC_ON && VQAAUDIO_ON) if (((config->OptionFlags & VQAOPTF_AUDIO) == 0) || (vqap->vocfh != -1) || (audio->Buffer == NULL)) { diff --git a/code/vqalib/vqafile.h b/code/vqalib/vqafile.h index 7b1a8927f..c041d8fac 100644 --- a/code/vqalib/vqafile.h +++ b/code/vqalib/vqafile.h @@ -112,10 +112,10 @@ typedef struct _VQAHeader { */ std::uint32_t AudioPreload; } VQAHeader; +static_assert(sizeof(VQAHeader) == 42, "the VQHD chunk is 42 bytes on disk"); // The VQHD chunk is 42 bytes on disk. MaxCBSize and AudioPreload were written as a 32-bit // long by the original 32-bit build, so they stay 32 bits wide here. -static_assert(sizeof(VQAHeader) == 42, "VQHD chunk layout changed"); static_assert(offsetof(VQAHeader, MaxCBSize) == 34, "VQHD chunk layout changed"); static_assert(offsetof(VQAHeader, AudioPreload) == 38, "VQHD chunk layout changed"); diff --git a/code/walk.cpp b/code/walk.cpp index 3413e895d..887e4e58a 100644 --- a/code/walk.cpp +++ b/code/walk.cpp @@ -661,9 +661,9 @@ LayerType WalkLocomotionClass::In_Which_Layer(void) /// /// The locomotor that is to take over the unit. /// bool; Was the locomotor taken on? One already carrying a locomotor refuses. -bool WalkLocomotionClass::Begin_Piggyback(std::unique_ptr carried) +bool WalkLocomotionClass::Begin_Piggyback(std::unique_ptr & carried) { - if (carried == NULL || Piggybacker != NULL) { + if (carried == nullptr || Piggybacker != nullptr) { return(false); } Piggybacker = std::move(carried); diff --git a/code/walk.h b/code/walk.h index f490df50f..267a54d66 100644 --- a/code/walk.h +++ b/code/walk.h @@ -36,10 +36,10 @@ class WalkLocomotionClass : public LocomotionClass, public IPiggyback virtual void Serialize(SaveStreamClass & stream) override; - virtual bool Begin_Piggyback(std::unique_ptr carried) override; + virtual bool Begin_Piggyback(std::unique_ptr & carried) override; virtual std::unique_ptr End_Piggyback(void) override; virtual bool Is_Ok_To_End(void) override; - virtual bool Is_Piggybacking(void) override {return(Piggybacker != NULL);} + virtual bool Is_Piggybacking(void) override {return(Piggybacker != nullptr);} virtual bool Is_Moving(void) override; virtual Coord Destination(void) override; diff --git a/code/zone.hh b/code/zone.hh index 177125280..08aecf77d 100644 --- a/code/zone.hh +++ b/code/zone.hh @@ -16,10 +16,11 @@ #include "coord.h" #include "vector.h" -#include "passblty.hh" +#include +#include +#include -template -class HashTableClass; +#include "passblty.hh" /********************************************************************** ** A base is broken up into several zones. This type enumerates the @@ -159,13 +160,17 @@ struct CellZoneStruct */ unsigned char Height; + // The alignment gap ahead of ZoneID, named and cleared because a save carries this + // struct as raw bytes. + unsigned short Padding; + /* * This is the base terrain zone the cell was flood filled into, or 0 for a cell that * lies outside the playfield. It indexes the Zones layers to give the movement zone. */ - unsigned short ZoneID; + int ZoneID; - CellZoneStruct(void) : Passability(PASSABLE_OUTSIDE), Height(0) {} + CellZoneStruct(void) : Passability(PASSABLE_OUTSIDE), Height(0), Padding(0), ZoneID(0) {} }; @@ -178,13 +183,13 @@ struct CellSubzoneStruct * This is the subzone the cell belongs to at each level of pathfinding coarseness * (SubzoneLevelType), or 0 where the cell has none at that level. */ - signed short SubzoneID[SUBZONE_COUNT]; + int SubzoneID[SUBZONE_COUNT]; /* * This is the base terrain zone of the cell, cached from CellZones on each rebuild. A * subzone never spans two zones, so the fill spreads only while this value matches. */ - signed short ZoneID; + int ZoneID; /* * This is the ground level height of the cell, cached alongside its zone. A subzone @@ -203,17 +208,13 @@ struct CellSubzoneStruct struct SubzoneConnectionStruct { SubzoneConnectionStruct(void) : SubzoneID(0), IsCrossBlock(false) {} - SubzoneConnectionStruct(int subzone_id) : SubzoneID(subzone_id), IsCrossBlock(false) {} SubzoneConnectionStruct(SubzoneConnectionStruct const &that) : SubzoneID(that.SubzoneID), IsCrossBlock(that.IsCrossBlock) {} bool operator==(const SubzoneConnectionStruct & that) const { return(SubzoneID == that.SubzoneID); } bool operator!=(const SubzoneConnectionStruct & that) const { return(SubzoneID != that.SubzoneID); } /* - * This is the subzone that the owning subzone connects to. While a connection is still - * staged in the SubzoneConnectionHashTable this holds both IDs packed together instead, as - * (neighbor << 16) | subzone, so that duplicate pairs fall out of the staging set before - * they are unpacked into the two subzones' adjacency lists. + * This is the subzone that the owning subzone connects to. */ int SubzoneID; @@ -254,7 +255,7 @@ struct SubzoneTrackingStruct * expand a subzone whose parent lay on the route the coarser level settled on, which is * what keeps a long path cheap to find. */ - unsigned short ParentSubzoneID; + int ParentSubzoneID; /* * This is the passability shared by all of this subzone's cells -- a subzone never spans @@ -272,5 +273,15 @@ struct SubzoneTrackingStruct int ThreatRegion; }; -typedef HashTableClass ZONE_PAIR_HASH_SET; -typedef HashTableClass SUBZONE_CONNECTION_HASH_SET; +/* + * A pair of ids, ordered as the site that staged it wrote them rather than smallest first, + * because the two staging containers below hold (a,b) and (b,a) as separate entries. + */ +using ZonePair = std::pair; + +using ZonePairSet = std::set; + +/* + * The subzone links a rebuild has staged, each mapped to whether it crosses a fill block. + */ +using SubzoneLinkStaging = std::map; diff --git a/docs/SAVE-FORMAT.md b/docs/SAVE-FORMAT.md index ea9ddc950..987883a50 100644 --- a/docs/SAVE-FORMAT.md +++ b/docs/SAVE-FORMAT.md @@ -57,10 +57,14 @@ The content is the game state: the bytes `Put_All` in `code/saveload.cpp` writes through `SaveStreamClass`, compressed as one block with LZO1X-1 when that makes it smaller, and stored as it is otherwise. The reader checks the stored length and checksum before decompressing, and refuses a block that does -not expand to exactly the recorded length. The decompressor checks every read -and write against its buffers, so a block forged to overrun either is refused -like a damaged one. An uncompressed length above 256 MiB is refused before -anything is allocated for it. +not expand to exactly the recorded length. An uncompressed length above 256 MiB +is refused before anything is allocated for it. + +The block is decompressed through `lzo1x_decompress_safe`, which stops at the +end of the output buffer, so a block forged to expand past the recorded length +is refused rather than written past it. The records after the header are still +read into live objects, so treat a save file from an untrusted source as +untrusted input. ### Object records @@ -73,35 +77,47 @@ names them. An object record is: | 4 | Length of the record body | | | The body: the swizzle identity, then the members the class's `Serialize` names | -The swizzle identity is the object's own address, written at the width a -pointer has in the build that wrote it. Every record therefore grows by four -bytes in a 64-bit build, and a save does not cross between builds of different -pointer widths. - The class identifier is the `ClassID` the object's `Class_ID` reports, the same one registered in `code/startup.cpp` and, for a locomotor, named by the -`Locomotor=` key. Its sixteen bytes are those of the COM class identifier the -class once registered, so a save written before COM left the engine still -names the same classes. The reader creates the object through that registration, -hands it the stream, checks that it consumed exactly the recorded length, and -only then lets it finish restoring itself, so a refused record never reaches -the map or a side table. A record that comes up short or long fails the load with the object's -type and offset in the debug log, which is what a member added to one build -and not the other looks like. A record read where a locomotor belongs fails -the load the same way when its class is not one. A vector of objects is a -4-byte count followed by that many records, and a locomotor nested inside a -unit's record is a record of its own. A count that the bytes remaining in the -content could not hold fails the load before anything is allocated for it. +`Locomotor=` key. Its sixteen bytes are those of the COM class identifier +the class once registered, kept because the `Locomotor=` values in rules +files carry them. The reader creates the object through that +registration, hands it the stream, checks that it consumed exactly the +recorded length, and only then lets it finish restoring itself, so a refused +record never reaches the map or a side table. A record that comes up short +or long fails the load with the object's type and offset in the debug log, +which is what a member added to one build and not the other looks like. A +record read where a locomotor belongs fails the load the same way when its +class is not one. A vector of objects is a 4-byte count followed by that +many records, all of the heap's own class; a record naming any other class +fails the load, since nothing else belongs in that heap. A locomotor nested +inside a unit's record is a record of its own. A count that the bytes +remaining in the content could not hold fails the load before anything is +allocated for it. An object whose record fails is destroyed before the load fails. The pointer slots it had registered are cleared first, since they still hold identities -rather than addresses. The objects loaded before it keep their places in the -heaps and have their slots cleared the same way, so a failed load leaves -nothing that a later teardown cannot delete. +rather than addresses, and the slots the records before it registered are +cleared the same way. Those earlier objects stay in their heaps, and the ones +that had finished loading have already taken their place in the map or a side +table. A failed load therefore leaves a partly built game that the caller has +to clear, not one it can carry on from. + +A character buffer travels as its text: a length and that many characters, and +a load clears the rest of the buffer. How much room a build keeps for a string +is its own business, so the file carries neither the capacity nor whatever the +memory held past the terminator. The text is at most one character shorter than +the buffer, so a loaded buffer is always terminated; a length that would fill it +outright fails the load, since the engine reads these buffers as C strings. The body is what each class's `Serialize` produces, member by member, in host byte order. It is not described here; the classes are the description. +The swizzle identity and every pointer member travel as four bytes. The save +numbers the objects it meets rather than writing the address one sat at, so +the body depends neither on the pointer width of the build that wrote it nor +on where the objects were in memory. + ## Versions Two numbers gate a save. The format version in the header says how to parse @@ -152,10 +168,11 @@ file on disk is left as it was. ## Checks -`tests/save` builds `code/savefile.cpp` against the LZO library on every target +`tests/save` builds `code/savefile.cpp` against the vendored LZO library and covers the round trip, the fields-only read, replacement of an existing file and of a stale `.tmp`, and each refusal above, including a later version, an unknown flag, a file cut at every boundary, a byte flipped in the header, the table and the content, a field table above its limit, a gap before the -content, compressed blocks forged to overrun the reader, and a write above -each limit that leaves the earlier save in place. It reads no game data. +content, a block that ends before or expands past its declared length, and a +write above each limit that leaves the earlier save in place. It reads no game +data. diff --git a/manual/changes/deployment-file-names.md b/manual/changes/deployment-file-names.md new file mode 100644 index 000000000..fc8a04bdd --- /dev/null +++ b/manual/changes/deployment-file-names.md @@ -0,0 +1,32 @@ +--- +title: Name the deployment's own files in OPENTS.INI +category: feature +release: 0.2.0 +targets: +- type: format + id: opents-ini + effect: changed +- type: format + id: sound-ini + effect: changed +- type: format + id: theme-ini + effect: changed +- type: format + id: tutorial-ini + effect: changed +- type: format + id: ui-ini + effect: changed +credit: +- ZivDero +--- + +A deployment names its own game data files in `OPENTS.INI`: rules, artwork, AI, sound, +music, campaigns, translated rules, tutorial text and interface, the expansion copy of each, +the file a player's settings are written back to, and the two palettes the game starts with. +A name it does not write keeps the one Tiberian Sun uses. The expansion rules file it names +is also what the game looks for to decide the expansion is installed. + +A palette file the game cannot find leaves that palette unchanged. It stopped the game +before. diff --git a/manual/changes/large-map-zone-ids.md b/manual/changes/large-map-zone-ids.md new file mode 100644 index 000000000..7d9955c51 --- /dev/null +++ b/manual/changes/large-map-zone-ids.md @@ -0,0 +1,17 @@ +--- +title: Carry zone and subzone ids on large maps +category: fix +release: 0.2.0 +targets: +- type: system + id: route-search + effect: changed +- type: format + id: save-games + effect: changed +credit: [ZivDero] +--- + +Terrain zone and subzone identifiers are held as full integers rather than 16-bit values, so a map large enough to produce more than 32767 subzones no longer indexes the route search's tables with a negative number. A terrain change near the bottom or right edge of the playfield also no longer clears subzone identifiers past the end of the zone tables, and a route longer than 2000 cells is now abandoned instead of overrunning the move list it is written into. + +The map record of a save game grew with those identifiers. A save written by an earlier development snapshot of this release cycle is refused when that record is read. diff --git a/manual/changes/tiberium-growth-duplicate-entries.md b/manual/changes/tiberium-growth-duplicate-entries.md new file mode 100644 index 000000000..973068085 --- /dev/null +++ b/manual/changes/tiberium-growth-duplicate-entries.md @@ -0,0 +1,16 @@ +--- +title: Queue a Tiberium cell to grow only once +category: fix +release: 0.2.0 +targets: +- type: system + id: tiberium + effect: changed +credit: +- ZivDero +--- + +A cell could be queued to grow many times over, most often after a chain reaction across a +field, and every stale copy taken off the queue spent one of the pass's slots without +growing anything. Growth crawled until the pile drained. A cell now holds at most one place +in the queue. diff --git a/manual/changes/tiberium-spread-dead-cells.md b/manual/changes/tiberium-spread-dead-cells.md new file mode 100644 index 000000000..75d173511 --- /dev/null +++ b/manual/changes/tiberium-spread-dead-cells.md @@ -0,0 +1,16 @@ +--- +title: Drop a Tiberium cell that can no longer spread +category: fix +release: 0.2.0 +targets: +- type: system + id: tiberium + effect: changed +credit: +- ZivDero +--- + +A cell whose Tiberium has gone since it was queued to spread is now dropped from the +spread queue. It was previously re-queued at the head of the queue whether or not it still +carried anything to seed from, so a handful of harvested cells could come back first on +every pass and leave that Tiberium type looking as though it had stopped spreading. diff --git a/manual/changes/utf8-text.md b/manual/changes/utf8-text.md index adaa15dff..290515f6e 100644 --- a/manual/changes/utf8-text.md +++ b/manual/changes/utf8-text.md @@ -14,4 +14,4 @@ Game text, INI files, typed input, player names and chat are UTF-8. The shipped An INI file that is not valid UTF-8 is read as Windows-1252, so existing maps and mods keep their accented names and a file whose digest was written in that code page still verifies. Saving writes UTF-8 without a byte order mark. -Player names now hold 64 bytes and chat lines 224, where an accented Latin character takes two bytes. The UTF-8 code page needs Windows 10 version 1903 or newer; older Windows keeps its own code page, where the game's text still renders. +Player names now hold 64 bytes and chat lines 224, where an accented Latin character takes two bytes. The UTF-8 code page needs Windows 10 version 1903, which is now the minimum Windows version OpenTS supports. diff --git a/manual/content/formats/opents-ini.md b/manual/content/formats/opents-ini.md index a52af0701..64ae82b3a 100644 --- a/manual/content/formats/opents-ini.md +++ b/manual/content/formats/opents-ini.md @@ -1,11 +1,12 @@ --- format_id: opents-ini title: OPENTS.INI -summary: Names the folders a deployment keeps its game files sorted into, and what its saves carry. +summary: Names the folders a deployment keeps its game files sorted into, the files it reads them from, and what its saves carry. kind: file source_files: - code/deploymentconfig.cpp - code/gamedirs.cpp + - code/init.cpp filenames: - OPENTS.INI related: @@ -30,6 +31,49 @@ Without the file, and without the key, the game behaves as though `SearchPaths=I The game's own directory is examined before any listed folder, so naming it adds nothing. Naming only it, as `SearchPaths=.`, is how a deployment asks for no other folder to be searched. An empty `SearchPaths=` does not do this: the file reader passes over an entry with nothing after the equals sign, leaving the default in force. +## The files it reads + +```ini title="OPENTS.INI" +[Files] +Rules=RULES.INI +RulesExpansion=FIRESTRM.INI +Art=ART.INI +ArtExpansion=ARTFS.INI +AI=AI.INI +AIExpansion=AIFS.INI +Sound=SOUND.INI +SoundExpansion=SOUND01.INI +Theme=THEME.INI +ThemeExpansion=THEME01.INI +Battle=BATTLE.INI +BattleExpansion=BATTLEFS.INI +LanguageRules=LANGRULE.INI +LanguageRulesExpansion=LANGFS.INI +Tutorial=TUTORIAL.INI +UI=UI.INI +Settings=SUN.INI +``` + +Each key names one file, and an unwritten key keeps the name above. `Rules` names the rules, `Art` the artwork, `AI` the computer player's data, `Sound` the [sound registry](/formats/sound-ini/), `Theme` the [music registry](/formats/theme-ini/), `Battle` the campaign list, `LanguageRules` the translated rules read over the rest, `Tutorial` the [numbered text lines](/formats/tutorial-ini/), `UI` the [interface settings](/formats/ui-ini/), and `Settings` the file a player's own options are written back to. + +The seven `Expansion` keys name the expansion's copy of a file, which is read over the base one. `RulesExpansion` also decides whether the expansion is installed: the game looks for that file and nothing else, so renaming it moves the test. + +Renaming a file does not move it. Every name here is searched for in the order the section below gives, the same as any other file the game opens. + +Rules and campaign files are also gathered by wildcard, as `RULE*.INI` and `BATTLE*.INI`, and those two patterns are fixed. A rules file named outside the pattern is read anyway and is the one the game starts from; where the search turns up others as well, the game asks which set to play with, as it does for a stock installation. Campaign files add to one another, so one named outside the pattern is read alongside those inside it. + +## The palettes it starts with + +```ini title="OPENTS.INI" +[Palettes] +Scheme=UNITSNO.PAL +Game=TEMPERAT.PAL +``` + +`Scheme` names the palette the player colors are built against, and `Game` the one the game is drawn through. Both stand only until a scenario loads its [theater](/formats/theater-control/), which replaces them with the palettes the theater's `Root=` and `Suffix=` name. They are settings of this file because nothing has declared a theater yet when they are read. + +A palette file the deployment does not ship leaves that palette unchanged, and the name goes to the debug log. The game starts either way. + ## What a save carries ```ini title="OPENTS.INI" diff --git a/manual/content/formats/save-games.md b/manual/content/formats/save-games.md index f3f3fe50e..8d60d0bb7 100644 --- a/manual/content/formats/save-games.md +++ b/manual/content/formats/save-games.md @@ -75,7 +75,7 @@ In a game against other machines the master can load one of the match's saved ga ## What the file holds -The field table carries the description shown in the list, the player's name and house, the campaign and scenario numbers, the game type, three timestamps, the name of the program that wrote the save, and the build version of the game that wrote it. The header carries the format's own version. +The field table carries the description shown in the list, the player's house, the campaign and scenario numbers, the game type, three timestamps, the name of the program that wrote the save, and the build version of the game that wrote it. The header carries the format's own version. The game state is a fixed sequence of records — the scenario, the environment, the rules, the map, the loose global values, and every list of type definitions and runtime objects — written and read back in the same order. Among the loose values are the looping sounds left at waypoints by [Play Sound Effect At](/mapping/actions/taction-play-sound-at/) and the sounds attached to objects, each as the sound and its place or object; the playing sound itself is not saved and starts again on the first sound tick after the load. Each list stores its own length ahead of its members, and each member writes out the members its class declares, in the order that class lists them. What a save holds is therefore a field-by-field record of each object rather than a copy of the bytes it occupied in memory. Type definitions travel with the save, so a save carries the rules types it was made with rather than looking them up again on load. Artwork does not travel with it: once a restored type's members have been read, its shape and voxel pointers are released and fetched from the archives again, so a save loaded against a changed set of files gets the current artwork. One piece does not come back. A UnitType drawn from shapes is given a [voxel turret](/formats/vxl-hva/) when the rules are read, by a routine no restore calls; the restore takes the ordinary voxel path instead, which releases that turret along with the body model it could not find. Its voxel barrel is fetched back, and the barrel is what the shape path draws. diff --git a/manual/content/formats/sound-ini.md b/manual/content/formats/sound-ini.md index 4c8813926..529313315 100644 --- a/manual/content/formats/sound-ini.md +++ b/manual/content/formats/sound-ini.md @@ -13,6 +13,7 @@ key_scopes: source: sound related: - { type: format, id: aud } + - { type: format, id: opents-ini } source_files: - code/init.cpp - code/vocini.cpp diff --git a/manual/content/formats/theme-ini.md b/manual/content/formats/theme-ini.md index 79e0bbb38..2e0a25a6d 100644 --- a/manual/content/formats/theme-ini.md +++ b/manual/content/formats/theme-ini.md @@ -13,6 +13,7 @@ key_scopes: source: theme related: - { type: format, id: aud } + - { type: format, id: opents-ini } source_files: - code/init.cpp - code/theme.cpp diff --git a/manual/content/formats/tutorial-ini.md b/manual/content/formats/tutorial-ini.md index 8f95bbaaa..71d0db897 100644 --- a/manual/content/formats/tutorial-ini.md +++ b/manual/content/formats/tutorial-ini.md @@ -13,6 +13,8 @@ related: id: TACTION_TEXT_TRIGGER - type: key id: MessageDelay +- type: format + id: opents-ini filenames: - TUTORIAL.INI --- diff --git a/manual/content/internals/locomotion.md b/manual/content/internals/locomotion.md index d2055d75e..d9e912943 100644 --- a/manual/content/internals/locomotion.md +++ b/manual/content/internals/locomotion.md @@ -28,11 +28,11 @@ Movement, destination, layer, occupation, and locomotor-specific drawing queries | Operation | State transition | | --- | --- | -| `Begin_Piggyback(previous)` | Takes ownership of `previous` and stores it inside the new locomotor. It refuses a null locomotor or an already occupied slot, and a refused locomotor is destroyed rather than returned to the caller. | +| `Begin_Piggyback(previous)` | Stores `previous` inside the new locomotor and takes ownership of it. Answers `false` and leaves `previous` with the caller when there is nothing to store or the slot is already occupied. | | Replace `FootClass::Locomotion` | Makes the new locomotor the object's active movement interface. The new locomotor must already be linked to the same object. | -| `End_Piggyback()` | Gives the stored locomotor back to the caller and empties the piggyback slot. It gives back nothing when no locomotor is stored. | +| `End_Piggyback()` | Hands the stored locomotor back to the caller and empties the piggyback slot. Answers nothing when no locomotor was stored. | -`FootClass::Link_DropPod` applies this sequence with the ballistic locomotor: it retains the passenger's current locomotor through `Begin_Piggyback`, then installs the ballistic interface. Drop-pod touchdown assigns what `End_Piggyback` gives back to `FootClass::Locomotion` before attempting ground placement. +`FootClass::Link_DropPod` applies this sequence with the ballistic locomotor: it retains the passenger's current locomotor through `Begin_Piggyback`, then installs the ballistic interface. Drop-pod touchdown assigns the locomotor `End_Piggyback` returns back to `FootClass::Locomotion`, when the pod carried one, before attempting ground placement. Callers that perform opportunistic restoration first consult `Is_Ok_To_End`. The drop-pod touchdown path calls `End_Piggyback` directly at ground contact because its descent state already establishes the transition. diff --git a/manual/content/keys/maxveinholegrowth.md b/manual/content/keys/maxveinholegrowth.md index 84e367ca9..f367bc586 100644 --- a/manual/content/keys/maxveinholegrowth.md +++ b/manual/content/keys/maxveinholegrowth.md @@ -7,7 +7,7 @@ when_omitted: value: "1000" --- -Every monster is measured against the figure twice before each [growth step](/systems/veins/#growth): it may have handed out at most this many less 40 frontier entries, and it may cover at most this many less 100 mature cells. The same figure sizes the frontier record each monster allocates, so it is a hard ceiling on one monster's field rather than a target the field settles at. Every monster in the scenario shares the one setting. +Every monster is measured against the figure twice before each [growth step](/systems/veins/#growth): it may have handed out at most this many less 40 frontier entries, and it may cover at most this many less 100 mature cells. Neither test is ever relaxed, so the figure is a hard ceiling on one monster's field rather than a target the field settles at. Every monster in the scenario shares the one setting. :::caution[A figure below 100 stops growth outright] The coverage test compares a monster's mature-cell count against the figure less 100. A monster covering nothing at all already fails that test when the figure is below 100, so no vein grows anywhere in the scenario. diff --git a/manual/content/systems/route-search.md b/manual/content/systems/route-search.md index 6bde576fb..3af001360 100644 --- a/manual/content/systems/route-search.md +++ b/manual/content/systems/route-search.md @@ -56,14 +56,12 @@ The corridor stage is skipped outright under **any of:** With the stage skipped the cell search is free to spread anywhere on the playfield. -:::danger[A corridor of more than 500 blocks is written past the end of the list holding it] -Each of the three block sizes has a list of 500 entries to record its chain in. The chain is written into that list at the length the block search settled on, and nothing compares that length against the 500 the list holds, so a longer chain simply runs off the end. +:::caution[A corridor of more than 500 blocks is abandoned] +Each of the three block sizes has a list of 500 entries to record its chain in. A chain longer than the list is refused, and the cell search runs unrestricted instead. -The 2-by-2 chain is the long one. A route crosses about one 2-by-2 block every two cells, and four cells of route is the most that can ever fall inside a single block, so 500 of them is a walk of roughly a thousand cells — half of what it takes to fill the move list at the foot of this page. +The 2-by-2 chain is the long one. A route crosses about one 2-by-2 block every two cells, and four cells of route is the most that can ever fall inside a single block, so 500 of them is a walk of roughly a thousand cells -- half of what it takes to fill the move list at the foot of this page. A journey that long gives up the corridor and prices every cell it passes, which costs more effort than a corridor would have. -The three lists sit one after another, smallest blocks first, with the record of how long each chain is behind them. Entries past the end of the 2-by-2 list land on the 4-by-4 chain; a chain past a thousand blocks reaches the 8-by-8 chain as well; and one past fifteen hundred runs over the three recorded lengths and then past the end of the pathfinder itself. The chain is written from its far end backwards, so the furthest write is the first one made. A retry reads the chains it has overwritten, and strikes out block links accordingly. - -The block stage runs before the cell search, so this happens before a single cell has been priced. It also runs on its own whenever the game measures how far an object would have to walk between two cells, and that measurement takes none of the skips above. +The block stage runs before the cell search, so this is settled before a single cell has been priced. It also runs on its own whenever the game measures how far an object would have to walk between two cells, and that measurement takes none of the skips above. ::: ### The cell-by-cell search @@ -86,10 +84,10 @@ A request that ends with no route at all arms the object's [`PathDelay`](/keys/p Alongside the effort limit, a finished pass is checked against a second figure. A pass that reaches the destination having taken up exactly 10,000 cells is treated as a failure and hands back no route, although the route was found and is complete. No other count is treated that way, and nothing about such a route distinguishes it from one found a cell earlier or later. ::: -:::danger[A pass that reaches more than 65,536 cells writes past the end of its record of them] -The effort limit counts the cells a pass takes up, but a cell is recorded the moment it is first priced — well before it is taken up, and for many cells that never are. The cells on the search's outer edge count against the record too, and the record holds 65,536 of them against an effort limit of 65,527. Nothing checks it as cells are added, so a pass that reaches a 65,537th cell writes that cell over the tally of how many the record holds and over whatever follows it. The tally is then a nonsense figure, and every cell reached afterwards is written wherever it points. +:::caution[A pass that reaches more than 131,072 cells stops taking new ones] +The effort limit counts the cells a pass takes up, but a cell is recorded the moment it is first priced -- well before it is taken up, and for many cells that never are. The cells on the search's outer edge count against the record too. The record holds 131,072 of them. A pass that has filled it passes over every further cell it reaches, so it runs out of candidates and hands back no route. -A cell is recorded once and no more, so reaching that many needs ground to match: more than 65,536 cells the object may enter in a single pass, which a square playable area of open ground passes at around 181 cells on a side. A cell spanned by a bridge is recorded twice over, once for the ground and once for the deck. The destination has also to be far enough off, or awkward enough to arrive at, that the search spreads over all of that ground before it settles on a route. +A cell is recorded once and no more, so reaching that many needs ground to match: more than 131,072 cells the object may enter in a single pass, which a square playable area of open ground passes at around 256 cells on a side. A cell spanned by a bridge is recorded twice over, once for the ground and once for the deck. The destination has also to be far enough off, or awkward enough to arrive at, that the search spreads over all of that ground before it settles on a route. ::: ## Why a route is not the shortest one diff --git a/manual/content/systems/tiberium.md b/manual/content/systems/tiberium.md index 91026bf5c..6c6bc3cdc 100644 --- a/manual/content/systems/tiberium.md +++ b/manual/content/systems/tiberium.md @@ -115,7 +115,7 @@ Removing stages from a cell standing at stage 11 tries to put it back into the g ## Spread -Spread passes are scheduled the same way, from the [`Spread`](/keys/spread/#scope-tiberium) delay, and no flag shortens them. The budget is the queued count multiplied by [`SpreadPercentage`](/keys/spreadpercentage/), clamped to between 5 and 25, with a random figure drawn from 1 up to it. Only a cell that finds somewhere to seed counts against that budget; a cell hemmed in on all eight sides is dropped from the queue without spending any of it, and a cell with more than one free neighbor is re-queued to run again on the next pass. +Spread passes are scheduled the same way, from the [`Spread`](/keys/spread/#scope-tiberium) delay, and no flag shortens them. The budget is the queued count multiplied by [`SpreadPercentage`](/keys/spreadpercentage/), clamped to between 5 and 25, with a random figure drawn from 1 up to it. Only a cell that finds somewhere to seed counts against that budget. A cell hemmed in on all eight sides is dropped from the queue without spending any of it, as is one that no longer carries Tiberium of its own; a cell with more than one free neighbor is re-queued to run again on the next pass. A cell may spread when all of the following hold, tested in this order: diff --git a/manual/content/using/configuration-files.md b/manual/content/using/configuration-files.md index 830e86789..bb8e271d5 100644 --- a/manual/content/using/configuration-files.md +++ b/manual/content/using/configuration-files.md @@ -21,6 +21,6 @@ Configuration is documented in five areas rather than one. The table names what | [Command line options](/using/command-line/) | Options accepted on the OpenTS command line | | [Mapping](/mapping/) | Scenario sections, triggers, TeamTypes, TaskForces, Scripts, and AI triggers | -`SUN.INI` stores local player options. `KEYBOARD.INI` maps command names to keys. [`UI.INI`](/formats/ui-ini/), which a mod or deployment may ship, styles the order lines. Rules, art, sound, theme, and scenario files supply game and mod data. +`SUN.INI` stores local player options. `KEYBOARD.INI` maps command names to keys. [`UI.INI`](/formats/ui-ini/), which a mod or deployment may ship, styles the order lines. Rules, art, sound, theme, and scenario files supply game and mod data. A deployment that keeps these under other names writes them in its [`OPENTS.INI`](/formats/opents-ini/#the-files-it-reads). The engine loads its configuration files in a defined order. Use the relevant Format page when file selection, layering, registration, or section identity affects the result. diff --git a/manual/data/ini-keys.yaml b/manual/data/ini-keys.yaml index dd6af0039..d3ce77030 100644 --- a/manual/data/ini-keys.yaml +++ b/manual/data/ini-keys.yaml @@ -13123,7 +13123,7 @@ Locomotor: section: kind: identifier source: object-type - value_type: classid + value_type: Locomotor class identifier status: generated _provenance: default_candidate: null diff --git a/manual/data/ini-read-exclusions.yaml b/manual/data/ini-read-exclusions.yaml index 298d7cbcd..5f9a5a7ee 100644 --- a/manual/data/ini-read-exclusions.yaml +++ b/manual/data/ini-read-exclusions.yaml @@ -133,9 +133,9 @@ site_exclusions: - path: code/deploymentconfig.cpp function: DeploymentConfigClass::Read_INI - keys: [SearchPaths, CarryScenarioFile] + keys: [SearchPaths, CarryScenarioFile, Rules, RulesExpansion, Art, ArtExpansion, AI, AIExpansion, Sound, SoundExpansion, Theme, ThemeExpansion, Battle, BattleExpansion, LanguageRules, LanguageRulesExpansion, Tutorial, UI, Settings, Scheme, Game] classification: excluded - reason: Both are settings of the deployment's own file, documented as that format rather than as game data. + reason: Each is a setting of the deployment's own file, documented as that format rather than as game data. - path: code/spawnerconfig.cpp function: SpawnerConfigClass::Read_Slots diff --git a/manual/site/tests/documentation-source-contract.test.mjs b/manual/site/tests/documentation-source-contract.test.mjs index 4dc60e2c6..e51c5ba59 100644 --- a/manual/site/tests/documentation-source-contract.test.mjs +++ b/manual/site/tests/documentation-source-contract.test.mjs @@ -108,7 +108,7 @@ test('Blocked Drop pod touchdown retains its exact damage, animation, and deleti 'FootClass * linked = LinkedTo;', 'coord = linked->PositionCoord;', 'linked->Limbo();', - 'LinkedTo->Locomotion = End_Piggyback();', + 'LinkedTo->Locomotion = std::move(carried);', 'if (!linked->Unlimbo(coord, DIR_N)) {', 'Explosion_Damage(coord, 100, LinkedTo, Rule->C4Warhead);', 'Combat_Anim(100, Rule->C4Warhead, LAND_CLEAR, coord)', @@ -971,3 +971,55 @@ test('New theater artwork is renamed by image letter, not by a prefix list', () 'the shape fetch calls the convention rather than repeating it', ); }); + +test('The deployment names the files the game reads', () => { + const config = functionBody( + source('code/deploymentconfig.cpp'), + 'void DeploymentConfigClass::Read_INI(INIClass const & ini)', + ); + + for (const [key, member] of [ + ['Rules', 'RulesFile'], + ['RulesExpansion', 'RulesExpansionFile'], + ['Art', 'ArtFile'], + ['Settings', 'SettingsFile'], + ]) { + assert.match( + config, + new RegExp(`${member} = ini\\.Get_String\\("Files", "${key}", ${member}\\.c_str\\(\\)\\);`), + `the deployment names its ${key} file`, + ); + } + + assert.match( + config, + /SchemePaletteFile = ini\.Get_String\("Palettes", "Scheme", SchemePaletteFile\.c_str\(\)\);/, + 'and the palette it starts from', + ); + + const init = source('code/init.cpp'); + + assert.match( + init, + /stricmp\(name\.c_str\(\), DeploymentConfig\.RulesFile\.c_str\(\)\) == 0/, + 'the wildcard search knows the rules file by the name the deployment gives it', + ); + + assert.match( + init, + /CCFileClass file\(DeploymentConfig\.RulesFile\.c_str\(\)\);/, + 'and the file it falls back on is that same one', + ); + + assert.match( + init, + /Read_Palette\(SchemePalette, DeploymentConfig\.SchemePaletteFile\.c_str\(\)\);/, + 'the palettes are read through the names it gives', + ); + + assert.match( + functionBody(source('code/addon.cpp'), 'void Detect_Addons(void)'), + /CCFileClass\(DeploymentConfig\.RulesExpansionFile\.c_str\(\)\)\.Is_Available\(\)/, + 'the expansion is looked for under the name the deployment gives it', + ); +}); diff --git a/manual/tools/extract_engine.py b/manual/tools/extract_engine.py index c4f3cf656..f999f560c 100644 --- a/manual/tools/extract_engine.py +++ b/manual/tools/extract_engine.py @@ -93,7 +93,7 @@ def _yaml(): "SpeedType": "SpeedType", "MPHType": "speed", "Side": "Side", - "CLSID": "Locomotor CLSID", + "ClassID": "Locomotor class identifier", "Owners": "list of HouseTypes", "Scheme_Index": "colour scheme", "BuildingType_List": "list of BuildingTypes", @@ -975,7 +975,7 @@ def resolve_default(rec, all_defaults, tree): "DIR_N": "0", "CALL_WAIT_CUSTOM": "3", "MAX_PLAYERS": "8", - "CLSID_TeleportLocomotion": "the Teleport locomotor", + "ClassID_TeleportLocomotion": "the Teleport locomotor", "CELL_LEPTON_W": "256", "4*CELL_LEPTON_W": "1024", "9*CELL_LEPTON_W": "2304", diff --git a/platform/win32compat/include/windows.h b/platform/win32compat/include/windows.h index 684d493a8..6c20ae8af 100644 --- a/platform/win32compat/include/windows.h +++ b/platform/win32compat/include/windows.h @@ -20,6 +20,9 @@ typedef unsigned char BYTE; typedef unsigned short WORD; typedef unsigned int DWORD; typedef long LONG; +// Windows makes LONG 32 bits wide. It is the host's long here, so a structure whose +// on-disk layout is fixed names LONG32 for the fields that must stay four bytes. +typedef std::int32_t LONG32; typedef unsigned long ULONG; typedef unsigned int UINT; typedef int INT; @@ -151,10 +154,11 @@ typedef struct tagMEASUREITEMSTRUCT { UINT CtlType, CtlID, itemID, itemWidth, it typedef struct tagBITMAPFILEHEADER { WORD bfType; DWORD bfSize; WORD bfReserved1, bfReserved2; DWORD bfOffBits; } BITMAPFILEHEADER, *LPBITMAPFILEHEADER; #pragma pack(pop) typedef struct tagBITMAPINFOHEADER { - DWORD biSize; LONG biWidth, biHeight; WORD biPlanes, biBitCount; - DWORD biCompression, biSizeImage; LONG biXPelsPerMeter, biYPelsPerMeter; + DWORD biSize; LONG32 biWidth, biHeight; WORD biPlanes, biBitCount; + DWORD biCompression, biSizeImage; LONG32 biXPelsPerMeter, biYPelsPerMeter; DWORD biClrUsed, biClrImportant; } BITMAPINFOHEADER, *LPBITMAPINFOHEADER; +static_assert(sizeof(BITMAPINFOHEADER) == 40, "the BMP info header is 40 bytes on disk"); typedef struct tagRGBQUAD { BYTE rgbBlue, rgbGreen, rgbRed, rgbReserved; } RGBQUAD; typedef struct tagBITMAPINFO { BITMAPINFOHEADER bmiHeader; RGBQUAD bmiColors[1]; } BITMAPINFO, *LPBITMAPINFO; typedef struct tagBITMAP { LONG bmType, bmWidth, bmHeight, bmWidthBytes; WORD bmPlanes, bmBitsPixel; LPVOID bmBits; } BITMAP; @@ -797,6 +801,8 @@ BOOL EnumDisplaySettings(LPCSTR device, DWORD mode, DEVMODE * settings); #define FILE_CURRENT 1 #define FILE_END 2 #define INVALID_SET_FILE_POINTER 0xFFFFFFFFL +#define INVALID_FILE_SIZE 0xFFFFFFFFL +#define MOVEFILE_REPLACE_EXISTING 0x00000001L #define INVALID_FILE_ATTRIBUTES 0xFFFFFFFFL #define FILE_ATTRIBUTE_READONLY 0x00000001L #define FILE_ATTRIBUTE_HIDDEN 0x00000002L @@ -858,7 +864,10 @@ HANDLE CreateFileA(LPCSTR name, DWORD access, DWORD share, LPSECURITY_ATTRIBUTES BOOL ReadFile(HANDLE file, LPVOID buffer, DWORD size, LPDWORD read, LPOVERLAPPED overlapped); BOOL WriteFile(HANDLE file, LPCVOID buffer, DWORD size, LPDWORD written, LPOVERLAPPED overlapped); DWORD SetFilePointer(HANDLE file, LONG distance, LONG * distancehigh, DWORD method); +DWORD GetFileSize(HANDLE file, LPDWORD sizehigh); +BOOL FlushFileBuffers(HANDLE file); BOOL DeleteFileA(LPCSTR name); +BOOL MoveFileExA(LPCSTR from, LPCSTR to, DWORD flags); BOOL CopyFile(LPCSTR from, LPCSTR to, BOOL failifexists); BOOL CreateDirectory(LPCSTR path, LPSECURITY_ATTRIBUTES attributes); BOOL SetCurrentDirectory(LPCSTR path); @@ -871,6 +880,7 @@ BOOL FileTimeToLocalFileTime(FILETIME const * file, LPFILETIME local); BOOL FileTimeToSystemTime(FILETIME const * file, LPSYSTEMTIME system); BOOL SystemTimeToFileTime(SYSTEMTIME const * system, LPFILETIME file); void GetSystemTime(LPSYSTEMTIME system); +void GetSystemTimeAsFileTime(LPFILETIME file); BOOL AllocConsole(void); HWND GetConsoleWindow(void); diff --git a/platform/win32compat/src/kernel.cpp b/platform/win32compat/src/kernel.cpp index 787fac0ac..f52a2c9dc 100644 --- a/platform/win32compat/src/kernel.cpp +++ b/platform/win32compat/src/kernel.cpp @@ -453,6 +453,26 @@ extern "C" void GetSystemTime(LPSYSTEMTIME system) } +// A file time counts hundred nanosecond ticks from the start of 1601, which is +// 11644473600 seconds before the epoch the host clock counts from. +extern "C" void GetSystemTimeAsFileTime(LPFILETIME file) +{ + if (file == NULL) { + return; + } + + timeval now; + gettimeofday(&now, NULL); + + unsigned long long const ticks = 116444736000000000ULL + + (unsigned long long)now.tv_sec * 10000000ULL + + (unsigned long long)now.tv_usec * 10ULL; + + file->dwLowDateTime = (DWORD)(ticks & 0xFFFFFFFFULL); + file->dwHighDateTime = (DWORD)(ticks >> 32); +} + + extern "C" void GetLocalTime(LPSYSTEMTIME system) { if (system == NULL) { @@ -711,6 +731,44 @@ extern "C" DWORD SetFilePointer(HANDLE handle, LONG distance, LONG * distancehig } +extern "C" DWORD GetFileSize(HANDLE handle, LPDWORD sizehigh) +{ + if (handle == NULL || handle == INVALID_HANDLE_VALUE) { + return(INVALID_FILE_SIZE); + } + + std::FILE * const file = (std::FILE *)handle; + long const here = std::ftell(file); + + if (here < 0 || std::fseek(file, 0, SEEK_END) != 0) { + return(INVALID_FILE_SIZE); + } + + long const end = std::ftell(file); + std::fseek(file, here, SEEK_SET); + + if (end < 0) { + return(INVALID_FILE_SIZE); + } + + if (sizehigh != NULL) { + *sizehigh = 0; + } + + return((DWORD)end); +} + + +extern "C" BOOL FlushFileBuffers(HANDLE handle) +{ + if (handle == NULL || handle == INVALID_HANDLE_VALUE) { + return(FALSE); + } + + return(std::fflush((std::FILE *)handle) == 0 ? TRUE : FALSE); +} + + extern "C" BOOL CloseHandle(HANDLE handle) { if (handle == NULL || handle == INVALID_HANDLE_VALUE) { @@ -728,6 +786,20 @@ extern "C" BOOL DeleteFileA(LPCSTR name) } +// Only the replacing form is supplied, since a rename over an existing file is what the +// standard library's rename already does and is the one the engine asks for. +extern "C" BOOL MoveFileExA(LPCSTR from, LPCSTR to, DWORD flags) +{ + if (from == NULL || to == NULL || (flags & MOVEFILE_REPLACE_EXISTING) == 0) { + return(FALSE); + } + + std::error_code error; + std::filesystem::rename(from, to, error); + return(error ? FALSE : TRUE); +} + + extern "C" BOOL CopyFile(LPCSTR from, LPCSTR to, BOOL failifexists) { if (from == NULL || to == NULL) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 89e98bbb3..b2c0e9a8b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,6 +23,7 @@ function(opents_add_test target) add_executable(${target} ${sources}) target_compile_features(${target} PRIVATE cxx_std_20) + set_target_properties(${target} PROPERTIES FOLDER "Tests") target_include_directories(${target} PRIVATE "${CMAKE_SOURCE_DIR}/code" @@ -97,4 +98,5 @@ add_subdirectory(tutorial) add_subdirectory(utf8) add_subdirectory(shapefacing) add_subdirectory(zbufring) +add_subdirectory(priorityqueue) add_subdirectory(save) diff --git a/tests/deploymentconfig/deploymentconfigcontract.cpp b/tests/deploymentconfig/deploymentconfigcontract.cpp index 14031ffe2..592725d4b 100644 --- a/tests/deploymentconfig/deploymentconfigcontract.cpp +++ b/tests/deploymentconfig/deploymentconfigcontract.cpp @@ -211,6 +211,90 @@ void Test_Carry_Scenario_File(void) Check(!config.CarryScenarioFile, "with the file gone it is off"); } + +void Test_File_Names(void) +{ + DeploymentConfigClass config; + + Check(config.RulesFile == "RULES.INI", "with no file the rules come from RULES.INI"); + Check(config.SettingsFile == "SUN.INI", "and a player's settings from SUN.INI"); + + Write_File(Root + "\\OPENTS.INI", "[Paths]\nSearchPaths=Data\n"); + config.Read_File(""); + Check(config.ArtFile == "ART.INI", "a file that names none of them leaves the defaults"); + + Write_File(Root + "\\OPENTS.INI", + "[Files]\nRules=dtarules.ini\nArt=dtaart.ini\nAI=dtaai.ini\nSound=dtasound.ini\n" + "Theme=dtatheme.ini\nBattle=dtabattle.ini\nLanguageRules=dtalang.ini\n" + "Tutorial=dtatutorial.ini\nUI=dtaui.ini\nSettings=Settings.ini\n"); + config.Read_File(""); + Check(config.RulesFile == "dtarules.ini", "a name it writes for the rules is taken"); + Check(config.ArtFile == "dtaart.ini", "and for the artwork"); + Check(config.AIFile == "dtaai.ini", "and for the AI"); + Check(config.SoundFile == "dtasound.ini", "and for the sounds"); + Check(config.ThemeFile == "dtatheme.ini", "and for the music"); + Check(config.BattleFile == "dtabattle.ini", "and for the campaigns"); + Check(config.LanguageRulesFile == "dtalang.ini", "and for the translated rules"); + Check(config.TutorialFile == "dtatutorial.ini", "and for the tutorial text"); + Check(config.UIFile == "dtaui.ini", "and for the interface"); + Check(config.SettingsFile == "Settings.ini", "and for a player's settings"); + Check(config.ArtExpansionFile == "ARTFS.INI", "an expansion file it leaves alone keeps its name"); + + Remove_File(Root + "\\OPENTS.INI"); + config.Read_File(""); + Check(config.RulesFile == "RULES.INI", "with the file gone the names return to the defaults"); + Check(config.SettingsFile == "SUN.INI", "every one of them"); +} + + +void Test_Expansion_File_Names(void) +{ + DeploymentConfigClass config; + + Check(config.RulesExpansionFile == "FIRESTRM.INI", "with no file the expansion rules are FIRESTRM.INI"); + + Write_File(Root + "\\OPENTS.INI", + "[Files]\nRulesExpansion=fsrules.ini\nArtExpansion=fsart.ini\nAIExpansion=fsai.ini\n" + "SoundExpansion=fssound.ini\nThemeExpansion=fstheme.ini\nBattleExpansion=fsbattle.ini\n" + "LanguageRulesExpansion=fslang.ini\n"); + config.Read_File(""); + Check(config.RulesExpansionFile == "fsrules.ini", "a name it writes for the expansion rules is taken"); + Check(config.ArtExpansionFile == "fsart.ini", "and for the expansion artwork"); + Check(config.AIExpansionFile == "fsai.ini", "and for the expansion AI"); + Check(config.SoundExpansionFile == "fssound.ini", "and for the expansion sounds"); + Check(config.ThemeExpansionFile == "fstheme.ini", "and for the expansion music"); + Check(config.BattleExpansionFile == "fsbattle.ini", "and for the expansion campaigns"); + Check(config.LanguageRulesExpansionFile == "fslang.ini", "and for the translated expansion rules"); + Check(config.RulesFile == "RULES.INI", "while the base files stand where it names none of them"); + + Remove_File(Root + "\\OPENTS.INI"); + config.Read_File(""); + Check(config.RulesExpansionFile == "FIRESTRM.INI", "with the file gone they return to the defaults"); +} + + +void Test_Palette_Names(void) +{ + DeploymentConfigClass config; + + Check(config.SchemePaletteFile == "UNITSNO.PAL", "with no file the scheme palette is UNITSNO.PAL"); + Check(config.GamePaletteFile == "TEMPERAT.PAL", "and the starting palette TEMPERAT.PAL"); + + Write_File(Root + "\\OPENTS.INI", "[Palettes]\nScheme=UNITTEM.PAL\nGame=DESERT.PAL\n"); + config.Read_File(""); + Check(config.SchemePaletteFile == "UNITTEM.PAL", "the names it writes are taken"); + Check(config.GamePaletteFile == "DESERT.PAL", "both of them"); + + Write_File(Root + "\\OPENTS.INI", "[Palettes]\nScheme=UNITTEM.PAL\n"); + config.Read_File(""); + Check(config.GamePaletteFile == "TEMPERAT.PAL", "and one named alone leaves the other at its default"); + + Remove_File(Root + "\\OPENTS.INI"); + config.Read_File(""); + Check(config.SchemePaletteFile == "UNITSNO.PAL", "with the file gone both return to the defaults"); + Check(config.GamePaletteFile == "TEMPERAT.PAL", "as they stand in Tiberian Sun"); +} + } @@ -231,6 +315,9 @@ int main(void) Test_The_Directory_Named(); Test_A_Read_Starts_Over(); Test_Carry_Scenario_File(); + Test_File_Names(); + Test_Expansion_File_Names(); + Test_Palette_Names(); Remove_Root(); diff --git a/tests/priorityqueue/CMakeLists.txt b/tests/priorityqueue/CMakeLists.txt new file mode 100644 index 000000000..ab070d7c1 --- /dev/null +++ b/tests/priorityqueue/CMakeLists.txt @@ -0,0 +1,15 @@ +# The queue is a template with no source of its own, so the harness compiles nothing from +# code/. It lives outside code/ so that the recursive glob building OpenTS cannot pick this +# target's entry point up. +# +# FLOAT is not decoration here: the order two nodes of equal score come out in turns on +# exact float comparison, so the harness has to carry the float semantics the engine builds +# with to measure what the engine measures. + +opents_add_test(PriorityQueue + NAME priorityqueue + SOURCES priorityqueuetest.cpp + INCLUDES ${CMAKE_CURRENT_SOURCE_DIR} + DEFINITIONS WIN32 _WINDOWS _MBCS NOMINMAX + FLOAT +) diff --git a/tests/priorityqueue/priorityqueuetest.cpp b/tests/priorityqueue/priorityqueuetest.cpp new file mode 100644 index 000000000..8b9948289 --- /dev/null +++ b/tests/priorityqueue/priorityqueuetest.cpp @@ -0,0 +1,445 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// Holds the priority queue to its extraction order: what comes out for a given set of +// scores, and above all what comes out when several nodes carry the same score. +// +// The tie expectations are why this file exists. Random maps are built locally on every +// peer from a shared seed, and map generation seeds its spreads at exactly Score = 0.0f, +// so a reordering among equal scores would change the maps peers build from one build to +// the next. Every sequence below was recorded from the queue as it stood before its +// storage was rewritten, and every result crosses the API through Element_Of, so the same +// expectations read the same whether extraction hands back a pointer into the caller's +// nodes or a value. +// +// Needs no game data. + +#include "priority.h" + +#include +#include +#include + + +namespace { + +int Failures = 0; +int Checked = 0; + + +/// Stands in for a node the queue orders. It carries CellNode's contract rather than the +/// type itself, which would drag the map and COM headers in behind it: the queue orders on +/// Score alone and matches on Element alone, and every comparison widens to double exactly +/// as code/nodes.h does. +struct TestNode { + int Element; + float Score; + + bool operator==(TestNode const & other) const { return((double)Score == (double)other.Score); } + bool operator!=(TestNode const & other) const { return((double)Score != (double)other.Score); } + bool operator<(TestNode const & other) const { return((double)Score < (double)other.Score); } + bool operator>(TestNode const & other) const { return((double)Score > (double)other.Score); } + bool operator<=(TestNode const & other) const { return((double)Score <= (double)other.Score); } + bool operator>=(TestNode const & other) const { return((double)Score >= (double)other.Score); } +}; + + +/// Stands for the queue handing nothing back. +int const EMPTY = -1; + + +void Check(bool passed, char const * what) +{ + Checked++; + if (!passed) { + std::printf("FAILED %s\n", what); + Failures++; + } +} + + +/* + * These three name the element behind whatever the queue handed back. Extraction and + * Replace_Root return a pointer into the caller's nodes today and a value once the queue + * holds its elements itself, and every expectation below is written against the element + * number so that neither spelling reaches the cases. + */ +int Element_Of(TestNode const * node) +{ + return(node != NULL ? node->Element : EMPTY); +} + + +int Element_Of(TestNode const & node) +{ + return(node.Element); +} + + +int Element_Of(std::optional const & node) +{ + return(node ? node->Element : EMPTY); +} + + +template +int Pop(Q & queue) +{ + return(Element_Of(queue.Extract_Min())); +} + + +template +int Replace(Q & queue, TestNode & node) +{ + return(Element_Of(queue.Replace_Root(node))); +} + + +/// Fills the queue from a score list, numbering each node by its position in that list +/// counting from one. The caller's array has to outlive the queue, because the queue held +/// pointers into it before it held values. +template +void Fill(Q & queue, TestNode * nodes, float const * scores, int count) +{ + for (int index = 0; index < count; index++) { + nodes[index].Element = index + 1; + nodes[index].Score = scores[index]; + queue.Insert(nodes[index]); + } +} + + +/// Empties the queue into a comma separated list of element numbers, which is the form +/// every expectation below is written in. +template +void Drain(Q & queue, char * out, int size) +{ + int used = 0; + out[0] = '\0'; + + for (;;) { + int element = Pop(queue); + if (element == EMPTY) { + break; + } + + int written = std::snprintf(out + used, size - used, used == 0 ? "%d" : ",%d", element); + if (written <= 0 || written >= size - used) { + break; + } + used += written; + } +} + + +template +void Check_Drain(Q & queue, char const * expected, char const * what) +{ + char got[256]; + Drain(queue, got, sizeof(got)); + + Checked++; + if (std::strcmp(got, expected) != 0) { + std::printf("FAILED %s\n expected %s\n got %s\n", what, expected, got); + Failures++; + } +} + + +void Check_Ordering(void) +{ + { + PriorityQueueClass queue(64); + TestNode nodes[8]; + float const scores[8] = {5.0f, 1.0f, 8.0f, 3.0f, 7.0f, 2.0f, 6.0f, 4.0f}; + Fill(queue, nodes, scores, 8); + + Check(queue.Count() == 8, "the queue counts what went in"); + Check_Drain(queue, "2,6,4,8,1,7,5,3", "a shuffled set comes out in score order"); + } + + { + PriorityQueueClass queue(64); + TestNode nodes[5]; + float const scores[5] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + Fill(queue, nodes, scores, 5); + + Check_Drain(queue, "1,2,3,4,5", "an ascending set comes out unchanged"); + } + + { + PriorityQueueClass queue(64); + TestNode nodes[5]; + float const scores[5] = {5.0f, 4.0f, 3.0f, 2.0f, 1.0f}; + Fill(queue, nodes, scores, 5); + + Check_Drain(queue, "5,4,3,2,1", "a descending set comes out reversed"); + } + + { + PriorityQueueClass queue(64); + TestNode node; + node.Element = 1; + node.Score = 0.0f; + queue.Insert(node); + + Check_Drain(queue, "1", "a queue holding one node hands it back"); + } + + { + PriorityQueueClass queue(64); + + Check(queue.Count() == 0, "a fresh queue counts nothing"); + Check(Pop(queue) == EMPTY, "an empty queue hands nothing back"); + Check(Pop(queue) == EMPTY, "an empty queue hands nothing back twice running"); + } +} + + +void Check_Ties(void) +{ + { + PriorityQueueClass queue(64); + TestNode nodes[8]; + float const scores[8] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; + Fill(queue, nodes, scores, 8); + + Check_Drain(queue, "1,8,7,6,5,4,3,2", "eight nodes at one score come out in a fixed order"); + } + + { + PriorityQueueClass queue(64); + TestNode nodes[8]; + float const scores[8] = {5.0f, 1.0f, 5.0f, 3.0f, 1.0f, 9.0f, 3.0f, 1.0f}; + Fill(queue, nodes, scores, 8); + + Check_Drain(queue, "2,5,8,4,7,1,3,6", "nodes sharing a score come out in a fixed order"); + } + + { + PriorityQueueClass queue(64); + TestNode nodes[4]; + float const scores[4] = {0.0f, -0.0f, 0.0f, -0.0f}; + Fill(queue, nodes, scores, 4); + + Check_Drain(queue, "1,4,3,2", "a negative zero ties with a positive one"); + } + + { + PriorityQueueClass queue(64); + TestNode nodes[8]; + float const scores[8] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; + + Fill(queue, nodes, scores, 8); + queue.Clear(); + Fill(queue, nodes, scores, 8); + + Check_Drain(queue, "1,8,7,6,5,4,3,2", "a cleared queue ties the same way a fresh one does"); + } +} + + +void Check_Replace_Root(void) +{ + { + PriorityQueueClass queue(64); + TestNode offered; + offered.Element = 9; + offered.Score = 4.0f; + + Check(Replace(queue, offered) == 9, "an empty queue hands the offered node straight back"); + Check(queue.Count() == 0, "an empty queue stays empty"); + } + + { + PriorityQueueClass queue(64); + TestNode nodes[4]; + float const scores[4] = {5.0f, 7.0f, 6.0f, 8.0f}; + Fill(queue, nodes, scores, 4); + + TestNode offered; + offered.Element = 9; + offered.Score = 2.0f; + + Check(Replace(queue, offered) == 9, "a node below the root comes straight back"); + Check(queue.Count() == 4, "a node below the root does not enter"); + Check_Drain(queue, "1,3,2,4", "the queue is untouched by a node below the root"); + } + + { + PriorityQueueClass queue(64); + TestNode nodes[4]; + float const scores[4] = {5.0f, 7.0f, 6.0f, 8.0f}; + Fill(queue, nodes, scores, 4); + + TestNode offered; + offered.Element = 9; + offered.Score = 6.5f; + + Check(Replace(queue, offered) == 1, "a node above the root turns the root out"); + Check(queue.Count() == 4, "replacing the root leaves the count alone"); + Check_Drain(queue, "3,9,2,4", "the offered node takes its place in order"); + } + + { + PriorityQueueClass queue(64); + TestNode nodes[4]; + float const scores[4] = {5.0f, 7.0f, 6.0f, 8.0f}; + Fill(queue, nodes, scores, 4); + + TestNode offered; + offered.Element = 9; + offered.Score = 5.0f; + + Check(Replace(queue, offered) == 1, "a node level with the root turns the root out"); + Check_Drain(queue, "9,3,2,4", "a node level with the root takes its place"); + } +} + + +void Check_Remove_Matching(void) +{ + float const scores[7] = {5.0f, 7.0f, 6.0f, 9.0f, 8.0f, 10.0f, 11.0f}; + + { + PriorityQueueClass queue(64); + TestNode nodes[7]; + Fill(queue, nodes, scores, 7); + + TestNode wanted; + wanted.Element = 7; + wanted.Score = 0.0f; + + Check(queue.Remove_Matching(wanted) == true, "a node in the last slot is found"); + Check(queue.Count() == 6, "removing a node drops the count"); + Check_Drain(queue, "1,3,2,5,4,6", "the rest come out in score order"); + } + + { + PriorityQueueClass queue(64); + TestNode nodes[7]; + Fill(queue, nodes, scores, 7); + + TestNode wanted; + wanted.Element = 1; + wanted.Score = 0.0f; + + Check(queue.Remove_Matching(wanted) == true, "the node at the root is found"); + Check_Drain(queue, "3,2,5,4,6,7", "the queue re-forms around a removed root"); + } + + { + PriorityQueueClass queue(64); + TestNode nodes[7]; + Fill(queue, nodes, scores, 7); + + TestNode wanted; + wanted.Element = 3; + wanted.Score = 0.0f; + + Check(queue.Remove_Matching(wanted) == true, "a node in a middle slot is found"); + Check_Drain(queue, "1,2,5,4,6,7", "the queue re-forms around a removed middle node"); + } + + { + PriorityQueueClass queue(64); + TestNode nodes[7]; + Fill(queue, nodes, scores, 7); + + TestNode wanted; + wanted.Element = 2; + wanted.Score = 0.0f; + + Check(queue.Remove_Matching(wanted) == true, "a node with a lighter parent is found"); + Check_Drain(queue, "1,3,5,4,6,7", "the queue re-forms around it"); + } + + { + PriorityQueueClass queue(64); + TestNode nodes[7]; + Fill(queue, nodes, scores, 7); + + TestNode wanted; + wanted.Element = 99; + wanted.Score = 0.0f; + + Check(queue.Remove_Matching(wanted) == false, "a node that is not held is not found"); + Check(queue.Count() == 7, "a search that finds nothing removes nothing"); + Check_Drain(queue, "1,3,2,5,4,6,7", "a search that finds nothing leaves the order alone"); + } + + { + PriorityQueueClass queue(64); + TestNode nodes[4]; + float const shared[4] = {5.0f, 7.0f, 6.0f, 9.0f}; + + for (int index = 0; index < 4; index++) { + nodes[index].Element = (index == 1 || index == 3) ? 2 : index + 1; + nodes[index].Score = shared[index]; + queue.Insert(nodes[index]); + } + + TestNode wanted; + wanted.Element = 2; + wanted.Score = 0.0f; + + Check(queue.Remove_Matching(wanted) == true, "one of two nodes sharing an element is found"); + Check_Drain(queue, "1,3,2", "the queue walks its slots rather than the order they went in"); + } +} + + +void Check_Clear(void) +{ + PriorityQueueClass queue(64); + TestNode nodes[6]; + float const scores[6] = {4.0f, 2.0f, 6.0f, 1.0f, 5.0f, 3.0f}; + + Fill(queue, nodes, scores, 6); + queue.Clear(); + + Check(queue.Count() == 0, "a cleared queue counts nothing"); + Check(Pop(queue) == EMPTY, "a cleared queue hands nothing back"); + + Fill(queue, nodes, scores, 6); + Check_Drain(queue, "4,2,6,1,5,3", "a refilled queue orders as a fresh one does"); +} + + +void Check_Capacity(void) +{ + // The size a queue is built with is only how much room it takes up front. + PriorityQueueClass queue(8); + TestNode nodes[10]; + float const scores[10] = {9.0f, 8.0f, 7.0f, 6.0f, 5.0f, 4.0f, 3.0f, 2.0f, 1.0f, 0.0f}; + Fill(queue, nodes, scores, 10); + + Check(queue.Count() == 10, "a queue built with eight slots takes ten nodes"); + Check_Drain(queue, "10,9,8,7,6,5,4,3,2,1", "every node it took comes out in score order"); +} + +} // namespace + + +int main(void) +{ + Check_Ordering(); + Check_Ties(); + Check_Replace_Root(); + Check_Remove_Matching(); + Check_Clear(); + Check_Capacity(); + + std::printf("%-52s %s\n", "Priority queue extraction order", + Failures == 0 ? "ok" : "FAILED"); + std::printf("checked %d cases, %d mismatches\n", Checked, Failures); + + return(Failures == 0 ? 0 : 1); +} diff --git a/tests/save/CMakeLists.txt b/tests/save/CMakeLists.txt index 556327111..fd69fde2f 100644 --- a/tests/save/CMakeLists.txt +++ b/tests/save/CMakeLists.txt @@ -1,29 +1,14 @@ # The harness drives the file a saved game is kept in: savefile.cpp's writer and reader over # the field table, the compressed content, and every refusal the reader makes. It links the -# same LZO the engine does. -add_executable(SaveTest - "${CMAKE_CURRENT_SOURCE_DIR}/savetest.cpp" - "${CMAKE_SOURCE_DIR}/code/savefile.cpp" +# same vendored LZO library the engine does, and compiles crc.cpp for the checksum the +# header and the content carry. + +opents_add_test(SaveTest + NAME save + SOURCES savetest.cpp + ENGINE + crc.cpp + savefile.cpp + DEFINITIONS WIN32 _WINDOWS _MBCS NOMINMAX + LIBRARIES lzo ) - -target_compile_features(SaveTest PRIVATE cxx_std_20) - -target_include_directories(SaveTest PRIVATE - "${CMAKE_SOURCE_DIR}/code" -) - -target_compile_definitions(SaveTest PRIVATE WIN32 _WINDOWS _MBCS) - -target_compile_options(SaveTest PRIVATE - $<$:/MTd /EHsc /Zc:__cplusplus> - $<$:/MT /EHsc /Zc:__cplusplus> -) - -target_link_libraries(SaveTest PRIVATE kernel32 lzo) - -set_target_properties(SaveTest PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" -) - -# The harness writes the files it reads back, so it is given a directory of its own. -add_test(NAME save COMMAND SaveTest "${CMAKE_CURRENT_BINARY_DIR}") diff --git a/tests/save/savetest.cpp b/tests/save/savetest.cpp index 04fae38a3..24bf41d0e 100644 --- a/tests/save/savetest.cpp +++ b/tests/save/savetest.cpp @@ -3,11 +3,11 @@ // whole, intact save of a version it knows. // // Every file it touches it creates itself, in a scratch directory named by the first -// argument, so it reads no game data and leaves nothing behind. +// argument or the working directory, so it reads no game data and leaves nothing behind. #include "savefile.h" -#include +#include #include #include @@ -70,14 +70,14 @@ static std::vector Prose(std::size_t length) static std::vector Read_Whole_File(char const * path) { std::vector data; - HANDLE const file = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, NULL); + HANDLE const file = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) return(data); - DWORD const size = GetFileSize(file, NULL); + DWORD const size = GetFileSize(file, nullptr); if (size != INVALID_FILE_SIZE && size > 0) { data.resize(size); DWORD got = 0; - if (!ReadFile(file, data.data(), size, &got, NULL) || got != size) data.clear(); + if (!ReadFile(file, data.data(), size, &got, nullptr) || got != size) data.clear(); } CloseHandle(file); return(data); @@ -86,13 +86,13 @@ static std::vector Read_Whole_File(char const * path) static bool Write_Whole_File(char const * path, std::vector const & data) { - HANDLE const file = CreateFileA(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, - FILE_ATTRIBUTE_NORMAL, NULL); + HANDLE const file = CreateFileA(path, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) return(false); DWORD written = 0; bool ok = true; if (!data.empty()) { - ok = WriteFile(file, data.data(), (DWORD)data.size(), &written, NULL) && written == data.size(); + ok = WriteFile(file, data.data(), (DWORD)data.size(), &written, nullptr) && written == data.size(); } CloseHandle(file); return(ok); @@ -101,8 +101,8 @@ static bool Write_Whole_File(char const * path, std::vector const static bool File_Exists(char const * path) { - HANDLE const file = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, NULL); + HANDLE const file = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) return(false); CloseHandle(file); return(true); @@ -394,20 +394,13 @@ static void Test_Refusals(void) Check("refuse: the reference save is compressed", (image[6] & 0x01) != 0); std::vector const stored(image.begin() + content_offset, image.end()); - Write_Whole_File(damaged.c_str(), Forge_Content(image, table, stored, content_length - 1)); - Check_Result("refuse: a block that expands past its declared length", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); - Write_Whole_File(damaged.c_str(), Forge_Content(image, table, stored, 16)); - Check_Result("refuse: a block that expands far past its declared length", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); Write_Whole_File(damaged.c_str(), Forge_Content(image, table, stored, content_length + 1)); Check_Result("refuse: a block that ends before its declared length", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); - std::vector const reaching_back = { 18, 'A', 4, 0 }; - Write_Whole_File(damaged.c_str(), Forge_Content(image, table, reaching_back, 3)); - Check_Result("refuse: a block whose match reaches before the start", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); - - std::vector const unfinished = { 18, 'A' }; - Write_Whole_File(damaged.c_str(), Forge_Content(image, table, unfinished, 1)); - Check_Result("refuse: a block with no end marker", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); + // The reader sizes the output buffer from the declared length, so this block runs past + // the end of it. The bounds-checked decompressor stops there rather than writing on. + Write_Whole_File(damaged.c_str(), Forge_Content(image, table, stored, content_length - 1)); + Check_Result("refuse: a block that expands past its declared length", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); Write_Whole_File(damaged.c_str(), Forge_Content(image, table, stored, 0x10000001)); Check_Result("refuse: a block declared larger than any save", read.Read(damaged.c_str()), SaveFileClass::RESULT_CORRUPT); @@ -465,17 +458,13 @@ static void Test_Refusals(void) int main(int argc, char ** argv) { - if (argc < 2) { - printf("usage: SaveTest \n"); - return(2); - } if (lzo_init() != LZO_E_OK) { printf("lzo_init failed\n"); return(2); } - Scratch = argv[1]; - CreateDirectoryA(Scratch.c_str(), NULL); + Scratch = (argc > 1) ? argv[1] : "."; + CreateDirectoryA(Scratch.c_str(), nullptr); Test_Round_Trip(); Test_Cuts(); @@ -486,7 +475,7 @@ int main(int argc, char ** argv) Test_Refusals(); char const * const names[] = { "ROUNDTRIP.SAV", "NOISE.SAV", "EMPTY.SAV", "REPLACE.SAV", - "PLAIN.SAV", "GOOD.SAV", "DAMAGED.SAV" }; + "LIMITS.SAV", "PLAIN.SAV", "GOOD.SAV", "DAMAGED.SAV" }; for (char const * name : names) { DeleteFileA(Scratch_Path(name).c_str()); } diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index 05a416481..2edbd2dbf 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -239,3 +239,31 @@ add_library(imgui STATIC "${CMAKE_CURRENT_SOURCE_DIR}/imgui/imgui_widgets.cpp" ) target_include_directories(imgui PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/imgui") + +# +# --------------------------------------------------------- +# Solution layout +# --------------------------------------------------------- +# + +# Files a directory's targets, and those of every directory below it, under one solution +# folder. A target that already names a folder keeps it as a subfolder of that one, so the +# grouping bgfx gives its own targets survives. +function(opents_set_solution_folder directory folder) + get_property(targets DIRECTORY "${directory}" PROPERTY BUILDSYSTEM_TARGETS) + foreach(target IN LISTS targets) + get_target_property(current ${target} FOLDER) + if(current) + set_target_properties(${target} PROPERTIES FOLDER "${folder}/${current}") + else() + set_target_properties(${target} PROPERTIES FOLDER "${folder}") + endif() + endforeach() + + get_property(subdirectories DIRECTORY "${directory}" PROPERTY SUBDIRECTORIES) + foreach(subdirectory IN LISTS subdirectories) + opents_set_solution_folder("${subdirectory}" "${folder}") + endforeach() +endfunction() + +opents_set_solution_folder("${CMAKE_CURRENT_SOURCE_DIR}" "Thirdparty")