Subversion Repositories Games.Descent

Rev

Blame | Last modification | View Log | Download | RSS feed

  1. /*
  2.  * Portions of this file are copyright Rebirth contributors and licensed as
  3.  * described in COPYING.txt.
  4.  * Portions of this file are copyright Parallax Software and licensed
  5.  * according to the Parallax license below.
  6.  * See COPYING.txt for license details.
  7.  
  8. THE COMPUTER CODE CONTAINED HEREIN IS THE SOLE PROPERTY OF PARALLAX
  9. SOFTWARE CORPORATION ("PARALLAX").  PARALLAX, IN DISTRIBUTING THE CODE TO
  10. END-USERS, AND SUBJECT TO ALL OF THE TERMS AND CONDITIONS HEREIN, GRANTS A
  11. ROYALTY-FREE, PERPETUAL LICENSE TO SUCH END-USERS FOR USE BY SUCH END-USERS
  12. IN USING, DISPLAYING,  AND CREATING DERIVATIVE WORKS THEREOF, SO LONG AS
  13. SUCH USE, DISPLAY OR CREATION IS FOR NON-COMMERCIAL, ROYALTY OR REVENUE
  14. FREE PURPOSES.  IN NO EVENT SHALL THE END-USER USE THE COMPUTER CODE
  15. CONTAINED HEREIN FOR REVENUE-BEARING PURPOSES.  THE END-USER UNDERSTANDS
  16. AND AGREES TO THE TERMS HEREIN AND ACCEPTS THE SAME BY USE OF THIS FILE.
  17. COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION.  ALL RIGHTS RESERVED.
  18. */
  19.  
  20. /*
  21.  *
  22.  * Code to handle multiple missions
  23.  *
  24.  */
  25.  
  26. #include <algorithm>
  27. #include <vector>
  28. #include <stdio.h>
  29. #include <stdlib.h>
  30. #include <string.h>
  31. #include <ctype.h>
  32. #include <limits.h>
  33.  
  34. #include "pstypes.h"
  35. #include "strutil.h"
  36. #include "inferno.h"
  37. #include "window.h"
  38. #include "mission.h"
  39. #include "gameseq.h"
  40. #include "gamesave.h"
  41. #include "titles.h"
  42. #include "piggy.h"
  43. #include "console.h"
  44. #include "songs.h"
  45. #include "polyobj.h"
  46. #include "dxxerror.h"
  47. #include "config.h"
  48. #include "newmenu.h"
  49. #include "text.h"
  50. #include "u_mem.h"
  51. #include "ignorecase.h"
  52. #include "physfsx.h"
  53. #include "physfs_list.h"
  54. #include "bm.h"
  55. #include "event.h"
  56. #if defined(DXX_BUILD_DESCENT_II)
  57. #include "movie.h"
  58. #endif
  59. #include "null_sentinel_iterator.h"
  60.  
  61. #include "compiler-poison.h"
  62. #include "compiler-range_for.h"
  63. #include "d_enumerate.h"
  64. #include <memory>
  65.  
  66. #define BIMD1_BRIEFING_FILE             "briefing.txb"
  67.  
  68. using std::min;
  69.  
  70. #define MISSION_EXTENSION_DESCENT_I     ".msn"
  71. #if defined(DXX_BUILD_DESCENT_II)
  72. #define MISSION_EXTENSION_DESCENT_II    ".mn2"
  73. #endif
  74.  
  75. #define CON_PRIORITY_DEBUG_MISSION_LOAD CON_DEBUG
  76.  
  77. namespace {
  78.  
  79. using mission_candidate_search_path = std::array<char, PATH_MAX>;
  80.  
  81. }
  82.  
  83. namespace dsx {
  84.  
  85. namespace {
  86.  
  87. struct mle;
  88. using mission_list_type = std::vector<mle>;
  89.  
  90. //mission list entry
  91. struct mle : Mission_path
  92. {
  93.         int     builtin_hogsize;    // if it's the built-in mission, used for determining the version
  94.         ntstring<75> mission_name;
  95. #if defined(DXX_BUILD_DESCENT_II)
  96.         descent_version_type descent_version;    // descent 1 or descent 2?
  97. #endif
  98.         ubyte   anarchy_only_flag;  // if true, mission is anarchy only
  99.         mission_list_type directory;
  100.         mle(Mission_path &&m) :
  101.                 Mission_path(std::move(m))
  102.         {
  103.         }
  104.         mle(const char *const name, std::vector<mle> &&d);
  105. };
  106.  
  107. struct mission_subdir_stats
  108. {
  109.         std::size_t immediate_directories = 0, immediate_missions = 0, total_missions = 0;
  110.         static std::size_t count_missions(const mission_list_type &directory)
  111.         {
  112.                 std::size_t total_missions = 0;
  113.                 range_for (auto &&i, directory)
  114.                 {
  115.                         if (i.directory.empty())
  116.                                 ++ total_missions;
  117.                         else
  118.                                 total_missions += count_missions(i.directory);
  119.                 }
  120.                 return total_missions;
  121.         }
  122.         void count(const mission_list_type &directory)
  123.         {
  124.                 range_for (auto &&i, directory)
  125.                 {
  126.                         if (i.directory.empty())
  127.                         {
  128.                                 ++ total_missions;
  129.                                 ++ immediate_missions;
  130.                         }
  131.                         else
  132.                         {
  133.                                 ++ immediate_directories;
  134.                                 total_missions += count_missions(i.directory);
  135.                         }
  136.                 }
  137.         }
  138. };
  139.  
  140. struct mission_name_and_version
  141. {
  142. #if defined(DXX_BUILD_DESCENT_II)
  143.         const Mission::descent_version_type descent_version = {};
  144. #endif
  145.         char *const name = nullptr;
  146.         mission_name_and_version() = default;
  147.         mission_name_and_version(Mission::descent_version_type, char *);
  148. };
  149.  
  150. mission_name_and_version::mission_name_and_version(Mission::descent_version_type const v, char *const n) :
  151. #if defined(DXX_BUILD_DESCENT_II)
  152.         descent_version(v),
  153. #endif
  154.         name(n)
  155. {
  156. #if defined(DXX_BUILD_DESCENT_I)
  157.         (void)v;
  158. #endif
  159. }
  160.  
  161. const char *prepare_mission_list_count_dirbuf(std::array<char, 12> &dirbuf, const std::size_t immediate_directories)
  162. {
  163.         /* Limit the count of directories to what can be formatted
  164.          * successfully without truncation.  If a user has more than this
  165.          * many directories, an empty string will be used instead of showing
  166.          * the actual count.
  167.          */
  168.         if (immediate_directories && immediate_directories <= 99999)
  169.         {
  170.                 snprintf(dirbuf.data(), dirbuf.size(), "DIR:%zu; ", immediate_directories);
  171.                 return dirbuf.data();
  172.         }
  173.         return "";
  174. }
  175.  
  176. mle::mle(const char *const name, std::vector<mle> &&d) :
  177.         Mission_path(name, 0), directory(std::move(d))
  178. {
  179.         mission_subdir_stats ss;
  180.         ss.count(directory);
  181.         std::array<char, 12> dirbuf;
  182.         snprintf(mission_name.data(), mission_name.size(), "%s/ [%sMSN:L%zu;T%zu]", name, prepare_mission_list_count_dirbuf(dirbuf, ss.immediate_directories), ss.immediate_missions, ss.total_missions);
  183. }
  184.  
  185. static const mle *compare_mission_predicate_to_leaf(const mission_entry_predicate mission_predicate, const mle &candidate, const char *candidate_filesystem_name)
  186. {
  187. #if defined(DXX_BUILD_DESCENT_II)
  188.         if (mission_predicate.check_version && mission_predicate.descent_version != candidate.descent_version)
  189.         {
  190.                 con_printf(CON_PRIORITY_DEBUG_MISSION_LOAD, DXX_STRINGIZE_FL(__FILE__, __LINE__, "mission version check requires %u, but found %u; skipping string comparison for mission \"%s\""), static_cast<unsigned>(mission_predicate.descent_version), static_cast<unsigned>(candidate.descent_version), candidate.path.data());
  191.                 return nullptr;
  192.         }
  193. #endif
  194.         if (!d_stricmp(mission_predicate.filesystem_name, candidate_filesystem_name))
  195.         {
  196.                 con_printf(CON_PRIORITY_DEBUG_MISSION_LOAD, DXX_STRINGIZE_FL(__FILE__, __LINE__, "found mission \"%s\"[\"%s\"] at %p"), candidate.path.data(), &*candidate.filename, &candidate);
  197.                 return &candidate;
  198.         }
  199.         con_printf(CON_PRIORITY_DEBUG_MISSION_LOAD, DXX_STRINGIZE_FL(__FILE__, __LINE__, "want mission \"%s\", no match for mission \"%s\"[\"%s\"] at %p"), mission_predicate.filesystem_name, candidate.path.data(), &*candidate.filename, &candidate);
  200.         return nullptr;
  201. }
  202.  
  203. static const mle *compare_mission_by_guess(const mission_entry_predicate mission_predicate, const mle &candidate)
  204. {
  205.         if (candidate.directory.empty())
  206.                 return compare_mission_predicate_to_leaf(mission_predicate, candidate, &*candidate.filename);
  207.         {
  208.                 const unsigned long size = candidate.directory.size();
  209.                 con_printf(CON_PRIORITY_DEBUG_MISSION_LOAD, DXX_STRINGIZE_FL(__FILE__, __LINE__, "want mission \"%s\", check %lu missions under \"%s\""), mission_predicate.filesystem_name, size, candidate.path.data());
  210.         }
  211.         range_for (auto &i, candidate.directory)
  212.         {
  213.                 if (const auto r = compare_mission_by_guess(mission_predicate, i))
  214.                         return r;
  215.         }
  216.         con_printf(CON_PRIORITY_DEBUG_MISSION_LOAD, DXX_STRINGIZE_FL(__FILE__, __LINE__, "no matches under \"%s\""), candidate.path.data());
  217.         return nullptr;
  218. }
  219.  
  220. static const mle *compare_mission_by_pathname(const mission_entry_predicate mission_predicate, const mle &candidate)
  221. {
  222.         if (candidate.directory.empty())
  223.                 return compare_mission_predicate_to_leaf(mission_predicate, candidate, candidate.path.data());
  224.         const auto mission_name = mission_predicate.filesystem_name;
  225.         const auto path_length = candidate.path.size();
  226.         if (!strncmp(mission_name, candidate.path.data(), path_length) && mission_name[path_length] == '/')
  227.         {
  228.                 {
  229.                         const unsigned long size = candidate.directory.size();
  230.                         con_printf(CON_PRIORITY_DEBUG_MISSION_LOAD, DXX_STRINGIZE_FL(__FILE__, __LINE__, "want mission pathname \"%s\", check %lu missions under \"%s\""), mission_predicate.filesystem_name, size, candidate.path.data());
  231.                 }
  232.                 range_for (auto &i, candidate.directory)
  233.                 {
  234.                         if (const auto r = compare_mission_by_pathname(mission_predicate, i))
  235.                                 return r;
  236.                 }
  237.                 con_printf(CON_PRIORITY_DEBUG_MISSION_LOAD, DXX_STRINGIZE_FL(__FILE__, __LINE__, "no matches under \"%s\""), candidate.path.data());
  238.         }
  239.         else
  240.                 con_printf(CON_PRIORITY_DEBUG_MISSION_LOAD, DXX_STRINGIZE_FL(__FILE__, __LINE__, "want mission pathname \"%s\", ignore non-matching directory \"%s\""), mission_predicate.filesystem_name, candidate.path.data());
  241.         return nullptr;
  242. }
  243.  
  244. }
  245.  
  246. }
  247.  
  248. Mission_ptr Current_mission; // currently loaded mission
  249.  
  250. static bool null_or_space(char c)
  251. {
  252.         return !c || isspace(static_cast<unsigned>(c));
  253. }
  254.  
  255. // Allocate the Level_names, Secret_level_names and Secret_level_table arrays
  256. static int allocate_levels(void)
  257. {
  258.         Level_names = std::make_unique<d_fname[]>(Last_level);
  259.         if (Last_secret_level)
  260.         {
  261.                 N_secret_levels = -Last_secret_level;
  262.                 Secret_level_names = std::make_unique<d_fname[]>(N_secret_levels);
  263.                 Secret_level_table = std::make_unique<ubyte[]>(N_secret_levels);
  264.         }
  265.        
  266.         return 1;
  267. }
  268.  
  269. //
  270. //  Special versions of mission routines for d1 builtins
  271. //
  272.  
  273. static const char *load_mission_d1()
  274. {
  275.         switch (PHYSFSX_fsize("descent.hog"))
  276.         {
  277.                 case D1_SHAREWARE_MISSION_HOGSIZE:
  278.                 case D1_SHAREWARE_10_MISSION_HOGSIZE:
  279.                         N_secret_levels = 0;
  280.        
  281.                         Last_level = 7;
  282.                         Last_secret_level = 0;
  283.                        
  284.                         if (!allocate_levels())
  285.                         {
  286.                                 Current_mission.reset();
  287.                                 return "Failed to allocate level memory for Descent 1 shareware";
  288.                         }
  289.        
  290.                         //build level names
  291.                         for (int i=0;i<Last_level;i++)
  292.                                 snprintf(&Level_names[i][0u], Level_names[i].size(), "level%02d.sdl", i+1);
  293.                         Briefing_text_filename = BIMD1_BRIEFING_FILE;
  294.                         Ending_text_filename = BIMD1_ENDING_FILE_SHARE;
  295.                         break;
  296.                 case D1_MAC_SHARE_MISSION_HOGSIZE:
  297.                         N_secret_levels = 0;
  298.        
  299.                         Last_level = 3;
  300.                         Last_secret_level = 0;
  301.        
  302.                         if (!allocate_levels())
  303.                         {
  304.                                 Current_mission.reset();
  305.                                 return "Failed to allocate level memory for Descent 1 Mac shareware";
  306.                         }
  307.                        
  308.                         //build level names
  309.                         for (int i=0;i<Last_level;i++)
  310.                                 snprintf(&Level_names[i][0u], Level_names[i].size(), "level%02d.sdl", i+1);
  311.                         Briefing_text_filename = BIMD1_BRIEFING_FILE;
  312.                         Ending_text_filename = BIMD1_ENDING_FILE_SHARE;
  313.                         break;
  314.                 case D1_OEM_MISSION_HOGSIZE:
  315.                 case D1_OEM_10_MISSION_HOGSIZE:
  316.                         {
  317.                         N_secret_levels = 1;
  318.        
  319.                         constexpr unsigned last_level = 15;
  320.                         constexpr int last_secret_level = -1;
  321.                         Last_level = last_level;
  322.                         Last_secret_level = last_secret_level;
  323.        
  324.                         if (!allocate_levels())
  325.                         {
  326.                                 Current_mission.reset();
  327.                                 return "Failed to allocate level memory for Descent 1 OEM";
  328.                         }
  329.                        
  330.                         //build level names
  331.                         for (unsigned i = 0; i < last_level - 1; ++i)
  332.                         {
  333.                                 auto &ln = Level_names[i];
  334.                                 snprintf(&ln[0u], ln.size(), "level%02u.rdl", i + 1);
  335.                         }
  336.                         {
  337.                                 auto &ln = Level_names[last_level - 1];
  338.                                 snprintf(&ln[0u], ln.size(), "saturn%02d.rdl", last_level);
  339.                         }
  340.                         for (int i = 0; i < -last_secret_level; ++i)
  341.                         {
  342.                                 auto &sn = Secret_level_names[i];
  343.                                 snprintf(&sn[0u], sn.size(), "levels%1d.rdl", i + 1);
  344.                         }
  345.                         Secret_level_table[0] = 10;
  346.                         Briefing_text_filename = "briefsat.txb";
  347.                         Ending_text_filename = BIMD1_ENDING_FILE_OEM;
  348.                         }
  349.                         break;
  350.                 default:
  351.                         Int3();
  352.                         DXX_BOOST_FALLTHROUGH;
  353.                 case D1_MISSION_HOGSIZE:
  354.                 case D1_MISSION_HOGSIZE2:
  355.                 case D1_10_MISSION_HOGSIZE:
  356.                 case D1_MAC_MISSION_HOGSIZE:
  357.                         {
  358.                         N_secret_levels = 3;
  359.        
  360.                         constexpr unsigned last_level = BIMD1_LAST_LEVEL;
  361.                         constexpr int last_secret_level = BIMD1_LAST_SECRET_LEVEL;
  362.                         Last_level = last_level;
  363.                         Last_secret_level = last_secret_level;
  364.        
  365.                         if (!allocate_levels())
  366.                         {
  367.                                 Current_mission.reset();
  368.                                 return "Failed to allocate level memory for Descent 1";
  369.                         }
  370.  
  371.                         //build level names
  372.                         for (unsigned i = 0; i < last_level; ++i)
  373.                         {
  374.                                 auto &ln = Level_names[i];
  375.                                 snprintf(&ln[0u], ln.size(), "level%02u.rdl", i + 1);
  376.                         }
  377.                         for (int i = 0; i < -last_secret_level; ++i)
  378.                         {
  379.                                 auto &sn = Secret_level_names[i];
  380.                                 snprintf(&sn[0u], sn.size(), "levels%1d.rdl", i + 1);
  381.                         }
  382.                         Secret_level_table[0] = 10;
  383.                         Secret_level_table[1] = 21;
  384.                         Secret_level_table[2] = 24;
  385.                         Briefing_text_filename = BIMD1_BRIEFING_FILE;
  386.                         Ending_text_filename = "endreg.txb";
  387.                         break;
  388.                         }
  389.         }
  390.         return nullptr;
  391. }
  392.  
  393. #if defined(DXX_BUILD_DESCENT_II)
  394. //
  395. //  Special versions of mission routines for shareware
  396. //
  397.  
  398. static const char *load_mission_shareware()
  399. {
  400.     Current_mission->mission_name.copy_if(SHAREWARE_MISSION_NAME);
  401.     Current_mission->descent_version = Mission::descent_version_type::descent2;
  402.     Current_mission->anarchy_only_flag = 0;
  403.    
  404.     switch (Current_mission->builtin_hogsize)
  405.         {
  406.                 case MAC_SHARE_MISSION_HOGSIZE:
  407.                         N_secret_levels = 1;
  408.  
  409.                         Last_level = 4;
  410.                         Last_secret_level = -1;
  411.  
  412.                         if (!allocate_levels())
  413.                         {
  414.                                 Current_mission.reset();
  415.                                 return "Failed to allocate level memory for Descent 2 Mac shareware";
  416.                         }
  417.                        
  418.                         // mac demo is using the regular hog and rl2 files
  419.                         Level_names[0] = "d2leva-1.rl2";
  420.                         Level_names[1] = "d2leva-2.rl2";
  421.                         Level_names[2] = "d2leva-3.rl2";
  422.                         Level_names[3] = "d2leva-4.rl2";
  423.                         Secret_level_names[0] = "d2leva-s.rl2";
  424.                         break;
  425.                 default:
  426.                         Int3();
  427.                         DXX_BOOST_FALLTHROUGH;
  428.                 case SHAREWARE_MISSION_HOGSIZE:
  429.                         N_secret_levels = 0;
  430.  
  431.                         Last_level = 3;
  432.                         Last_secret_level = 0;
  433.  
  434.                         if (!allocate_levels())
  435.                         {
  436.                                 Current_mission.reset();
  437.                                 return "Failed to allocate level memory for Descent 2 shareware";
  438.                         }
  439.                         Level_names[0] = "d2leva-1.sl2";
  440.                         Level_names[1] = "d2leva-2.sl2";
  441.                         Level_names[2] = "d2leva-3.sl2";
  442.         }
  443.         return nullptr;
  444. }
  445.  
  446.  
  447. //
  448. //  Special versions of mission routines for Diamond/S3 version
  449. //
  450.  
  451. static const char *load_mission_oem()
  452. {
  453.     Current_mission->mission_name.copy_if(OEM_MISSION_NAME);
  454.     Current_mission->descent_version = Mission::descent_version_type::descent2;
  455.     Current_mission->anarchy_only_flag = 0;
  456.    
  457.         N_secret_levels = 2;
  458.  
  459.         Last_level = 8;
  460.         Last_secret_level = -2;
  461.  
  462.         if (!allocate_levels())
  463.         {
  464.                 Current_mission.reset();
  465.                 return "Failed to allocate level memory for Descent 2 OEM";
  466.         }
  467.         Level_names[0] = "d2leva-1.rl2";
  468.         Level_names[1] = "d2leva-2.rl2";
  469.         Level_names[2] = "d2leva-3.rl2";
  470.         Level_names[3] = "d2leva-4.rl2";
  471.         Secret_level_names[0] = "d2leva-s.rl2";
  472.         Level_names[4] = "d2levb-1.rl2";
  473.         Level_names[5] = "d2levb-2.rl2";
  474.         Level_names[6] = "d2levb-3.rl2";
  475.         Level_names[7] = "d2levb-4.rl2";
  476.         Secret_level_names[1] = "d2levb-s.rl2";
  477.         Secret_level_table[0] = 1;
  478.         Secret_level_table[1] = 5;
  479.         return nullptr;
  480. }
  481. #endif
  482.  
  483. //compare a string for a token. returns true if match
  484. static int istok(const char *buf,const char *tok)
  485. {
  486.         return d_strnicmp(buf,tok,strlen(tok)) == 0;
  487. }
  488.  
  489. //returns ptr to string after '=' & white space, or NULL if no '='
  490. //adds 0 after parm at first white space
  491. static char *get_value(char *buf)
  492. {
  493.         char *t = strchr(buf,'=');
  494.  
  495.         if (t) {
  496.                 while (isspace(static_cast<unsigned>(*++t)));
  497.  
  498.                 if (*t)
  499.                         return t;
  500.         }
  501.  
  502.         return NULL;            //error!
  503. }
  504.  
  505. static mission_name_and_version get_any_mission_type_name_value(PHYSFSX_gets_line_t<80> &buf, PHYSFS_File *const f, const Mission::descent_version_type descent_version)
  506. {
  507.         if (!PHYSFSX_fgets(buf,f))
  508.                 return {};
  509.         if (istok(buf, "name"))
  510.                 return {descent_version, get_value(buf)};
  511. #if defined(DXX_BUILD_DESCENT_II)
  512.         if (descent_version == Mission::descent_version_type::descent1)
  513.                 /* If reading a Descent 1 `.msn` file, do not check for the
  514.                  * extended mission types.  D1X-Rebirth would ignore them, so
  515.                  * D2X-Rebirth should also ignore them.
  516.                  */
  517.                 return {};
  518.         struct name_type_pair
  519.         {
  520.                 /* std::pair cannot be used here because direct initialization
  521.                  * from a string literal fails to compile.
  522.                  */
  523.                 char name[7];
  524.                 Mission::descent_version_type descent_version;
  525.         };
  526.         static constexpr name_type_pair mission_name_type_values[] = {
  527.                 {"xname", Mission::descent_version_type::descent2x},    // enhanced mission
  528.                 {"zname", Mission::descent_version_type::descent2z},    // super-enhanced mission
  529.                 {"!name", Mission::descent_version_type::descent2a},    // extensible-enhanced mission
  530.         };
  531.         range_for (const auto &parm, mission_name_type_values)
  532.         {
  533.                 if (istok(buf, parm.name))
  534.                         return {parm.descent_version, get_value(buf)};
  535.         }
  536. #endif
  537.         return {};
  538. }
  539.  
  540. static bool ml_sort_func(const mle &e0,const mle &e1)
  541. {
  542.         const auto d0 = e0.directory.empty();
  543.         const auto d1 = e1.directory.empty();
  544.         if (d0 != d1)
  545.                 /* If d0 is a directory and d1 is a mission, or if d0 is a
  546.                  * mission and d1 is a directory, then apply a special case.
  547.                  *
  548.                  * Consider d0 to be less (and therefore ordered earlier) if d1
  549.                  * is a mission.  This moves directories to the top of the list.
  550.                  */
  551.                 return d1;
  552.         /* If both d0 and d1 are directories, or if both are missions, then
  553.          * apply the usual sorting rule.  This makes directories sort
  554.          * as usual relative to each other.
  555.          */
  556.         return d_stricmp(e0.mission_name,e1.mission_name) < 0;
  557. }
  558.  
  559. //returns 1 if file read ok, else 0
  560. namespace dsx {
  561. static int read_mission_file(mission_list_type &mission_list, mission_candidate_search_path &pathname)
  562. {
  563.         if (const auto mfile = PHYSFSX_openReadBuffered(pathname.data()))
  564.         {
  565.                 std::string str_pathname = pathname.data();
  566.                 const auto idx_last_slash = str_pathname.find_last_of('/');
  567.                 const auto idx_filename = (idx_last_slash == str_pathname.npos) ? 0 : idx_last_slash + 1;
  568.                 const auto idx_file_extension = str_pathname.find_first_of('.', idx_filename);
  569.                 if (idx_file_extension == str_pathname.npos)
  570.                         return 0;       //missing extension
  571.                 if (idx_file_extension >= DXX_MAX_MISSION_PATH_LENGTH)
  572.                         return 0;       // path too long, would be truncated in save game files
  573.                 str_pathname.resize(idx_file_extension);
  574.                 mission_list.emplace_back(Mission_path(std::move(str_pathname), idx_filename));
  575.                 mle *mission = &mission_list.back();
  576. #if defined(DXX_BUILD_DESCENT_I)
  577.                 constexpr auto descent_version = Mission::descent_version_type::descent1;
  578. #elif defined(DXX_BUILD_DESCENT_II)
  579.                 // look if it's .mn2 or .msn
  580.                 auto descent_version = (pathname[idx_file_extension + 3] == MISSION_EXTENSION_DESCENT_II[3])
  581.                         ? Mission::descent_version_type::descent2
  582.                         : Mission::descent_version_type::descent1;
  583. #endif
  584.                 mission->anarchy_only_flag = 0;
  585.  
  586.                 PHYSFSX_gets_line_t<80> buf;
  587.                 const auto &&nv = get_any_mission_type_name_value(buf, mfile, descent_version);
  588.  
  589.                 if (const auto p = nv.name) {
  590. #if defined(DXX_BUILD_DESCENT_II)
  591.                         mission->descent_version = nv.descent_version;
  592. #endif
  593.                         char *t;
  594.                         if ((t=strchr(p,';'))!=NULL)
  595.                         {
  596.                                 *t=0;
  597.                                 --t;
  598.                         }
  599.                         else
  600.                                 t = p + strlen(p) - 1;
  601.                         while (isspace(static_cast<unsigned>(*t)))
  602.                                 *t-- = 0; // remove trailing whitespace
  603.                         mission->mission_name.copy_if(p, mission->mission_name.size() - 1);
  604.                 }
  605.                 else {
  606.                         mission_list.pop_back();
  607.                         return 0;
  608.                 }
  609.  
  610.                 {
  611.                         PHYSFSX_gets_line_t<4096> temp;
  612.                 if (PHYSFSX_fgets(temp,mfile))
  613.                 {
  614.                         if (istok(temp,"type"))
  615.                         {
  616.                                 const auto p = get_value(temp);
  617.                                 //get mission type
  618.                                 if (p)
  619.                                         mission->anarchy_only_flag = istok(p,"anarchy");
  620.                         }
  621.                 }
  622.                 }
  623.                 return 1;
  624.         }
  625.  
  626.         return 0;
  627. }
  628. }
  629.  
  630. namespace dsx {
  631. static void add_d1_builtin_mission_to_list(mission_list_type &mission_list)
  632. {
  633.     int size;
  634.    
  635.         size = PHYSFSX_fsize("descent.hog");
  636.         if (size == -1)
  637.                 return;
  638.  
  639.         mission_list.emplace_back(Mission_path(D1_MISSION_FILENAME, 0));
  640.         mle *mission = &mission_list.back();
  641.         switch (size) {
  642.         case D1_SHAREWARE_MISSION_HOGSIZE:
  643.         case D1_SHAREWARE_10_MISSION_HOGSIZE:
  644.         case D1_MAC_SHARE_MISSION_HOGSIZE:
  645.                 mission->mission_name.copy_if(D1_SHAREWARE_MISSION_NAME);
  646.                 mission->anarchy_only_flag = 0;
  647.                 break;
  648.         case D1_OEM_MISSION_HOGSIZE:
  649.         case D1_OEM_10_MISSION_HOGSIZE:
  650.                 mission->mission_name.copy_if(D1_OEM_MISSION_NAME);
  651.                 mission->anarchy_only_flag = 0;
  652.                 break;
  653.         default:
  654.                 Warning("Unknown D1 hogsize %d\n", size);
  655.                 Int3();
  656.                 DXX_BOOST_FALLTHROUGH;
  657.         case D1_MISSION_HOGSIZE:
  658.         case D1_MISSION_HOGSIZE2:
  659.         case D1_10_MISSION_HOGSIZE:
  660.         case D1_MAC_MISSION_HOGSIZE:
  661.                 mission->mission_name.copy_if(D1_MISSION_NAME);
  662.                 mission->anarchy_only_flag = 0;
  663.                 break;
  664.         }
  665.  
  666.         mission->anarchy_only_flag = 0;
  667. #if defined(DXX_BUILD_DESCENT_I)
  668.         mission->builtin_hogsize = size;
  669. #elif defined(DXX_BUILD_DESCENT_II)
  670.         mission->descent_version = Mission::descent_version_type::descent1;
  671.         mission->builtin_hogsize = 0;
  672. #endif
  673. }
  674. }
  675.  
  676. #if defined(DXX_BUILD_DESCENT_II)
  677. template <std::size_t N1, std::size_t N2>
  678. static void set_hardcoded_mission(mission_list_type &mission_list, const char (&path)[N1], const char (&mission_name)[N2])
  679. {
  680.         mission_list.emplace_back(Mission_path(path, 0));
  681.         mle *mission = &mission_list.back();
  682.         mission->mission_name.copy_if(mission_name);
  683.         mission->anarchy_only_flag = 0;
  684. }
  685.  
  686. static void add_builtin_mission_to_list(mission_list_type &mission_list, d_fname &name)
  687. {
  688.     int size = PHYSFSX_fsize("descent2.hog");
  689.    
  690.         if (size == -1)
  691.                 size = PHYSFSX_fsize("d2demo.hog");
  692.  
  693.         switch (size) {
  694.         case SHAREWARE_MISSION_HOGSIZE:
  695.         case MAC_SHARE_MISSION_HOGSIZE:
  696.                 set_hardcoded_mission(mission_list, SHAREWARE_MISSION_FILENAME, SHAREWARE_MISSION_NAME);
  697.                 break;
  698.         case OEM_MISSION_HOGSIZE:
  699.                 set_hardcoded_mission(mission_list, OEM_MISSION_FILENAME, OEM_MISSION_NAME);
  700.                 break;
  701.         default:
  702.                 Warning("Unknown hogsize %d, trying %s\n", size, FULL_MISSION_FILENAME MISSION_EXTENSION_DESCENT_II);
  703.                 Int3();
  704.                 DXX_BOOST_FALLTHROUGH;
  705.         case FULL_MISSION_HOGSIZE:
  706.         case FULL_10_MISSION_HOGSIZE:
  707.         case MAC_FULL_MISSION_HOGSIZE:
  708.                 {
  709.                         mission_candidate_search_path full_mission_filename = {{FULL_MISSION_FILENAME MISSION_EXTENSION_DESCENT_II}};
  710.                         if (!read_mission_file(mission_list, full_mission_filename))
  711.                                 Error("Could not find required mission file <%s>", FULL_MISSION_FILENAME MISSION_EXTENSION_DESCENT_II);
  712.                 }
  713.         }
  714.  
  715.         mle *mission = &mission_list.back();
  716.         name.copy_if(mission->path.c_str(), FILENAME_LEN);
  717.     mission->builtin_hogsize = size;
  718.         mission->descent_version = Mission::descent_version_type::descent2;
  719.         mission->anarchy_only_flag = 0;
  720. }
  721. #endif
  722.  
  723. namespace dsx {
  724.  
  725. static void add_missions_to_list(mission_list_type &mission_list, mission_candidate_search_path &path, const mission_candidate_search_path::iterator rel_path, const mission_filter_mode mission_filter)
  726. {
  727.         /* rel_path must point within the array `path`.
  728.          * rel_path must point to the null that follows a possibly empty
  729.          * directory prefix.
  730.          * If the directory prefix is not empty, it must end with a PHYSFS
  731.          * path separator, which is always slash, even on Windows.
  732.          *
  733.          * If any of these assertions fail, then the path transforms used to
  734.          * recurse into subdirectories and to open individual missions will
  735.          * not work correctly.
  736.          */
  737.         assert(std::distance(path.begin(), rel_path) < path.size() - 1);
  738.         assert(!*rel_path);
  739.         assert(path.begin() == rel_path || *std::prev(rel_path) == '/');
  740.         const std::size_t space_remaining = std::distance(rel_path, path.end());
  741.         *rel_path = '.';
  742.         *std::next(rel_path) = 0;
  743.         range_for (const auto i, PHYSFSX_uncounted_list{PHYSFS_enumerateFiles(path.data())})
  744.         {
  745.                 /* Add 1 to include the terminating null. */
  746.                 const std::size_t il = strlen(i) + 1;
  747.                 /* Add 2 for the slash+dot in case it is a directory. */
  748.                 if (il + 2 >= space_remaining)
  749.                         continue;       // path is too long
  750.  
  751.                 auto j = std::copy_n(i, il, rel_path);
  752.                 const char *ext;
  753.             PHYSFS_Stat statbuf; // Pierre-Marie Baty -- work around PHYSFS_isDirectory() deprecation
  754.                 //if (/*PHYSFS_*/isDirectory(path.data())) // Pierre-Marie Baty -- work around PHYSFS_isDirectory() deprecation
  755.             if (PHYSFS_stat(path.data(), &statbuf) && (statbuf.filetype == PHYSFS_FILETYPE_DIRECTORY)) // Pierre-Marie Baty -- work around PHYSFS_isDirectory() deprecation
  756.                 {
  757.                         const auto null = std::prev(j);
  758.                         *j = 0;
  759.                         *null = '/';
  760.                         mission_list_type sublist;
  761.                         add_missions_to_list(sublist, path, j, mission_filter);
  762.                         *null = 0;
  763.                         const auto found = sublist.size();
  764.                         if (!found)
  765.                         {
  766.                                 /* Ignore empty directories */
  767.                         }
  768.                         else if (found == 1)
  769.                         {
  770.                                 /* If only one found, promote it up to the next level so
  771.                                  * the user does not need to navigate into a
  772.                                  * single-element directory.
  773.                                  */
  774.                                 auto &sli = sublist.front();
  775.                                 mission_list.emplace_back(std::move(sli));
  776.                         }
  777.                         else
  778.                         {
  779.                                 std::sort(sublist.begin(), sublist.end(), ml_sort_func);
  780.                                 mission_list.emplace_back(path.data(), std::move(sublist));
  781.                         }
  782.                 }
  783.                 else if (il > 5 &&
  784.                         ((ext = &i[il - 5], !d_strnicmp(ext, MISSION_EXTENSION_DESCENT_I))
  785. #if defined(DXX_BUILD_DESCENT_II)
  786.                                 || !d_strnicmp(ext, MISSION_EXTENSION_DESCENT_II)
  787. #endif
  788.                         ))
  789.                         if (read_mission_file(mission_list, path))
  790.                         {
  791.                                 if (mission_filter != mission_filter_mode::exclude_anarchy || !mission_list.back().anarchy_only_flag)
  792.                                 {
  793.                                         mission_list.back().builtin_hogsize = 0;
  794.                                 }
  795.                                 else
  796.                                         mission_list.pop_back();
  797.                         }
  798.                
  799.                 if (mission_list.size() >= MAX_MISSIONS)
  800.                 {
  801.                         break;
  802.                 }
  803.                 *rel_path = 0;  // chop off the entry
  804.                 DXX_POISON_MEMORY(std::next(rel_path), path.end(), 0xcc);
  805.         }
  806. }
  807. }
  808.  
  809. /* move <mission_name> to <place> on mission list, increment <place> */
  810. static void promote (mission_list_type &mission_list, const char *const name, std::size_t &top_place)
  811. {
  812.         range_for (auto &i, partial_range(mission_list, top_place, mission_list.size()))
  813.                 if (!d_stricmp(&*i.filename, name)) {
  814.                         //swap mission positions
  815.                         auto &j = mission_list[top_place++];
  816.                         if (&j != &i)
  817.                                 std::swap(j, i);
  818.                         break;
  819.                 }
  820. }
  821.  
  822. Mission::~Mission()
  823. {
  824.     // May become more complex with the editor
  825.         if (!path.empty() && builtin_hogsize == 0)
  826.                 {
  827.                         char hogpath[PATH_MAX];
  828.                         snprintf(hogpath, sizeof(hogpath), "%s.hog", path.c_str());
  829.                         PHYSFSX_removeRelFromSearchPath(hogpath);
  830.                 }
  831. }
  832.  
  833.  
  834.  
  835. //fills in the global list of missions.  Returns the number of missions
  836. //in the list.  If anarchy_mode is set, then also add anarchy-only missions.
  837.  
  838. namespace dsx {
  839.  
  840. static mission_list_type build_mission_list(const mission_filter_mode mission_filter)
  841. {
  842.         //now search for levels on disk
  843.  
  844. //@@Took out this code because after this routine was called once for
  845. //@@a list of single-player missions, a subsequent call for a list of
  846. //@@anarchy missions would not scan again, and thus would not find the
  847. //@@anarchy-only missions.  If we retain the minimum level of install,
  848. //@@we may want to put the code back in, having it always scan for all
  849. //@@missions, and have the code that uses it sort out the ones it wants.
  850. //@@    if (num_missions != -1) {
  851. //@@            if (Current_mission_num != 0)
  852. //@@                    load_mission(0);                                //set built-in mission as default
  853. //@@            return num_missions;
  854. //@@    }
  855.  
  856.         mission_list_type mission_list;
  857.        
  858. #if defined(DXX_BUILD_DESCENT_II)
  859.         d_fname builtin_mission_filename;
  860.         add_builtin_mission_to_list(mission_list, builtin_mission_filename);  //read built-in first
  861. #endif
  862.         add_d1_builtin_mission_to_list(mission_list);
  863.         mission_candidate_search_path search_str = {{MISSION_DIR}};
  864.         DXX_POISON_MEMORY(std::next(search_str.begin(), sizeof(MISSION_DIR)), search_str.end(), 0xcc);
  865.         add_missions_to_list(mission_list, search_str, search_str.begin() + sizeof(MISSION_DIR) - 1, mission_filter);
  866.        
  867.         // move original missions (in story-chronological order)
  868.         // to top of mission list
  869.         std::size_t top_place = 0;
  870.         promote(mission_list, D1_MISSION_FILENAME, top_place); // original descent 1 mission
  871. #if defined(DXX_BUILD_DESCENT_II)
  872.         promote(mission_list, builtin_mission_filename, top_place); // d2 or d2demo
  873.         promote(mission_list, "d2x", top_place); // vertigo
  874. #endif
  875.  
  876.         if (mission_list.size() > top_place)
  877.                 std::sort(next(begin(mission_list), top_place), end(mission_list), ml_sort_func);
  878.         return mission_list;
  879. }
  880.  
  881. #if defined(DXX_BUILD_DESCENT_II)
  882. //values for built-in mission
  883.  
  884. int load_mission_ham()
  885. {
  886.         read_hamfile(); // intentionally can also read from the HOG
  887.  
  888.         if (Piggy_hamfile_version >= 3)
  889.         {
  890.                 // re-read sounds in case mission has custom .sXX
  891.                 Num_sound_files = 0;
  892.                 read_sndfile();
  893.                 piggy_read_sounds();
  894.         }
  895.  
  896.         if (Current_mission->descent_version == Mission::descent_version_type::descent2a &&
  897.                 Current_mission->alternate_ham_file)
  898.         {
  899.                 /*
  900.                  * If an alternate HAM is specified, map a HOG of the same name
  901.                  * (if it exists) so that users can reference a HAM within a
  902.                  * HOG.  This is required to let users reference the D2X.HAM
  903.                  * file provided by Descent II: Vertigo.
  904.                  *
  905.                  * Try both plain NAME and missions/NAME, in that order.
  906.                  */
  907.                 auto &altham = Current_mission->alternate_ham_file;
  908.                 unsigned l = strlen(*altham);
  909.                 char althog[PATH_MAX];
  910.                 snprintf(althog, sizeof(althog), MISSION_DIR "%.*s.hog", l - 4, static_cast<const char *>(*altham));
  911.                 char *p = althog + sizeof(MISSION_DIR) - 1;
  912.                 int exists = PHYSFSX_contfile_init(p, 0);
  913.                 if (!exists) {
  914.                         exists = PHYSFSX_contfile_init(p = althog, 0);
  915.                 }
  916.                 bm_read_extra_robots(*altham, Mission::descent_version_type::descent2z);
  917.                 if (exists)
  918.                         PHYSFSX_contfile_close(p);
  919.                 return 1;
  920.         }
  921.         else if (Current_mission->descent_version == Mission::descent_version_type::descent2a ||
  922.                                 Current_mission->descent_version == Mission::descent_version_type::descent2z ||
  923.                                 Current_mission->descent_version == Mission::descent_version_type::descent2x)
  924.         {
  925.                 char t[50];
  926.                 snprintf(t,sizeof(t), "%s.ham", &*Current_mission->filename);
  927.                 bm_read_extra_robots(t, Current_mission->descent_version);
  928.                 return 1;
  929.         } else
  930.                 return 0;
  931. }
  932. #endif
  933. }
  934.  
  935. #define tex ".tex"
  936. static void set_briefing_filename(d_fname &f, const char *const v, std::size_t d)
  937. {
  938.         f.copy_if(v, d);
  939.         f.copy_if(d, tex);
  940.         if (!PHYSFSX_exists(static_cast<const char *>(f), 1) && !(f.copy_if(++d, "txb"), PHYSFSX_exists(static_cast<const char *>(f), 1))) // check if this file exists ...
  941.                 f = {};
  942. }
  943.  
  944. static void set_briefing_filename(d_fname &f, const char *const v)
  945. {
  946.         using std::next;
  947.         auto a = [](char c) {
  948.                 return !c || c == '.';
  949.         };
  950.         auto i = std::find_if(v, next(v, f.size() - sizeof(tex)), a);
  951.         std::size_t d = std::distance(v, i);
  952.         set_briefing_filename(f, v, d);
  953. }
  954.  
  955. static void record_briefing(d_fname &f, std::array<char, PATH_MAX> &buf)
  956. {
  957.         const auto v = get_value(buf.data());
  958.         if (!v)
  959.                 return;
  960.         const std::size_t d = std::distance(v, std::find_if(v, buf.end(), null_or_space));
  961.         if (d >= FILENAME_LEN)
  962.                 return;
  963.         {
  964.                 set_briefing_filename(f, v, std::min(d, f.size() - sizeof(tex)));
  965.         }
  966. }
  967. #undef tex
  968.  
  969. //loads the specfied mission from the mission list.
  970. //build_mission_list() must have been called.
  971. //Returns true if mission loaded ok, else false.
  972. namespace dsx {
  973.  
  974. static const char *load_mission(const mle *const mission)
  975. {
  976.         char *v;
  977.  
  978. #if defined(DXX_BUILD_DESCENT_II)
  979.         close_extra_robot_movie();
  980. #endif
  981.         Current_mission = std::make_unique<Mission>(static_cast<const Mission_path &>(*mission));
  982.         Current_mission->builtin_hogsize = mission->builtin_hogsize;
  983.         Current_mission->mission_name.copy_if(mission->mission_name);
  984. #if defined(DXX_BUILD_DESCENT_II)
  985.         Current_mission->descent_version = mission->descent_version;
  986. #endif
  987.         Current_mission->anarchy_only_flag = mission->anarchy_only_flag;
  988.         Current_mission->n_secret_levels = 0;
  989. #if defined(DXX_BUILD_DESCENT_II)
  990.         Current_mission->alternate_ham_file = NULL;
  991. #endif
  992.  
  993.         //init vars
  994.         Last_level = 0;
  995.         Last_secret_level = 0;
  996.         Briefing_text_filename = {};
  997.         Ending_text_filename = {};
  998.         Secret_level_table.reset();
  999.         Level_names.reset();
  1000.         Secret_level_names.reset();
  1001.  
  1002.         // for Descent 1 missions, load descent.hog
  1003. #if defined(DXX_BUILD_DESCENT_II)
  1004.         if (EMULATING_D1)
  1005. #endif
  1006.         {
  1007.                 if (!PHYSFSX_contfile_init("descent.hog", 0))
  1008. #if defined(DXX_BUILD_DESCENT_I)
  1009.                         Error("descent.hog not available!\n");
  1010. #elif defined(DXX_BUILD_DESCENT_II)
  1011.                         Warning("descent.hog not available, this mission may be missing some files required for briefings and exit sequence\n");
  1012. #endif
  1013.                 if (!d_stricmp(Current_mission->path.c_str(), D1_MISSION_FILENAME))
  1014.                         return load_mission_d1();
  1015.         }
  1016. #if defined(DXX_BUILD_DESCENT_II)
  1017.         else
  1018.                 PHYSFSX_contfile_close("descent.hog");
  1019. #endif
  1020.  
  1021. #if defined(DXX_BUILD_DESCENT_II)
  1022.         if (PLAYING_BUILTIN_MISSION) {
  1023.                 switch (Current_mission->builtin_hogsize) {
  1024.                 case SHAREWARE_MISSION_HOGSIZE:
  1025.                 case MAC_SHARE_MISSION_HOGSIZE:
  1026.                         Briefing_text_filename = "brief2.txb";
  1027.                         Ending_text_filename = BIMD2_ENDING_FILE_SHARE;
  1028.                         return load_mission_shareware();
  1029.                 case OEM_MISSION_HOGSIZE:
  1030.                         Briefing_text_filename = "brief2o.txb";
  1031.                         Ending_text_filename = BIMD2_ENDING_FILE_OEM;
  1032.                         return load_mission_oem();
  1033.                 default:
  1034.                         Int3();
  1035.                         DXX_BOOST_FALLTHROUGH;
  1036.                 case FULL_MISSION_HOGSIZE:
  1037.                 case FULL_10_MISSION_HOGSIZE:
  1038.                 case MAC_FULL_MISSION_HOGSIZE:
  1039.                         Briefing_text_filename = "robot.txb";
  1040.                         // continue on... (use d2.mn2 from hogfile)
  1041.                         break;
  1042.                 }
  1043.         }
  1044. #endif
  1045.  
  1046.         //read mission from file
  1047.  
  1048.         auto &msn_extension =
  1049. #if defined(DXX_BUILD_DESCENT_II)
  1050.         (mission->descent_version != Mission::descent_version_type::descent1) ? MISSION_EXTENSION_DESCENT_II :
  1051. #endif
  1052.                 MISSION_EXTENSION_DESCENT_I;
  1053.         std::array<char, PATH_MAX> mission_filename;
  1054.         snprintf(mission_filename.data(), mission_filename.size(), "%s%s", mission->path.c_str(), msn_extension);
  1055.  
  1056.         PHYSFSEXT_locateCorrectCase(mission_filename.data());
  1057.  
  1058.         auto &&mfile = PHYSFSX_openReadBuffered(mission_filename.data());
  1059.         if (!mfile) {
  1060.                 Current_mission.reset();
  1061.                 con_printf(CON_NORMAL, DXX_STRINGIZE_FL(__FILE__, __LINE__, "error: failed to open mission \"%s\""), mission_filename.data());
  1062.                 return "Failed to open mission file";           //error!
  1063.         }
  1064.  
  1065.         //for non-builtin missions, load HOG
  1066. #if defined(DXX_BUILD_DESCENT_II)
  1067.         Current_mission->descent_version = mission->descent_version;
  1068.         if (!PLAYING_BUILTIN_MISSION)
  1069. #endif
  1070.         {
  1071.                 strcpy(&mission_filename[mission->path.size() + 1], "hog");             //change extension
  1072.                         PHYSFSX_contfile_init(mission_filename.data(), 0);
  1073.                 set_briefing_filename(Briefing_text_filename, &*Current_mission->filename);
  1074.                 Ending_text_filename = Briefing_text_filename;
  1075.         }
  1076.  
  1077.         for (PHYSFSX_gets_line_t<PATH_MAX> buf; PHYSFSX_fgets(buf,mfile);)
  1078.         {
  1079.                 if (istok(buf,"type"))
  1080.                         continue;                                               //already have name, go to next line
  1081.                 else if (istok(buf,"briefing")) {
  1082.                         record_briefing(Briefing_text_filename, buf);
  1083.                 }
  1084.                 else if (istok(buf,"ending")) {
  1085.                         record_briefing(Ending_text_filename, buf);
  1086.                 }
  1087.                 else if (istok(buf,"num_levels")) {
  1088.  
  1089.                         if ((v=get_value(buf))!=NULL) {
  1090.                                 char *ip;
  1091.                                 const auto n_levels = strtoul(v, &ip, 10);
  1092.                                 Assert(n_levels <= MAX_LEVELS_PER_MISSION);
  1093.                                 if (n_levels > MAX_LEVELS_PER_MISSION)
  1094.                                         continue;
  1095.                                 if (*ip)
  1096.                                 {
  1097.                                         while (isspace(static_cast<unsigned>(*ip)))
  1098.                                                 ++ip;
  1099.                                         if (*ip && *ip != ';')
  1100.                                                 continue;
  1101.                                 }
  1102.                                 Level_names = std::make_unique<d_fname[]>(n_levels);
  1103.                                 range_for (auto &i, unchecked_partial_range(Level_names.get(), n_levels))
  1104.                                 {
  1105.                                         if (!PHYSFSX_fgets(buf, mfile))
  1106.                                                 break;
  1107.                                         auto &line = buf.line();
  1108.                                         auto s = std::find_if(line.begin(), line.end(), null_or_space);
  1109.                                         if (i.copy_if(buf.line(), std::distance(line.begin(), s)))
  1110.                                         {
  1111.                                                 Last_level++;
  1112.                                         }
  1113.                                         else
  1114.                                                 break;
  1115.                                 }
  1116.  
  1117.                         }
  1118.                 }
  1119.                 else if (istok(buf,"num_secrets")) {
  1120.                         if ((v=get_value(buf))!=NULL) {
  1121.                                 char *ip;
  1122.                                 const auto n_levels = strtoul(v, &ip, 10);
  1123.                                 Assert(n_levels <= MAX_SECRET_LEVELS_PER_MISSION);
  1124.                                 if (n_levels > MAX_SECRET_LEVELS_PER_MISSION)
  1125.                                         continue;
  1126.                                 if (*ip)
  1127.                                 {
  1128.                                         while (isspace(static_cast<unsigned>(*ip)))
  1129.                                                 ++ip;
  1130.                                         if (*ip && *ip != ';')
  1131.                                                 continue;
  1132.                                 }
  1133.                                 N_secret_levels = n_levels;
  1134.                                 Secret_level_names = std::make_unique<d_fname[]>(n_levels);
  1135.                                 Secret_level_table = std::make_unique<uint8_t[]>(n_levels);
  1136.                                 for (int i=0;i<N_secret_levels;i++) {
  1137.                                         if (!PHYSFSX_fgets(buf, mfile))
  1138.                                                 break;
  1139.                                         const auto &line = buf.line();
  1140.                                         const auto lb = line.begin();
  1141.                                         /* No auto: returned value must be type const char*
  1142.                                          * Modern glibc maintains const-ness of the input.
  1143.                                          * Apple libc++ and mingw32 do not.
  1144.                                          */
  1145.                                         const char *const t = strchr(lb, ',');
  1146.                                         if (!t)
  1147.                                                 break;
  1148.                                         auto a = [](char c) {
  1149.                                                 return isspace(static_cast<unsigned>(c));
  1150.                                         };
  1151.                                         auto s = std::find_if(lb, t, a);
  1152.                                         if (Secret_level_names[i].copy_if(line, std::distance(lb, s)))
  1153.                                         {
  1154.                                                 unsigned long ls = strtoul(t + 1, &ip, 10);
  1155.                                                 if (ls < 1 || ls > Last_level)
  1156.                                                         break;
  1157.                                                 Secret_level_table[i] = ls;
  1158.                                                 Last_secret_level--;
  1159.                                         }
  1160.                                         else
  1161.                                                 break;
  1162.                                 }
  1163.  
  1164.                         }
  1165.                 }
  1166. #if defined(DXX_BUILD_DESCENT_II)
  1167.                 else if (Current_mission->descent_version == Mission::descent_version_type::descent2a && buf[0] == '!') {
  1168.                         if (istok(buf+1,"ham")) {
  1169.                                 Current_mission->alternate_ham_file = std::make_unique<d_fname>();
  1170.                                 if ((v=get_value(buf))!=NULL) {
  1171.                                         unsigned l = strlen(v);
  1172.                                         if (l <= 4)
  1173.                                                 con_printf(CON_URGENT, "Mission %s has short HAM \"%s\".", Current_mission->path.c_str(), v);
  1174.                                         else if (l >= sizeof(*Current_mission->alternate_ham_file))
  1175.                                                 con_printf(CON_URGENT, "Mission %s has excessive HAM \"%s\".", Current_mission->path.c_str(), v);
  1176.                                         else {
  1177.                                                 Current_mission->alternate_ham_file->copy_if(v, l + 1);
  1178.                                                 con_printf(CON_VERBOSE, "Mission %s will use HAM %s.", Current_mission->path.c_str(), static_cast<const char *>(*Current_mission->alternate_ham_file));
  1179.                                         }
  1180.                                 }
  1181.                                 else
  1182.                                         con_printf(CON_URGENT, "Mission %s has no HAM.", Current_mission->path.c_str());
  1183.                         }
  1184.                         else {
  1185.                                 con_printf(CON_URGENT, "Mission %s uses unsupported critical directive \"%s\".", Current_mission->path.c_str(), static_cast<const char *>(buf));
  1186.                                 Last_level = 0;
  1187.                                 break;
  1188.                         }
  1189.                 }
  1190. #endif
  1191.  
  1192.         }
  1193.         mfile.reset();
  1194.         if (Last_level <= 0) {
  1195.                 Current_mission.reset();                //no valid mission loaded
  1196.                 return "Failed to parse mission file";
  1197.         }
  1198.  
  1199. #if defined(DXX_BUILD_DESCENT_II)
  1200.         // re-read default HAM file, in case this mission brings it's own version of it
  1201.         free_polygon_models();
  1202.  
  1203.         if (load_mission_ham())
  1204.                 init_extra_robot_movie(&*Current_mission->filename);
  1205. #endif
  1206.         return nullptr;
  1207. }
  1208.  
  1209. //loads the named mission if exists.
  1210. //Returns nullptr if mission loaded ok, else error string.
  1211. const char *load_mission_by_name (const mission_entry_predicate mission_name, const mission_name_type name_match_mode)
  1212. {
  1213.         auto &&mission_list = build_mission_list(mission_filter_mode::include_anarchy);
  1214.         {
  1215.                 range_for (auto &i, mission_list)
  1216.                 {
  1217.                         switch (name_match_mode)
  1218.                         {
  1219.                                 case mission_name_type::basename:
  1220.                                         if (!d_stricmp(mission_name.filesystem_name, &*i.filename))
  1221.                                                 return load_mission(&i);
  1222.                                         continue;
  1223.                                 case mission_name_type::pathname:
  1224.                                 case mission_name_type::guess:
  1225.                                         if (const auto r = compare_mission_by_pathname(mission_name, i))
  1226.                                                 return load_mission(r);
  1227.                                         continue;
  1228.                                 default:
  1229.                                         return "Unhandled load mission type";
  1230.                         }
  1231.                 }
  1232.         }
  1233.         if (name_match_mode == mission_name_type::guess)
  1234.         {
  1235.                 const auto p = strrchr(mission_name.filesystem_name, '/');
  1236.                 const auto &guess_predicate = p
  1237.                         ? mission_name.with_filesystem_name(p + 1)
  1238.                         : mission_name;
  1239.                 range_for (auto &i, mission_list)
  1240.                 {
  1241.                         if (const auto r = compare_mission_by_guess(guess_predicate, i))
  1242.                         {
  1243.                                 con_printf(CON_NORMAL, "%s:%u: request for guessed mission name \"%s\" found \"%s\"", __FILE__, __LINE__, mission_name.filesystem_name, r->path.c_str());
  1244.                                 return load_mission(r);
  1245.                         }
  1246.                 }
  1247.         }
  1248.         return "No matching mission found in\ninstalled mission list.";
  1249. }
  1250.  
  1251. }
  1252.  
  1253. namespace {
  1254.  
  1255. class mission_menu
  1256. {
  1257.         mission_list_type mls;
  1258. public:
  1259.         static constexpr char listbox_go_up[] = "<..>";
  1260.         using callback_type = window_event_result (*)(void);
  1261.         const mission_list_type &ml;
  1262.         const std::unique_ptr<const char *[]> listbox_strings;
  1263.         const RAIIdmem<char[]> title;
  1264.         const callback_type when_selected;
  1265.         listbox *containing_listbox = nullptr;
  1266.         mission_menu *parent = nullptr;
  1267.         mission_menu(mission_list_type &&rml, std::unique_ptr<const char *[]> &&mn, const char *const message, const callback_type ws) :
  1268.                 mls(std::move(rml)), ml(mls), listbox_strings(std::move(mn)),
  1269.                 title(prepare_title(message, ml)), when_selected(ws)
  1270.         {
  1271.         }
  1272.         mission_menu(const mission_list_type *const p, std::unique_ptr<const char *[]> &&mn, const char *const message, const callback_type ws, mission_menu *const parent_menu) :
  1273.                 ml(*p), listbox_strings(std::move(mn)),
  1274.                 title(prepare_title(message, ml)), when_selected(ws),
  1275.                 parent(parent_menu)
  1276.         {
  1277.         }
  1278.         bool is_submenu() const
  1279.         {
  1280.                 return parent != nullptr;
  1281.         }
  1282.         static RAIIdmem<char[]> prepare_title(const char *const message, const mission_list_type &ml)
  1283.         {
  1284.                 mission_subdir_stats ss;
  1285.                 ss.count(ml);
  1286.                 std::array<char, 12> dirbuf;
  1287.                 char buf[128];
  1288.                 snprintf(buf, sizeof(buf), "%s\n[%sMSN:LOCAL %zu; TOTAL %zu]", message, prepare_mission_list_count_dirbuf(dirbuf, ss.immediate_directories), ss.immediate_missions, ss.total_missions);
  1289.                 return RAIIdmem<char[]>(d_strdup(buf));
  1290.         }
  1291. };
  1292.  
  1293. constexpr char mission_menu::listbox_go_up[];
  1294.  
  1295. struct mission_menu_create_state
  1296. {
  1297.         std::unique_ptr<const char *[]> listbox_strings;
  1298.         unsigned initial_selection = UINT_MAX;
  1299.         std::unique_ptr<mission_menu_create_state> submenu;
  1300.         mission_menu_create_state(const std::size_t len) :
  1301.                 listbox_strings(std::make_unique<const char *[]>(len))
  1302.         {
  1303.         }
  1304.         mission_menu_create_state(mission_menu_create_state &&) = default;
  1305. };
  1306.  
  1307. }
  1308.  
  1309. static window_event_result mission_menu_handler(listbox *const lb, const d_event &event, mission_menu *const mm)
  1310. {
  1311.         switch (event.type)
  1312.         {
  1313.                 case EVENT_WINDOW_CREATED:
  1314.                         mm->containing_listbox = lb;
  1315.                         break;
  1316.                 case EVENT_NEWMENU_SELECTED:
  1317.                 {
  1318.                         const auto raw_citem = static_cast<const d_select_event &>(event).citem;
  1319.                         auto citem = raw_citem;
  1320.                         if (mm->is_submenu())
  1321.                         {
  1322.                                 if (citem == 0)
  1323.                                 {
  1324.                                         /* Clear parent pointer so that the parent window is
  1325.                                          * not implicitly closed during handling of
  1326.                                          * EVENT_WINDOW_CLOSE.
  1327.                                          */
  1328.                                         mm->parent = nullptr;
  1329.                                         return window_event_result::close;
  1330.                                 }
  1331.                                 /* Adjust for the "Go up" placeholder item */
  1332.                                 -- citem;
  1333.                         }
  1334.                         if (citem >= 0)
  1335.                         {
  1336.                                 auto &mli = mm->ml[citem];
  1337.                                 if (!mli.directory.empty())
  1338.                                 {
  1339.                                         auto listbox_strings = std::make_unique<const char *[]>(mli.directory.size() + 1);
  1340.                                         listbox_strings[0] = mm->listbox_go_up;
  1341.                                         const auto a = [](const mle &m) -> const char * {
  1342.                                                 return m.mission_name;
  1343.                                         };
  1344.                                         std::transform(mli.directory.begin(), mli.directory.end(), &listbox_strings[1], a);
  1345.                                         const auto pls = listbox_strings.get();
  1346.                                         auto submm = std::make_unique<mission_menu>(&mli.directory, std::move(listbox_strings), mli.path.c_str(), mm->when_selected, mm);
  1347.                                         const auto pmm = submm.get();
  1348.                                         newmenu_listbox1(pmm->title.get(), pmm->ml.size() + 1, pls, 1, 0, mission_menu_handler, std::move(submm));
  1349.                                         return window_event_result::handled;
  1350.                                 }
  1351.                                 // Chose a mission
  1352.                                 else if (const auto errstr = load_mission(&mli))
  1353.                                 {
  1354.                                         nm_messagebox(nullptr, 1, TXT_OK, "%s\n\n%s\n\n%s", TXT_MISSION_ERROR, errstr, mli.path.c_str());
  1355.                                         return window_event_result::handled;    // stay in listbox so user can select another one
  1356.                                 }
  1357.                                 CGameCfg.LastMission.copy_if(mm->listbox_strings[raw_citem]);
  1358.                         }
  1359.                         return (*mm->when_selected)();
  1360.                 }
  1361.                 case EVENT_WINDOW_CLOSE:
  1362.                         /* If the user dismisses the listbox by pressing ESCAPE,
  1363.                          * do not close the parent listbox.
  1364.                          */
  1365.                         if (listbox_get_citem(lb) != -1)
  1366.                                 if (const auto parent = mm->parent)
  1367.                                 {
  1368.                                         window_close(listbox_get_window(parent->containing_listbox));
  1369.                                 }
  1370.                         std::default_delete<mission_menu>()(mm);
  1371.                         break;
  1372.                 default:
  1373.                         break;
  1374.         }
  1375.        
  1376.         return window_event_result::ignored;
  1377. }
  1378.  
  1379. using mission_menu_create_state_ptr = std::unique_ptr<mission_menu_create_state>;
  1380.  
  1381. static mission_menu_create_state_ptr prepare_mission_menu_state(const mission_list_type &mission_list, const char *const LastMission, const std::size_t extra_strings)
  1382. {
  1383.         auto mission_name_to_select = LastMission;
  1384.         auto p = std::make_unique<mission_menu_create_state>(mission_list.size() + extra_strings);
  1385.         auto &create_state = *p.get();
  1386.         auto listbox_strings = create_state.listbox_strings.get();
  1387.         std::fill_n(listbox_strings, extra_strings, nullptr);
  1388.         listbox_strings += extra_strings;
  1389.         range_for (auto &&e, enumerate(mission_list))
  1390.         {
  1391.                 auto &mli = e.value;
  1392.                 const char *const mission_name = mli.mission_name;
  1393.                 *listbox_strings++ = mission_name;
  1394.                 if (!mission_name_to_select)
  1395.                         continue;
  1396.                 if (!mli.directory.empty())
  1397.                 {
  1398.                         auto &&substate = prepare_mission_menu_state(mli.directory, mission_name_to_select, 1);
  1399.                         if (substate->initial_selection == UINT_MAX)
  1400.                                 continue;
  1401.                         substate->listbox_strings[0] = mission_menu::listbox_go_up;
  1402.                         create_state.submenu = std::move(substate);
  1403.                 }
  1404.                 else if (strcmp(mission_name, mission_name_to_select))
  1405.                         continue;
  1406.                 create_state.initial_selection = e.idx;
  1407.                 mission_name_to_select = nullptr;
  1408.         }
  1409.         return p;
  1410. }
  1411.  
  1412. namespace dsx {
  1413.  
  1414. int select_mission(const mission_filter_mode mission_filter, const char *message, window_event_result (*when_selected)(void))
  1415. {
  1416.         auto &&mission_list = build_mission_list(mission_filter);
  1417.         int new_mission_num;
  1418.  
  1419.     if (mission_list.size() <= 1)
  1420.         {
  1421.         new_mission_num = !mission_list.empty() && !load_mission(&mission_list.front()) ? 0 : -1;
  1422.                 (*when_selected)();
  1423.                
  1424.                 return (new_mission_num >= 0);
  1425.     }
  1426.         else
  1427.         {
  1428.                 auto &&create_state_ptr = prepare_mission_menu_state(mission_list, CGameCfg.LastMission, 0);
  1429.                 auto &create_state = *create_state_ptr.get();
  1430.                 mission_menu *parent_mission_menu;
  1431.                 {
  1432.                         auto mm = std::make_unique<mission_menu>(std::move(mission_list), std::move(create_state.listbox_strings), message, when_selected);
  1433.                         parent_mission_menu = mm.get();
  1434.                         newmenu_listbox1(message, parent_mission_menu->ml.size(), parent_mission_menu->listbox_strings.get(), 1, create_state.initial_selection == UINT_MAX ? 0 : create_state.initial_selection, mission_menu_handler, std::move(mm));
  1435.                 }
  1436.                 for (auto parent_state = &create_state; const auto substate = parent_state->submenu.get(); parent_state = substate)
  1437.                 {
  1438.                         const auto parent_initial_selection = parent_state->initial_selection;
  1439.                         const auto parent_mission_list_size = parent_mission_menu->ml.size();
  1440.                         assert(parent_initial_selection < parent_mission_list_size);
  1441.                         if (parent_initial_selection >= parent_mission_list_size)
  1442.                                 break;
  1443.                         const auto &substate_mission_list = parent_mission_menu->ml[parent_initial_selection];
  1444.                         auto mm = std::make_unique<mission_menu>(&substate_mission_list.directory, std::move(substate->listbox_strings), substate_mission_list.path.c_str(), when_selected, parent_mission_menu);
  1445.                         const auto pmm = mm.get();
  1446.                         parent_mission_menu = pmm;
  1447.                         newmenu_listbox1(pmm->title.get(), pmm->ml.size() + 1, pmm->listbox_strings.get(), 1, substate->initial_selection + 1, mission_menu_handler, std::move(mm));
  1448.                 }
  1449.     }
  1450.  
  1451.     return 1;   // presume success
  1452. }
  1453.  
  1454. #if DXX_USE_EDITOR
  1455. static int write_mission(void)
  1456. {
  1457.         auto &msn_extension =
  1458. #if defined(DXX_BUILD_DESCENT_II)
  1459.         (Current_mission->descent_version != Mission::descent_version_type::descent1) ? MISSION_EXTENSION_DESCENT_II :
  1460. #endif
  1461.         MISSION_EXTENSION_DESCENT_I;
  1462.         std::array<char, PATH_MAX> mission_filename;
  1463.         snprintf(mission_filename.data(), mission_filename.size(), "%s%s", Current_mission->path.c_str(), msn_extension);
  1464.        
  1465.         auto &&mfile = PHYSFSX_openWriteBuffered(mission_filename.data());
  1466.         if (!mfile)
  1467.         {
  1468.                 PHYSFS_mkdir(MISSION_DIR);      //try making directory - in *write* path
  1469.                 mfile = PHYSFSX_openWriteBuffered(mission_filename.data());
  1470.                 if (!mfile)
  1471.                         return 0;
  1472.         }
  1473.  
  1474.         const char *prefix = "";
  1475. #if defined(DXX_BUILD_DESCENT_II)
  1476.         switch (Current_mission->descent_version)
  1477.         {
  1478.                 case Mission::descent_version_type::descent2x:
  1479.                         prefix = "x";
  1480.                         break;
  1481.  
  1482.                 case Mission::descent_version_type::descent2z:
  1483.                         prefix = "z";
  1484.                         break;
  1485.                        
  1486.                 case Mission::descent_version_type::descent2a:
  1487.                         prefix = "!";
  1488.                         break;
  1489.  
  1490.                 default:
  1491.                         break;
  1492.         }
  1493. #endif
  1494.  
  1495.         PHYSFSX_printf(mfile, "%sname = %s\n", prefix, static_cast<const char *>(Current_mission->mission_name));
  1496.  
  1497.         PHYSFSX_printf(mfile, "type = %s\n", Current_mission->anarchy_only_flag ? "anarchy" : "normal");
  1498.  
  1499.         if (Briefing_text_filename[0])
  1500.                 PHYSFSX_printf(mfile, "briefing = %s\n", static_cast<const char *>(Briefing_text_filename));
  1501.  
  1502.         if (Ending_text_filename[0])
  1503.                 PHYSFSX_printf(mfile, "ending = %s\n", static_cast<const char *>(Ending_text_filename));
  1504.  
  1505.         PHYSFSX_printf(mfile, "num_levels = %i\n", Last_level);
  1506.  
  1507.         range_for (auto &i, unchecked_partial_range(Level_names.get(), Last_level))
  1508.                 PHYSFSX_printf(mfile, "%s\n", static_cast<const char *>(i));
  1509.  
  1510.         if (N_secret_levels)
  1511.         {
  1512.                 PHYSFSX_printf(mfile, "num_secrets = %i\n", N_secret_levels);
  1513.  
  1514.                 for (int i = 0; i < N_secret_levels; i++)
  1515.                         PHYSFSX_printf(mfile, "%s,%i\n", static_cast<const char *>(Secret_level_names[i]), Secret_level_table[i]);
  1516.         }
  1517.  
  1518. #if defined(DXX_BUILD_DESCENT_II)
  1519.         if (Current_mission->alternate_ham_file)
  1520.                 PHYSFSX_printf(mfile, "ham = %s\n", static_cast<const char *>(*Current_mission->alternate_ham_file.get()));
  1521. #endif
  1522.  
  1523.         return 1;
  1524. }
  1525.  
  1526. void create_new_mission(void)
  1527. {
  1528.         Current_mission = std::make_unique<Mission>(Mission_path(MISSION_DIR "new_miss", sizeof(MISSION_DIR) - 1));             // limited to eight characters because of savegame format
  1529.         Current_mission->mission_name.copy_if("Untitled");
  1530.         Current_mission->builtin_hogsize = 0;
  1531.         Current_mission->anarchy_only_flag = 0;
  1532.        
  1533.         Level_names = std::make_unique<d_fname[]>(1);
  1534.         if (!Level_names)
  1535.         {
  1536.                 Current_mission.reset();
  1537.                 return;
  1538.         }
  1539.  
  1540.         Level_names[0] = "GAMESAVE.LVL";
  1541.         Last_level = 1;
  1542.         N_secret_levels = 0;
  1543.         Last_secret_level = 0;
  1544.         Briefing_text_filename = {};
  1545.         Ending_text_filename = {};
  1546.         Secret_level_table.reset();
  1547.         Secret_level_names.reset();
  1548.  
  1549. #if defined(DXX_BUILD_DESCENT_II)
  1550.         if (Gamesave_current_version > 3)
  1551.                 Current_mission->descent_version = Mission::descent_version_type::descent2;     // custom ham not supported in editor (yet)
  1552.         else
  1553.                 Current_mission->descent_version = Mission::descent_version_type::descent1;
  1554.  
  1555.         Current_mission->alternate_ham_file = nullptr;
  1556. #endif
  1557.  
  1558.         write_mission();
  1559. }
  1560. #endif
  1561.  
  1562. }
  1563.