savegame.cpp 74.4 KB
Newer Older
1
2
//-------------------------------------------------------------------------
/*
3
Copyright (C) 2010 EDuke32 developers and contributors
4

5
This file is part of EDuke32.
6
7
8
9
10
11
12
13
14
15
16
17
18

EDuke32 is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
19
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20
21
22
23
*/
//-------------------------------------------------------------------------

#include "duke3d.h"
24
#include "premap.h"
25
#include "prlights.h"
26
#include "md4.h"
27
#include "savegame.h"
28

29
30
#include "vfs.h"

Evan Ramos's avatar
Evan Ramos committed
31
static OutputFileCounter savecounter;
32

33
34
35
36
37
// For storing pointers in files.
//  back_p==0: ptr -> "small int"
//  back_p==1: "small int" -> ptr
//
//  mode: see enum in savegame.h
38
void G_Util_PtrToIdx(void *ptr, int32_t const count, const void *base, int32_t const mode)
39
{
40
    intptr_t *iptr = (intptr_t *)ptr;
41
42
    intptr_t const ibase = (intptr_t)base;
    int32_t const onlynon0_p = mode&P2I_ONLYNON0_BIT;
43
44
45
46
47

    // TODO: convert to proper offsets/indices for (a step towards) cross-
    //       compatibility between 32- and 64-bit systems in the netplay.
    //       REMEMBER to bump BYTEVERSION then.

48
49
50
    // WARNING: C std doesn't say that bit pattern of NULL is necessarily 0!
    if ((mode & P2I_BACK_BIT) == 0)
    {
51
        for (bssize_t i = 0; i < count; i++)
52
            if (!onlynon0_p || iptr[i])
53
                iptr[i] -= ibase;
54
55
56
    }
    else
    {
57
        for (bssize_t i = 0; i < count; i++)
58
            if (!onlynon0_p || iptr[i])
59
                iptr[i] += ibase;
60
    }
61
62
}

63
void G_Util_PtrToIdx2(void *ptr, int32_t const count, size_t const stride, const void *base, int32_t const mode)
64
{
65
    uint8_t *iptr = (uint8_t *)ptr;
66
67
    intptr_t const ibase = (intptr_t)base;
    int32_t const onlynon0_p = mode&P2I_ONLYNON0_BIT;
68

69
    if ((mode & P2I_BACK_BIT) == 0)
70
    {
71
        for (bssize_t i = 0; i < count; ++i)
72
        {
73
            if (!onlynon0_p || *(intptr_t *)iptr)
74
                *(intptr_t *)iptr -= ibase;
75
76

            iptr += stride;
77
        }
78
79
80
    }
    else
    {
81
        for (bssize_t i = 0; i < count; ++i)
82
83
84
        {
            if (!onlynon0_p || *(intptr_t *)iptr)
                *(intptr_t *)iptr += ibase;
85

86
87
            iptr += stride;
        }
88
89
90
    }
}

91
92
93
94
95
// TODO: sync with TROR special interpolations? (e.g. upper floor of subway)
void G_ResetInterpolations(void)
{
    int32_t k, i;

96
    g_interpolationCnt = 0;
97
98
99
100
101
102

    k = headspritestat[STAT_EFFECTOR];
    while (k >= 0)
    {
        switch (sprite[k].lotag)
        {
103
        case SE_31_FLOOR_RISE_FALL:
104
105
            G_SetInterpolation(&sector[sprite[k].sectnum].floorz);
            break;
106
        case SE_32_CEILING_RISE_FALL:
107
108
            G_SetInterpolation(&sector[sprite[k].sectnum].ceilingz);
            break;
109
110
        case SE_17_WARP_ELEVATOR:
        case SE_25_PISTON:
111
112
113
            G_SetInterpolation(&sector[sprite[k].sectnum].floorz);
            G_SetInterpolation(&sector[sprite[k].sectnum].ceilingz);
            break;
114
115
116
117
118
119
        case SE_0_ROTATING_SECTOR:
        case SE_5:
        case SE_6_SUBWAY:
        case SE_11_SWINGING_DOOR:
        case SE_14_SUBWAY_CAR:
        case SE_15_SLIDING_DOOR:
120
        case SE_16_REACTOR:
Richard Gobeille's avatar
Richard Gobeille committed
121
        case SE_26_ESCALATOR:
122
        case SE_30_TWO_WAY_TRAIN:
123
124
125
126
127
128
129
            Sect_SetInterpolation(sprite[k].sectnum);
            break;
        }

        k = nextspritestat[k];
    }

130
131
132
    for (i=g_interpolationCnt-1; i>=0; i--) bakipos[i] = *curipos[i];
    for (i = g_animateCnt-1; i>=0; i--)
        G_SetInterpolation(g_animatePtr[i]);
133
134
}

Evan Ramos's avatar
Evan Ramos committed
135
136
137
138
savebrief_t g_lastautosave, g_lastusersave, g_freshload;
int32_t g_lastAutoSaveArbitraryID = -1;
bool g_saveRequested;
savebrief_t * g_quickload;
139

Evan Ramos's avatar
Evan Ramos committed
140
menusave_t * g_menusaves;
Richard Gobeille's avatar
Richard Gobeille committed
141
uint16_t g_nummenusaves;
142

143
static menusave_t * g_internalsaves;
Richard Gobeille's avatar
Richard Gobeille committed
144
static uint16_t g_numinternalsaves;
145

146
static void ReadSaveGameHeaders_CACHE1D(BUILDVFS_FIND_REC *f)
Evan Ramos's avatar
Evan Ramos committed
147
148
{
    savehead_t h;
149

Evan Ramos's avatar
Evan Ramos committed
150
    for (; f != nullptr; f = f->next)
151
    {
Evan Ramos's avatar
Evan Ramos committed
152
        char const * fn = f->name;
153
154
        buildvfs_kfd fil = kopen4loadfrommod(fn, 0);
        if (fil == buildvfs_kfd_invalid)
155
156
            continue;

157
        menusave_t & msv = g_internalsaves[g_numinternalsaves];
Evan Ramos's avatar
Evan Ramos committed
158

Evan Ramos's avatar
Evan Ramos committed
159
160
        msv.brief.isExt = 0;

Evan Ramos's avatar
Evan Ramos committed
161
        int32_t k = sv_loadheader(fil, 0, &h);
162
163
        if (k)
        {
164
165
            if (k < 0)
                msv.isUnreadable = 1;
Evan Ramos's avatar
Evan Ramos committed
166
167
168
169
170
171
            else
            {
                if (FURY)
                {
                    char extfn[BMAX_PATH];
                    snprintf(extfn, ARRAY_SIZE(extfn), "%s.ext", fn);
Evan Ramos's avatar
Evan Ramos committed
172
173
                    buildvfs_kfd extfil = kopen4loadfrommod(extfn, 0);
                    if (extfil != buildvfs_kfd_invalid)
Evan Ramos's avatar
Evan Ramos committed
174
175
                    {
                        msv.brief.isExt = 1;
Evan Ramos's avatar
Evan Ramos committed
176
                        kclose(extfil);
Evan Ramos's avatar
Evan Ramos committed
177
178
179
                    }
                }
            }
180
            msv.isOldVer = 1;
181
        }
Evan Ramos's avatar
Evan Ramos committed
182
183
        else
            msv.isOldVer = 0;
184

185
186
        msv.isAutoSave = h.isAutoSave();

187
188
189
        strncpy(msv.brief.path, fn, ARRAY_SIZE(msv.brief.path));
        ++g_numinternalsaves;

Evan Ramos's avatar
Evan Ramos committed
190
191
192
193
        if (k >= 0 && h.savename[0] != '\0')
        {
            memcpy(msv.brief.name, h.savename, ARRAY_SIZE(msv.brief.name));
        }
194
195
        else
            msv.isUnreadable = 1;
196
197
198
199
200

        kclose(fil);
    }
}

201
static int countcache1dfind(BUILDVFS_FIND_REC *f)
202
{
Richard Gobeille's avatar
Richard Gobeille committed
203
    int x = 0;
Evan Ramos's avatar
Evan Ramos committed
204
205
206
207
208
    for (; f != nullptr; f = f->next)
        ++x;
    return x;
}

Evan Ramos's avatar
Evan Ramos committed
209
static void ReadSaveGameHeaders_Internal(void)
Evan Ramos's avatar
Evan Ramos committed
210
211
212
{
    static char const DefaultPath[] = "/", SavePattern[] = "*.esv";

213
    BUILDVFS_FIND_REC *findfiles_default = klistpath(DefaultPath, SavePattern, BUILDVFS_FIND_FILE);
214

Evan Ramos's avatar
Evan Ramos committed
215
    // potentially overallocating but programmatically simple
Richard Gobeille's avatar
Richard Gobeille committed
216
    int const numfiles = countcache1dfind(findfiles_default);
217
    size_t const internalsavesize = sizeof(menusave_t) * numfiles;
218

219
    g_internalsaves = (menusave_t *)Xrealloc(g_internalsaves, internalsavesize);
220

Richard Gobeille's avatar
Richard Gobeille committed
221
    for (int x = 0; x < numfiles; ++x)
Evan Ramos's avatar
Evan Ramos committed
222
        g_internalsaves[x].clear();
223

224
    g_numinternalsaves = 0;
Evan Ramos's avatar
Evan Ramos committed
225
226
    ReadSaveGameHeaders_CACHE1D(findfiles_default);
    klistfree(findfiles_default);
227
228

    g_nummenusaves = 0;
Richard Gobeille's avatar
Richard Gobeille committed
229
    for (int x = g_numinternalsaves-1; x >= 0; --x)
230
231
232
233
234
235
236
237
    {
        menusave_t & msv = g_internalsaves[x];
        if (!msv.isUnreadable)
        {
            ++g_nummenusaves;
        }
    }
    size_t const menusavesize = sizeof(menusave_t) * g_nummenusaves;
238

239
    g_menusaves = (menusave_t *)Xrealloc(g_menusaves, menusavesize);
240

Richard Gobeille's avatar
Richard Gobeille committed
241
    for (int x = 0; x < g_nummenusaves; ++x)
Evan Ramos's avatar
Evan Ramos committed
242
        g_menusaves[x].clear();
243

Richard Gobeille's avatar
Richard Gobeille committed
244
    for (int x = g_numinternalsaves-1, y = 0; x >= 0; --x)
245
246
247
248
249
250
251
    {
        menusave_t & msv = g_internalsaves[x];
        if (!msv.isUnreadable)
        {
            g_menusaves[y++] = msv;
        }
    }
252

Richard Gobeille's avatar
Richard Gobeille committed
253
    for (int x = g_numinternalsaves-1; x >= 0; --x)
254
255
    {
        char const * const path = g_internalsaves[x].brief.path;
Richard Gobeille's avatar
Richard Gobeille committed
256
        int const pathlen = Bstrlen(path);
257
258
259
260
261
262
263
264
265
        if (pathlen < 12)
            continue;
        char const * const fn = path + (pathlen-12);
        if (fn[0] == 's' && fn[1] == 'a' && fn[2] == 'v' && fn[3] == 'e' &&
            isdigit(fn[4]) && isdigit(fn[5]) && isdigit(fn[6]) && isdigit(fn[7]))
        {
            char number[5];
            memcpy(number, fn+4, 4);
            number[4] = '\0';
Richard Gobeille's avatar
Richard Gobeille committed
266
            savecounter.count = Batoi(number)+1;
267
268
269
            break;
        }
    }
Evan Ramos's avatar
Evan Ramos committed
270
271
}

Evan Ramos's avatar
Evan Ramos committed
272
273
274
275
276
277
278
279
void ReadSaveGameHeaders(void)
{
    ReadSaveGameHeaders_Internal();

    if (!ud.autosavedeletion)
        return;

    bool didDelete = false;
Richard Gobeille's avatar
Richard Gobeille committed
280
281
    int numautosaves = 0;
    for (int x = 0; x < g_nummenusaves; ++x)
Evan Ramos's avatar
Evan Ramos committed
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
    {
        menusave_t & msv = g_menusaves[x];
        if (!msv.isAutoSave)
            continue;
        if (numautosaves >= ud.maxautosaves)
        {
            G_DeleteSave(msv.brief);
            didDelete = true;
        }
        ++numautosaves;
    }

    if (didDelete)
        ReadSaveGameHeaders_Internal();
}

Evan Ramos's avatar
Evan Ramos committed
298
299
int32_t G_LoadSaveHeaderNew(char const *fn, savehead_t *saveh)
{
300
301
    buildvfs_kfd fil = kopen4loadfrommod(fn, 0);
    if (fil == buildvfs_kfd_invalid)
302
303
        return -1;

Evan Ramos's avatar
Evan Ramos committed
304
    int32_t i = sv_loadheader(fil, 0, saveh);
305
    if (i < 0)
306
307
        goto corrupt;

Evan Ramos's avatar
Evan Ramos committed
308
    int32_t screenshotofs;
309
310
311
    if (kread(fil, &screenshotofs, 4) != 4)
        goto corrupt;

Richard Gobeille's avatar
Richard Gobeille committed
312
    walock[TILE_LOADSHOT] = CACHE1D_PERMANENT;
313
    if (waloff[TILE_LOADSHOT] == 0)
Richard Gobeille's avatar
Richard Gobeille committed
314
        g_cache.allocateBlock(&waloff[TILE_LOADSHOT], 320*200, &walock[TILE_LOADSHOT]);
315
316
    tilesiz[TILE_LOADSHOT].x = 200;
    tilesiz[TILE_LOADSHOT].y = 320;
317
318
    if (screenshotofs)
    {
319
        if (kdfread_LZ4((char *)waloff[TILE_LOADSHOT], 320, 200, fil) != 200)
320
        {
321
            OSD_Printf("G_LoadSaveHeaderNew(): failed reading screenshot in \"%s\"\n", fn);
322
            goto corrupt;
323
        }
Evan Ramos's avatar
Evan Ramos committed
324
325
326
327
328
329
330
331
332
333
334

#if 0
        // debug code to dump the screenshot
        char scrbuf[BMAX_PATH];
        if (G_ModDirSnprintf(scrbuf, sizeof(scrbuf), "%s.raw", fn) == 0)
        {
            buildvfs_FILE scrfil = buildvfs_fopen_write(scrbuf);
            buildvfs_fwrite((char *)waloff[TILE_LOADSHOT], 320, 200, scrfil);
            buildvfs_fclose(scrfil);
        }
#endif
335
336
337
338
339
    }
    else
    {
        Bmemset((char *)waloff[TILE_LOADSHOT], 0, 320*200);
    }
Richard Gobeille's avatar
Richard Gobeille committed
340
    tileInvalidate(TILE_LOADSHOT, 0, 255);
341
342
343

    kclose(fil);
    return 0;
344

345
346
347
348
349
corrupt:
    kclose(fil);
    return 1;
}

350
351
352
353
354
355
356
357
358
359
360
361
static int32_t sv_loadBoardMD4(char* const fn)
{
    buildvfs_kfd fil;
    if ((fil = kopen4load(fn,0)) == buildvfs_kfd_invalid)
        return -1;

    klseek(fil, 0, SEEK_SET);
    int32_t boardsize = kfilelength(fil);
    uint8_t *fullboard = (uint8_t*)Xmalloc(boardsize);
    if (kread_and_test(fil, fullboard, boardsize))
    {
        Xfree(fullboard);
362
        kclose(fil);
363
364
365
366
367
        return -1;
    }

    md4once(fullboard, boardsize, g_loadedMapHack.md4);
    Xfree(fullboard);
368
    kclose(fil);
369
370
371
    return 0;
}

372
static void sv_loadMhk(usermaphack_t* const mhkInfo, char* const currentboardfilename)
373
374
375
{
    bool loadedMhk = false;

376
377
    if (mhkInfo && (loadedMhk = (engineLoadMHK(mhkInfo->mhkfile) == 0)))
        initprintf("Loaded map hack file \"%s\"\n", mhkInfo->mhkfile);
378
379
380

    if (!loadedMhk)
    {
381
382
383
384
385
        char bfn[BMAX_PATH];
        Bstrcpy(bfn, currentboardfilename);
        append_ext_UNSAFE(bfn, ".mhk");
        if (engineLoadMHK(bfn) == 0)
            initprintf("Loaded map hack file \"%s\"\n", bfn);
386
387
    }
}
388

389
390
static void sv_loadMapart(usermaphack_t* const mhkInfo, char* const currentboardfilename)
{
391
    if (mhkInfo && mhkInfo->mapart)
392
393
394
395
396
397
398
    {
        initprintf("Using mapinfo-defined mapart \"%s\"\n", mhkInfo->mapart);
        artSetupMapArt(mhkInfo->mapart);
    }
    else artSetupMapArt(currentboardfilename);
}

399
static void sv_postudload();
400

401
402
403
// hack
static int different_user_map;

Evan Ramos's avatar
Evan Ramos committed
404
405
#include "sjson.h"

406
// XXX: keyboard input 'blocked' after load fail? (at least ESC?)
Evan Ramos's avatar
Evan Ramos committed
407
int32_t G_LoadPlayer(savebrief_t & sv)
408
{
Evan Ramos's avatar
Evan Ramos committed
409
410
    if (sv.isExt)
    {
Evan Ramos's avatar
Evan Ramos committed
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
        int volume = -1;
        int level = -1;
        int skill = -1;

        buildvfs_kfd const fil = kopen4loadfrommod(sv.path, 0);

        if (fil != buildvfs_kfd_invalid)
        {
            savehead_t h;
            int status = sv_loadheader(fil, 0, &h);
            if (status >= 0)
            {
                volume = h.volnum;
                level = h.levnum;
                skill = h.skill;
            }

            kclose(fil);
        }

Evan Ramos's avatar
Evan Ramos committed
431
432
        char extfn[BMAX_PATH];
        snprintf(extfn, ARRAY_SIZE(extfn), "%s.ext", sv.path);
Evan Ramos's avatar
Evan Ramos committed
433
434
        buildvfs_kfd extfil = kopen4loadfrommod(extfn, 0);
        if (extfil == buildvfs_kfd_invalid)
Evan Ramos's avatar
Evan Ramos committed
435
436
437
438
        {
            return -1;
        }

Evan Ramos's avatar
Evan Ramos committed
439
        int32_t len = kfilelength(extfil);
Evan Ramos's avatar
Evan Ramos committed
440
441
442
        auto text = (char *)Xmalloc(len+1);
        text[len] = '\0';

Evan Ramos's avatar
Evan Ramos committed
443
        if (kread_and_test(extfil, text, len))
Evan Ramos's avatar
Evan Ramos committed
444
        {
Evan Ramos's avatar
Evan Ramos committed
445
            kclose(extfil);
Evan Ramos's avatar
Evan Ramos committed
446
447
448
449
            Xfree(text);
            return -1;
        }

Evan Ramos's avatar
Evan Ramos committed
450
        kclose(extfil);
Evan Ramos's avatar
Evan Ramos committed
451
452
453
454
455
456
457


        sjson_context * ctx = sjson_create_context(0, 0, NULL);
        sjson_node * root = sjson_decode(ctx, text);

        Xfree(text);

Evan Ramos's avatar
Evan Ramos committed
458
459
460
461
462
463
        if (volume == -1)
            volume = sjson_get_int(root, "volume", volume);
        if (level == -1)
            level = sjson_get_int(root, "level", level);
        if (skill == -1)
            skill = sjson_get_int(root, "skill", skill);
Evan Ramos's avatar
Evan Ramos committed
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482

        if (volume == -1 || level == -1 || skill == -1)
        {
            sjson_destroy_context(ctx);
            return -1;
        }

        sjson_node * players = sjson_find_member(root, "players");

        int numplayers = sjson_child_count(players);

        if (numplayers != ud.multimode)
        {
            P_DoQuote(QUOTE_SAVE_BAD_PLAYERS, g_player[myconnectindex].ps);

            sjson_destroy_context(ctx);
            return 1;
        }

Evan Ramos's avatar
Evan Ramos committed
483
484
485
486
487
488

        ud.returnvar[0] = level;
        volume = VM_OnEventWithReturn(EVENT_VALIDATESTART, g_player[myconnectindex].ps->i, myconnectindex, volume);
        level = ud.returnvar[0];


Evan Ramos's avatar
Evan Ramos committed
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
        {
            // CODEDUP from non-isExt branch, with simplifying assumptions

            VM_OnEvent(EVENT_PRELOADGAME, g_player[screenpeek].ps->i, screenpeek);

            ud.multimode = numplayers;

            Net_WaitForServer();

            FX_StopAllSounds();
            S_ClearSoundLocks();

            ud.m_volume_number = volume;
            ud.m_level_number = level;
            ud.m_player_skill = skill;

            boardfilename[0] = '\0';

            int const mapIdx = volume*MAXLEVELS + level;

            if (boardfilename[0])
                Bstrcpy(currentboardfilename, boardfilename);
            else if (g_mapInfo[mapIdx].filename)
                Bstrcpy(currentboardfilename, g_mapInfo[mapIdx].filename);

514

Evan Ramos's avatar
Evan Ramos committed
515
516
            if (currentboardfilename[0])
            {
517
518
519
520
521
                usermaphack_t* mhkInfo = NULL;
                if (sv_loadBoardMD4(currentboardfilename) == 0)
                    mhkInfo = (usermaphack_t *)bsearch(&g_loadedMapHack, usermaphacks, num_usermaphacks,
                                 sizeof(usermaphack_t), compare_usermaphacks);

522
                // only setup art if map differs from previous
523
                if (!previousboardfilename[0] || Bstrcmp(previousboardfilename, currentboardfilename))
524
                {
525
                    sv_loadMapart(mhkInfo, currentboardfilename);
526
527
                    Bstrcpy(previousboardfilename, currentboardfilename);
                }
528

529
                sv_loadMhk(mhkInfo, currentboardfilename);
Evan Ramos's avatar
Evan Ramos committed
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
            }

            currentboardfilename[0] = '\0';

            // G_NewGame_EnterLevel();
        }

        {
            // CODEDUP from G_NewGame

            auto & p0 = *g_player[0].ps;

            ready2send = 0;

            ud.from_bonus    = 0;
            ud.last_level    = -1;
            ud.level_number  = level;
            ud.player_skill  = skill;
            ud.secretlevel   = 0;
            ud.skill_voice   = -1;
            ud.volume_number = volume;

            g_lastAutoSaveArbitraryID = -1;

#ifdef EDUKE32_TOUCH_DEVICES
            p0.zoom = 360;
#else
            p0.zoom = 768;
#endif
            p0.gm = 0;

            Menu_Close(0);

            Gv_ResetVars();
            Gv_InitWeaponPointers();
            Gv_RefreshPointers();
            Gv_ResetSystemDefaults();

            for (int i=0; i < (MAXVOLUMES*MAXLEVELS); i++)
                G_FreeMapState(i);

            if (ud.m_coop != 1)
                p0.last_weapon = -1;

            display_mirror = 0;
        }

        int p = 0;
        for (sjson_node * player = sjson_first_child(players); player != nullptr; player = player->next)
        {
            playerdata_t * playerData = &g_player[p];
            DukePlayer_t * ps = playerData->ps;
            auto pSprite = &sprite[ps->i];

            pSprite->extra = sjson_get_int(player, "extra", -1);
            ps->max_player_health = sjson_get_int(player, "max_player_health", -1);

            sjson_node * gotweapon = sjson_find_member(player, "gotweapon");
            int w_end = min<int>(MAX_WEAPONS, sjson_child_count(gotweapon));
            ps->gotweapon = 0;
            for (int w = 0; w < w_end; ++w)
            {
                sjson_node * ele = sjson_find_element(gotweapon, w);
                if (ele->tag == SJSON_BOOL && ele->bool_)
                    ps->gotweapon |= 1<<w;
            }

            /* bool flag_ammo_amount = */ sjson_get_int16s(ps->ammo_amount, MAX_WEAPONS, player, "ammo_amount");
            /* bool flag_max_ammo_amount = */ sjson_get_int16s(ps->max_ammo_amount, MAX_WEAPONS, player, "max_ammo_amount");
            /* bool flag_inv_amount = */ sjson_get_int16s(ps->inv_amount, GET_MAX, player, "inv_amount");

            ps->max_shield_amount = sjson_get_int(player, "max_shield_amount", -1);

            ps->curr_weapon = sjson_get_int(player, "curr_weapon", -1);
            ps->subweapon = sjson_get_int(player, "subweapon", -1);
            ps->inven_icon = sjson_get_int(player, "inven_icon", -1);

            sjson_node * vars = sjson_find_member(player, "vars");

            for (int j=0; j<g_gameVarCount; j++)
            {
                gamevar_t & var = aGameVars[j];

                if (!(var.flags & GAMEVAR_SERIALIZE))
                    continue;

                if ((var.flags & (GAMEVAR_PERPLAYER|GAMEVAR_PERACTOR)) != GAMEVAR_PERPLAYER)
                    continue;

                Gv_SetVar(j, sjson_get_int(vars, var.szLabel, var.defaultValue), ps->i, p);
            }

            ++p;
        }

        {
            sjson_node * vars = sjson_find_member(root, "vars");

            for (int j=0; j<g_gameVarCount; j++)
            {
                gamevar_t & var = aGameVars[j];

                if (!(var.flags & GAMEVAR_SERIALIZE))
                    continue;

                if (var.flags & (GAMEVAR_PERPLAYER|GAMEVAR_PERACTOR))
                    continue;

                Gv_SetVar(j, sjson_get_int(vars, var.szLabel, var.defaultValue));
            }
        }

        sjson_destroy_context(ctx);


        if (G_EnterLevel(MODE_GAME|MODE_EOL))
            G_BackToMenu();


        // postloadplayer(1);

        // sv_postudload();


        VM_OnEvent(EVENT_LOADGAME, g_player[screenpeek].ps->i, screenpeek);

        return 0;
    }

659
    buildvfs_kfd const fil = kopen4loadfrommod(sv.path, 0);
660

661
    if (fil == buildvfs_kfd_invalid)
662
663
664
665
        return -1;

    ready2send = 0;

Richard Gobeille's avatar
Richard Gobeille committed
666
667
668
669
    savehead_t h;
    int status = sv_loadheader(fil, 0, &h);

    if (status < 0 || h.numplayers != ud.multimode)
670
    {
Richard Gobeille's avatar
Richard Gobeille committed
671
        if (status == -4 || status == -3 || status == 1)
672
            P_DoQuote(QUOTE_SAVE_BAD_VERSION, g_player[myconnectindex].ps);
Richard Gobeille's avatar
Richard Gobeille committed
673
        else if (h.numplayers != ud.multimode)
674
675
676
677
678
679
680
681
682
            P_DoQuote(QUOTE_SAVE_BAD_PLAYERS, g_player[myconnectindex].ps);

        kclose(fil);
        ototalclock = totalclock;
        ready2send = 1;

        return 1;
    }

683
    VM_OnEvent(EVENT_PRELOADGAME, g_player[screenpeek].ps->i, screenpeek);
684

685
686
687
688
689
690
691
692
693
    // some setup first
    ud.multimode = h.numplayers;

    if (numplayers > 1)
    {
        pub = NUMPAGES;
        pus = NUMPAGES;
        G_UpdateScreenArea();
        G_DrawBackground();
Evan Ramos's avatar
Evan Ramos committed
694
        menutext_center(100, "Loading...");
695
        videoNextPage();
696
697
698
699
700
701
702
703
704
705
706
707
    }

    Net_WaitForServer();

    FX_StopAllSounds();
    S_ClearSoundLocks();

    // non-"m_" fields will be loaded from svgm_udnetw
    ud.m_volume_number = h.volnum;
    ud.m_level_number = h.levnum;
    ud.m_player_skill = h.skill;

708
    EDUKE32_STATIC_ASSERT(sizeof(h.boardfn) < sizeof(boardfilename));
709
    different_user_map = Bstrncmp(boardfilename, h.boardfn, sizeof(h.boardfn));
710
711
    // NOTE: size arg is (unconventionally) that of the source, it being smaller.
    Bstrncpyz(boardfilename, h.boardfn, sizeof(h.boardfn) /*!*/);
712

Richard Gobeille's avatar
Richard Gobeille committed
713
    int const mapIdx = h.volnum*MAXLEVELS + h.levnum;
714
715
716

    if (boardfilename[0])
        Bstrcpy(currentboardfilename, boardfilename);
717
718
    else if (g_mapInfo[mapIdx].filename)
        Bstrcpy(currentboardfilename, g_mapInfo[mapIdx].filename);
719
720
721

    if (currentboardfilename[0])
    {
722
723
724
725
726
        usermaphack_t* mhkInfo = NULL;
        if (sv_loadBoardMD4(currentboardfilename) == 0)
            mhkInfo = (usermaphack_t *)bsearch(&g_loadedMapHack, usermaphacks, num_usermaphacks,
                                 sizeof(usermaphack_t), compare_usermaphacks);

727
728
        // only setup art if map differs from previous
        if (!previousboardfilename[0] || Bstrcmp(previousboardfilename, currentboardfilename))
729
        {
730
            sv_loadMapart(mhkInfo, currentboardfilename);
731
732
            Bstrcpy(previousboardfilename, currentboardfilename);
        }
733

734
        sv_loadMhk(mhkInfo, currentboardfilename);
735
736
737
738
    }

    Bmemcpy(currentboardfilename, boardfilename, BMAX_PATH);

Richard Gobeille's avatar
Richard Gobeille committed
739
    if (status == 2)
740
        G_NewGame_EnterLevel();
Richard Gobeille's avatar
Richard Gobeille committed
741
    else if ((status = sv_loadsnapshot(fil, 0, &h)))  // read the rest...
742
    {
Richard Gobeille's avatar
Richard Gobeille committed
743
744
745
746
        // in theory, we could load into an initial dump first and trivially
        // recover if things go wrong...
        Bsprintf(tempbuf, "Loading save game file \"%s\" failed (code %d), cannot recover.", sv.path, status);
        G_GameExit(tempbuf);
747
748
    }

Richard Gobeille's avatar
Richard Gobeille committed
749
    sv_postudload();  // ud.m_XXX = ud.XXX
750
    VM_OnEvent(EVENT_LOADGAME, g_player[screenpeek].ps->i, screenpeek);
751
752
    kclose(fil);

753
754
755
    return 0;
}

756
757
758
////////// TIMER SAVING/RESTORING //////////

static struct {
759
760
    ClockTicks totalclock, totalclocklock;  // engine
    ClockTicks ototalclock, lockclock;  // game
761
762
763
764
} g_timers;

static void G_SaveTimers(void)
{
765
766
767
768
    g_timers.totalclock     = totalclock;
    g_timers.totalclocklock = totalclocklock;
    g_timers.ototalclock    = ototalclock;
    g_timers.lockclock      = lockclock;
769
770
771
772
}

static void G_RestoreTimers(void)
{
Richard Gobeille's avatar
Richard Gobeille committed
773
    totalclock     = g_timers.totalclock;
774
    totalclocklock = g_timers.totalclocklock;
Richard Gobeille's avatar
Richard Gobeille committed
775
776
    ototalclock    = g_timers.ototalclock;
    lockclock      = g_timers.lockclock;
777
778
779
}

//////////
780

781
782
783
784
785
786
787
788
789
790
791
792
793
void G_DeleteSave(savebrief_t const & sv)
{
    if (!sv.isValid())
        return;

    char temp[BMAX_PATH];

    if (G_ModDirSnprintf(temp, sizeof(temp), "%s", sv.path))
    {
        OSD_Printf("G_SavePlayer: file name \"%s\" too long\n", sv.path);
        return;
    }

794
    buildvfs_unlink(temp);
795
796
    Bstrcat(temp, ".ext");
    buildvfs_unlink(temp);
797
798
}

Evan Ramos's avatar
Evan Ramos committed
799
800
801
802
void G_DeleteOldSaves(void)
{
    ReadSaveGameHeaders();

Richard Gobeille's avatar
Richard Gobeille committed
803
    for (int x = 0; x < g_numinternalsaves; ++x)
Evan Ramos's avatar
Evan Ramos committed
804
805
806
807
808
809
810
    {
        menusave_t const & msv = g_internalsaves[x];
        if (msv.isOldVer || msv.isUnreadable)
            G_DeleteSave(msv.brief);
    }
}

Richard Gobeille's avatar
Richard Gobeille committed
811
uint16_t G_CountOldSaves(void)
812
813
814
{
    ReadSaveGameHeaders();

Richard Gobeille's avatar
Richard Gobeille committed
815
816
    int bad = 0;
    for (int x = 0; x < g_numinternalsaves; ++x)
817
818
819
820
821
822
823
824
825
    {
        menusave_t const & msv = g_internalsaves[x];
        if (msv.isOldVer || msv.isUnreadable)
            ++bad;
    }

    return bad;
}

826
int32_t G_SavePlayer(savebrief_t & sv, bool isAutoSave)
827
{
828
829
830
831
#ifdef __ANDROID__
    G_SavePalette();
#endif

832
833
    G_SaveTimers();

834
835
836
    Net_WaitForServer();
    ready2send = 0;

837
    char fn[BMAX_PATH];
838

Evan Ramos's avatar
Evan Ramos committed
839
    errno = 0;
840
    buildvfs_FILE fil;
Evan Ramos's avatar
Evan Ramos committed
841
842
843

    if (sv.isValid())
    {
844
        if (G_ModDirSnprintf(fn, sizeof(fn), "%s", sv.path))
845
        {
Evan Ramos's avatar
Evan Ramos committed
846
847
            OSD_Printf("G_SavePlayer: file name \"%s\" too long\n", sv.path);
            goto saveproblem;
848
        }
849
        fil = buildvfs_fopen_write(fn);
Evan Ramos's avatar
Evan Ramos committed
850
851
852
853
    }
    else
    {
        static char const SaveName[] = "save0000.esv";
854
855
        int const len = G_ModDirSnprintfLite(fn, ARRAY_SIZE(fn), SaveName);
        if (len >= ARRAY_SSIZE(fn)-1)
856
        {
Evan Ramos's avatar
Evan Ramos committed
857
858
            OSD_Printf("G_SavePlayer: could not form automatic save path\n");
            goto saveproblem;
859
        }
860
861
        char * zeros = fn + (len-8);
        fil = savecounter.opennextfile(fn, zeros);
Evan Ramos's avatar
Evan Ramos committed
862
863
        savecounter.count++;
        // don't copy the mod dir into sv.path
864
        Bstrcpy(sv.path, fn + (len-(ARRAY_SIZE(SaveName)-1)));
Evan Ramos's avatar
Evan Ramos committed
865
866
867
868
869
    }

    if (!fil)
    {
        OSD_Printf("G_SavePlayer: failed opening \"%s\" for writing: %s\n",
870
                   fn, strerror(errno));
Evan Ramos's avatar
Evan Ramos committed
871
        goto saveproblem;
872
873
    }

Evan Ramos's avatar
Evan Ramos committed
874
875
    sv.isExt = 0;

876
877
878
    // temporary hack
    ud.user_map = G_HaveUserMap();

879
#ifdef POLYMER
880
    if (videoGetRenderMode() == REND_POLYMER)
881
882
883
        polymer_resetlights();
#endif

884
    VM_OnEvent(EVENT_SAVEGAME, g_player[myconnectindex].ps->i, myconnectindex);
885

Evan Ramos's avatar
Evan Ramos committed
886
    portableBackupSave(sv.path, sv.name, ud.last_stateless_volume, ud.last_stateless_level);
Evan Ramos's avatar
Evan Ramos committed
887

888
    // SAVE!
889
    sv_saveandmakesnapshot(fil, sv.name, 0, 0, 0, 0, isAutoSave);
890

891
    buildvfs_fclose(fil);
892
893
894

    if (!g_netServer && ud.multimode < 2)
    {
895
        OSD_Printf("Saved: %s\n", fn);
896
        Bstrcpy(apStrings[QUOTE_RESERVED4], "Game Saved");
897
898
899
900
901
902
        P_DoQuote(QUOTE_RESERVED4, g_player[myconnectindex].ps);
    }

    ready2send = 1;
    Net_WaitForServer();

903
    G_RestoreTimers();
904

905
    VM_OnEvent(EVENT_POSTSAVEGAME, g_player[myconnectindex].ps->i, myconnectindex);
906

907
    return 0;
Evan Ramos's avatar
Evan Ramos committed
908
909
910
911
912
913
914
915

saveproblem:
    ready2send = 1;
    Net_WaitForServer();

    G_RestoreTimers();

    return -1;
916
}
917

918
int32_t G_LoadPlayerMaybeMulti(savebrief_t & sv)
919
920
921
{
    if (g_netServer || ud.multimode > 1)
    {
922
        Bstrcpy(apStrings[QUOTE_RESERVED4], "Multiplayer Loading Not Yet Supported");
923
924
925
        P_DoQuote(QUOTE_RESERVED4, g_player[myconnectindex].ps);

//        g_player[myconnectindex].ps->gm = MODE_GAME;
926
        return 127;
927
928
929
    }
    else
    {
Evan Ramos's avatar
Evan Ramos committed
930
        int32_t c = G_LoadPlayer(sv);
931
932
        if (c == 0)
            g_player[myconnectindex].ps->gm = MODE_GAME;
933
        return c;
934
935
936
    }
}

937
void G_SavePlayerMaybeMulti(savebrief_t & sv, bool isAutoSave)
938
{
939
    CONFIG_WriteSetup(2);
940

941
942
    if (g_netServer || ud.multimode > 1)
    {
943
        Bstrcpy(apStrings[QUOTE_RESERVED4], "Multiplayer Saving Not Yet Supported");
944
945
946
947
        P_DoQuote(QUOTE_RESERVED4, g_player[myconnectindex].ps);
    }
    else
    {
948
        G_SavePlayer(sv, isAutoSave);
949
950
    }
}
951
952
953
954
955
956

////////// GENERIC SAVING/LOADING SYSTEM //////////

typedef struct dataspec_
{
    uint32_t flags;
957
    void * const ptr;
958
959
960
961
    uint32_t size;
    intptr_t cnt;
} dataspec_t;

962
963
964
965
966
967
968
969
typedef struct dataspec_gv_
{
    uint32_t flags;
    void * ptr;
    uint32_t size;
    intptr_t cnt;
} dataspec_gv_t;

970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
#define SV_DEFAULTCOMPRTHRES 8
static uint8_t savegame_diffcompress;  // 0:none, 1:Ken's LZW in cache1d.c
static uint8_t savegame_comprthres;


#define DS_DYNAMIC 1  // dereference .ptr one more time
#define DS_STRING 2
#define DS_CMP 4
// 8
#define DS_CNT(x) ((sizeof(x))<<3)  // .cnt is pointer to...
#define DS_CNT16 16
#define DS_CNT32 32
#define DS_CNTMASK (8|DS_CNT16|DS_CNT32|64)
// 64
#define DS_LOADFN 128  // .ptr is function that is run when loading
#define DS_SAVEFN 256  // .ptr is function that is run when saving
986
#define DS_NOCHK 1024  // don't check for diffs (and don't write out in dump) since assumed constant throughout demo
Philipp Kutin's avatar
Philipp Kutin committed
987
#define DS_PROTECTFN 512
988
989
#define DS_END (0x70000000)

Richard Gobeille's avatar
Richard Gobeille committed
990
static int32_t ds_getcnt(const dataspec_t *spec)
991
{
Richard Gobeille's avatar
Richard Gobeille committed
992
    int cnt = -1;
993

Richard Gobeille's avatar
Richard Gobeille committed
994
    switch (spec->flags & DS_CNTMASK)
995
    {
Richard Gobeille's avatar
Richard Gobeille committed
996
997
998
        case 0: cnt = spec->cnt; break;
        case DS_CNT16: cnt = *((int16_t *)spec->cnt); break;
        case DS_CNT32: cnt = *((int32_t *)spec->cnt); break;
999
    }
1000

Richard Gobeille's avatar
Richard Gobeille committed
1001
    return cnt;
1002
1003
}

Richard Gobeille's avatar
Richard Gobeille committed
1004
static inline void ds_get(const dataspec_t *spec, void **ptr, int32_t *cnt)
1005
{
Richard Gobeille's avatar
Richard Gobeille committed
1006
1007
    *cnt = ds_getcnt(spec);
    *ptr = (spec->flags & DS_DYNAMIC) ? *((void **)spec->ptr) : spec->ptr;
1008
1009
1010
}

// write state to file and/or to dump
1011
static uint8_t *writespecdata(const dataspec_t *spec, buildvfs_FILE fil, uint8_t *dump)
1012
{
Richard Gobeille's avatar
Richard Gobeille committed
1013
    for (; spec->flags != DS_END; spec++)
1014
    {
Richard Gobeille's avatar
Richard Gobeille committed
1015
        if (spec->flags & (DS_SAVEFN|DS_LOADFN))
1016
        {
Richard Gobeille's avatar
Richard Gobeille committed
1017
1018
            if (spec->flags & DS_SAVEFN)
                (*(void (*)(void))spec->ptr)();
1019
1020
1021
            continue;
        }

Richard Gobeille's avatar
Richard Gobeille committed
1022
        if (!fil && (spec->flags & (DS_NOCHK|DS_CMP|DS_STRING)))
1023
            continue;
Richard Gobeille's avatar
Richard Gobeille committed
1024
        else if (spec->flags & DS_STRING)
1025
        {
1026
            buildvfs_fwrite(spec->ptr, Bstrlen((const char *)spec->ptr), 1, fil);  // not null-terminated!
1027
1028
1029
            continue;
        }

Richard Gobeille's avatar
Richard Gobeille committed
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
        void *  ptr;
        int32_t cnt;

        ds_get(spec, &ptr, &cnt);

        if (cnt < 0)
        {
            OSD_Printf("wsd: cnt=%d, f=0x%x.\n", cnt, spec->flags);
            continue;
        }
1040

1041
1042
1043
        if (!ptr || !cnt)
            continue;

1044
1045
        if (fil)
        {
Richard Gobeille's avatar
Richard Gobeille committed
1046
            if ((spec->flags & DS_CMP) || ((spec->flags & DS_CNTMASK) == 0 && spec->size * cnt <= savegame_comprthres))
1047
                buildvfs_fwrite(ptr, spec->size, cnt, fil);
1048
            else
Richard Gobeille's avatar
Richard Gobeille committed
1049
                dfwrite_LZ4((void *)ptr, spec->size, cnt, fil);
1050
1051
        }

Richard Gobeille's avatar
Richard Gobeille committed
1052
        if (dump && (spec->flags & (DS_NOCHK|DS_CMP)) == 0)
1053
        {
Richard Gobeille's avatar
Richard Gobeille committed
1054
1055
            Bmemcpy(dump, ptr, spec->size * cnt);
            dump += spec->size * cnt;
1056
1057
1058
1059
1060
1061
1062
1063
1064
        }
    }
    return dump;
}

// let havedump=dumpvar&&*dumpvar
// (fil>=0 && havedump): first restore dump from file, then restore state from dump
// (fil<0 && havedump): only restore state from dump
// (fil>=0 && !havedump): only restore state from file
1065
static int32_t readspecdata(const dataspec_t *spec, buildvfs_kfd fil, uint8_t **dumpvar)
1066
{
Richard Gobeille's avatar
Richard Gobeille committed
1067
1068
    uint8_t *  dump = dumpvar ? *dumpvar : NULL;
    auto const sptr = spec;
1069

Richard Gobeille's avatar
Richard Gobeille committed
1070
    for (; spec->flags != DS_END; spec++)
1071
    {
1072
        if (fil == buildvfs_kfd_invalid && spec->flags & (DS_NOCHK|DS_STRING|DS_CMP))  // we're updating
1073
1074
            continue;

Richard Gobeille's avatar
Richard Gobeille committed
1075
        if (spec->flags & (DS_LOADFN|DS_SAVEFN))
1076
        {
Richard Gobeille's avatar
Richard Gobeille committed
1077
1078
            if (spec->flags & DS_LOADFN)
                (*(void (*)())spec->ptr)();
1079
1080
1081
            continue;
        }

Richard Gobeille's avatar
Richard Gobeille committed
1082
        if (spec->flags & (DS_STRING|DS_CMP))  // DS_STRING and DS_CMP is for static data only
1083
        {
Richard Gobeille's avatar
Richard Gobeille committed
1084
1085
1086
            static char cmpstrbuf[32];
            int const siz  = (spec->flags & DS_STRING) ? Bstrlen((const char *)spec->ptr) : spec->size * spec->cnt;
            int const ksiz = kread(fil, cmpstrbuf, siz);
1087

Richard Gobeille's avatar
Richard Gobeille committed
1088
            if (ksiz != siz || Bmemcmp(spec->ptr, cmpstrbuf, siz))
1089
            {
Richard Gobeille's avatar
Richard Gobeille committed
1090
1091
1092
1093
                OSD_Printf("rsd: spec=%s, idx=%d:\n", (char *)sptr->ptr, (int32_t)(spec-sptr));

                if (ksiz!=siz)
                    OSD_Printf("    kread returned %d, expected %d.\n", ksiz, siz);
1094
                else
1095
                    OSD_Printf("    sp->ptr and cmpstrbuf not identical!\n");
Richard Gobeille's avatar
Richard Gobeille committed
1096

1097
1098
1099
1100
1101
                return -1;
            }
            continue;
        }

Richard Gobeille's avatar
Richard Gobeille committed
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
        void *  ptr;
        int32_t cnt;

        ds_get(spec, &ptr, &cnt);

        if (cnt < 0)
        {
            OSD_Printf("rsd: cnt<0... wtf?\n");
            return -1;
        }
1112

1113
1114
1115
        if (!ptr || !cnt)
            continue;