Subversion Repositories Games.Descent

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
1 pmbaty 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
}