vwad-save: check vwad archive author to quickly reject invalid archives
[k8vavoom.git] / source / server / sv_save.cpp
blob716d2db9b1cc546170d5407750e57e627acd20f7
1 //**************************************************************************
2 //**
3 //** ## ## ## ## ## #### #### ### ###
4 //** ## ## ## ## ## ## ## ## ## ## #### ####
5 //** ## ## ## ## ## ## ## ## ## ## ## ## ## ##
6 //** ## ## ######## ## ## ## ## ## ## ## ### ##
7 //** ### ## ## ### ## ## ## ## ## ##
8 //** # ## ## # #### #### ## ##
9 //**
10 //** Copyright (C) 1999-2006 Jānis Legzdiņš
11 //** Copyright (C) 2018-2023 Ketmar Dark
12 //**
13 //** This program is free software: you can redistribute it and/or modify
14 //** it under the terms of the GNU General Public License as published by
15 //** the Free Software Foundation, version 3 of the License ONLY.
16 //**
17 //** This program is distributed in the hope that it will be useful,
18 //** but WITHOUT ANY WARRANTY; without even the implied warranty of
19 //** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 //** GNU General Public License for more details.
21 //**
22 //** You should have received a copy of the GNU General Public License
23 //** along with this program. If not, see <http://www.gnu.org/licenses/>.
24 //**
25 //**************************************************************************
26 //**
27 //** Archiving: SaveGame I/O.
28 //**
29 //**************************************************************************
30 //#define USE_OLD_SAVE_CODE
32 #ifdef USE_OLD_SAVE_CODE
33 # include "sv_save.old.cpp"
34 #else
36 //#define VXX_DEBUG_SECTION_READER
37 //#define VXX_DEBUG_SECTION_WRITER
39 #include "../gamedefs.h"
40 #include "../host.h"
41 #include "../psim/p_entity.h"
42 #include "../psim/p_worldinfo.h"
43 #include "../psim/p_levelinfo.h"
44 #include "../psim/p_playerreplicationinfo.h"
45 #include "../psim/p_player.h"
46 #include "../filesys/files.h"
47 #ifdef CLIENT
48 # include "../drawer.h" /* VRenderLevelPublic */
49 # include "../client/client.h"
50 # include "../sound/sound.h"
51 #endif
52 #include "../utils/qs_data.h"
53 #include "../decorate/vc_decorate.h" /* ListLoaderCanSkipClass */
54 #include "server.h"
55 #include "sv_local.h"
56 #include "sv_save.h"
58 #include <time.h>
59 #include <unistd.h>
60 #include <sys/time.h>
62 #ifdef _WIN32
63 //# if __GNUC__ < 6
64 static inline struct tm *localtime_r (const time_t *_Time, struct tm *_Tm) {
65 return (localtime_s(_Tm, _Time) ? NULL : _Tm);
67 //# endif
68 #endif
71 // ////////////////////////////////////////////////////////////////////////// //
72 #define VAVOOM_LOADER_CAN_SKIP_CLASSES
75 // ////////////////////////////////////////////////////////////////////////// //
76 enum { NUM_AUTOSAVES = 9 };
79 // ////////////////////////////////////////////////////////////////////////// //
80 #define NEWFMT_VWAD_AUTHOR "k8vavoom engine (save game)"
82 #define NEWFMT_FNAME_SAVE_HEADER "k8vavoom_savegame_header.dat"
83 #define NEWFMT_FNAME_SAVE_DESCR "description.dat"
84 #define NEWFMT_FNAME_SAVE_WADLIST "wadlist.dat"
85 #define NEWFMT_FNAME_SAVE_HWADLIST "wadlist.txt"
86 #define NEWFMT_FNAME_SAVE_CURRMAP "current_map.dat"
87 #define NEWFMT_FNAME_SAVE_MAPLIST "maplist.dat"
88 #define NEWFMT_FNAME_SAVE_CPOINT "checkpoint.dat"
89 #define NEWFMT_FNAME_SAVE_SKILL "skill.dat"
90 #define NEWFMT_FNAME_SAVE_DATE "datetime.dat"
92 #define NEWFMT_FNAME_MAP_STRTBL "strings.dat"
93 #define NEWFMT_FNAME_MAP_NAMES "names.dat"
94 #define NEWFMT_FNAME_MAP_ACSEXPT "acs_exports.dat"
95 #define NEWFMT_FNAME_MAP_WORDINFO "worldinfo.dat"
96 #define NEWFMT_FNAME_MAP_ACTPLYS "active_players.dat"
97 #define NEWFMT_FNAME_MAP_EXPOBJNS "object_nameidx.dat"
98 #define NEWFMT_FNAME_MAP_THINKERS "object_data.dat"
99 #define NEWFMT_FNAME_MAP_ACS "acs_state.dat"
100 #define NEWFMT_FNAME_MAP_ACS_DATA "acs_data.dat"
101 #define NEWFMT_FNAME_MAP_SOUNDS "sounds.dat"
102 #define NEWFMT_FNAME_MAP_GINFO "ginfo.dat"
105 // ////////////////////////////////////////////////////////////////////////// //
106 static VCvarB dbg_save_on_level_exit("dbg_save_on_level_exit", false, "Save before exiting a level.\nNote that after loading this save you prolly won't be able to exit again.", CVAR_PreInit|CVAR_NoShadow/*|CVAR_Archive*/);
107 static VCvarB dbg_load_ignore_wadlist("dbg_load_ignore_wadlist", false, "Ignore list of loaded wads in savegame when hash mated?", CVAR_PreInit|CVAR_NoShadow/*|CVAR_Archive*/);
108 static VCvarB dbg_save_in_old_format("dbg_save_in_old_format", false, "Save games in old format instead of vwads?", CVAR_PreInit|CVAR_NoShadow/*|CVAR_Archive*/);
110 static VCvarI save_compression_level("save_compression_level", "1", "Save file compression level [0..3]", CVAR_Archive|CVAR_NoShadow);
112 static VCvarB sv_new_map_autosave("sv_new_map_autosave", true, "Autosave when entering new map (except first one)?", CVAR_PreInit|CVAR_NoShadow/*|CVAR_Archive*/);
114 static VCvarB sv_save_messages("sv_save_messages", true, "Show messages on save/load?", CVAR_Archive|CVAR_NoShadow);
116 //static VCvarB loader_recalc_z("loader_recalc_z", true, "Recalculate Z on load (this should help with some edge cases)?", CVAR_Archive|CVAR_NoShadow);
117 static VCvarB loader_ignore_kill_on_unarchive("loader_ignore_kill_on_unarchive", false, "Ignore 'Kill On Unarchive' flag when loading a game?", CVAR_PreInit|CVAR_NoShadow/*|CVAR_Archive*/);
119 static VCvarI dbg_save_verbose("dbg_save_verbose", "0", "Slightly more verbose save. DO NOT USE, THIS IS FOR DEBUGGING!\n 0x01: register skips player\n 0x02: registered object\n 0x04: skipped actual player write\n 0x08: skipped unknown object\n 0x10: dump object data writing\b 0x20: dump checkpoints", CVAR_PreInit|CVAR_Archive|CVAR_NoShadow);
121 static VCvarB dbg_checkpoints("dbg_checkpoints", false, "Checkpoint save/load debug dumps", CVAR_NoShadow);
123 extern VCvarB r_precalc_static_lights;
124 extern int r_precalc_static_lights_override; // <0: not set
125 extern VCvarB loader_cache_data;
128 // ////////////////////////////////////////////////////////////////////////// //
129 extern VCvarI Skill;
130 static VCvarB sv_autoenter_checkpoints("sv_autoenter_checkpoints", true, "Use checkpoints for autosaves when possible?", CVAR_Archive|CVAR_NoShadow);
132 static VStr saveFileBase;
135 // ////////////////////////////////////////////////////////////////////////// //
136 #define QUICKSAVE_SLOT (-666)
138 #define EMPTYSTRING "Empty Slot"
139 #define MOBJ_NULL (-1)
141 #define SAVE_DESCRIPTION_LENGTH (24)
142 //#define SAVE_VERSION_TEXT_NO_DATE "Version 1.34.4"
143 #define SAVE_VERSION_TEXT "Version 1.34.12"
144 #define SAVE_VERSION_TEXT_LENGTH (16)
146 static_assert(strlen(SAVE_VERSION_TEXT) <= SAVE_VERSION_TEXT_LENGTH, "oops");
148 #define SAVE_EXTDATA_ID_END (0)
149 #define SAVE_EXTDATA_ID_DATEVAL (1)
150 #define SAVE_EXTDATA_ID_DATESTR (2)
153 // ////////////////////////////////////////////////////////////////////////// //
154 enum gameArchiveSegment_t {
155 ASEG_MAP_HEADER = 101,
156 ASEG_WORLD,
157 ASEG_SCRIPTS,
158 ASEG_SOUNDS,
159 ASEG_END,
163 // extra gameslot data
164 enum {
165 GSLOT_DATA_SKILL = 202,
167 GSLOT_DATA_START = 667,
168 GSLOT_DATA_END = 666,
172 // ////////////////////////////////////////////////////////////////////////// //
173 class VSavedCheckpoint {
174 public:
175 struct EntityInfo {
176 //vint32 index;
177 VEntity *ent;
178 VStr ClassName; // used only in loader
181 public:
182 TArray<QSValue> QSList;
183 TArray<EntityInfo> EList;
184 vint32 ReadyWeapon; // 0: none, otherwise entity index+1
185 vint32 Skill; // -1 means "don't change"
187 VSavedCheckpoint () : QSList(), EList(), ReadyWeapon(0), Skill(-1) {}
188 ~VSavedCheckpoint () { Clear(); }
190 void AddEntity (VEntity *ent) {
191 vassert(ent);
192 const int len = EList.length();
193 for (int f = 0; f < len; ++f) {
194 if (EList[f].ent == ent) return;
196 EntityInfo &ei = EList.alloc();
197 //ei.index = EList.length()-1;
198 ei.ent = ent;
199 ei.ClassName = VStr(ent->GetClass()->GetName());
202 int FindEntity (VEntity *ent) const {
203 if (!ent) return 0;
204 const int len = EList.length();
205 for (int f = 0; f < len; ++f) {
206 if (EList[f].ent == ent) return f+1;
208 // the thing that should not be
209 abort();
210 return -1;
213 void Clear () {
214 QSList.Clear();
215 EList.Clear();
216 ReadyWeapon = 0;
217 Skill = -1; // this should be "no skill"
220 void Serialise (VStream *strm) {
221 vassert(strm);
222 // note that we cannot use `VNTValueIOEx` here, because names are already written!
223 if (strm->IsLoading()) {
224 // load players inventory
225 Clear();
226 // load ready weapon
227 vint32 rw = 0;
228 *strm << STRM_INDEX(rw);
229 ReadyWeapon = rw;
230 // load entity list
231 vint32 entCount;
232 *strm << STRM_INDEX(entCount);
233 if (dbg_save_verbose&0x20) GCon->Logf("*** LOAD: rw=%d; entCount=%d", rw, entCount);
234 if (entCount < 0 || entCount > 1024*1024) Host_Error("invalid entity count (%d)", entCount);
235 for (int f = 0; f < entCount; ++f) {
236 EntityInfo &ei = EList.alloc();
237 ei.ent = nullptr;
238 *strm << ei.ClassName;
239 if (dbg_save_verbose&0x20) GCon->Logf(" ent #%d: '%s'", f+1, *ei.ClassName);
241 // load value list
242 vint32 valueCount;
243 *strm << STRM_INDEX(valueCount);
244 if (dbg_save_verbose&0x20) GCon->Logf(" valueCount=%d", valueCount);
245 for (int f = 0; f < valueCount; ++f) {
246 QSValue &v = QSList.alloc();
247 v.Serialise(*strm);
248 // check for special values
249 if (v.ent == nullptr) {
250 if (v.name.strEqu("\x07 skill")) {
251 if (v.type != QST_Int) Host_Error("invalid skill variable type in checkpoint save");
252 Skill = v.ival;
253 QSList.removeAt(QSList.length()-1);
254 continue;
257 if (dbg_save_verbose&0x20) GCon->Logf(" val #%d(%d): %s", f, v.objidx, *v.toString());
259 if (ReadyWeapon < 0 || ReadyWeapon > EList.length()) Host_Error("invalid ready weapon index (%d)", ReadyWeapon);
260 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "checkpoint data loaded (rw=%d; skill=%d)", ReadyWeapon, Skill);
261 } else {
262 // save players inventory
263 // ready weapon
264 vint32 rw = ReadyWeapon;
265 *strm << STRM_INDEX(rw);
266 // save entity list
267 vint32 entCount = EList.length();
268 *strm << STRM_INDEX(entCount);
269 for (int f = 0; f < entCount; ++f) {
270 EntityInfo &ei = EList[f];
271 *strm << ei.ClassName;
273 const int addvalsCount = 1;
274 // save value list
275 vint32 valueCount = QSList.length()+addvalsCount; // one is reserved for skill
276 *strm << STRM_INDEX(valueCount);
277 // write special variables (only the skill for now)
279 QSValue v = QSValue::CreateInt(nullptr, "\x07 skill", Skill);
280 v.Serialise(*strm);
282 for (auto &&v : QSList) v.Serialise(*strm);
283 //GCon->Logf("*** SAVE: rw=%d; entCount=%d", rw, entCount);
289 // ////////////////////////////////////////////////////////////////////////// //
290 class VSavedMap {
291 public:
292 VName Name;
293 int Index;
294 // for old format
295 // for new format, we are keeping map vwads here
296 TArrayNC<vuint8> Data;
297 // only for old format
298 vuint8 Compressed;
299 vint32 DecompressedSize;
301 public:
302 VSavedMap (bool asNewFormat) : Compressed(asNewFormat ? 69 : 0), DecompressedSize(0) {}
304 inline void ClearData (bool asNewFormat) {
305 Data.clear();
306 Compressed = (asNewFormat ? 69 : 0);
307 DecompressedSize = 0;
310 //inline void SetNewFormat () noexcept { Compressed = 69; }
311 inline bool IsNewFormat () const noexcept { return (Compressed == 69); }
313 VStr GenVWadName () const {
315 RIPEMD160_Ctx ctx;
316 uint8_t hash[RIPEMD160_BYTES];
317 ripemd160_init(&ctx);
318 VStr n = VStr(Name);
319 if (!n.IsEmpty()) {
320 ripemd160_put(&ctx, *n, (unsigned)n.length());
322 ripemd160_finish(&ctx, hash);
323 return "map_"+VStr::buf2hex(&hash, RIPEMD160_BYTES)+".vwad";
325 return va("map_%04d.vwad", Index);
330 // ////////////////////////////////////////////////////////////////////////// //
331 // save slot may contain several maps for hub saves
332 // also, some maps may be overwritten time and time again (hub)
333 // for new format, we will use subdir with vwads to store it all
334 class VSaveSlot {
335 public:
336 VStr Description;
337 VName CurrentMap;
339 TArray<VSavedMap *> Maps; // if there are no maps, it is a checkpoint
341 VSavedCheckpoint CheckPoint;
342 vint32 SavedSkill; // -1: don't change/unknown
344 private:
345 bool newFormat;
347 private:
348 // doesn't destroy stream on error
349 bool LoadSlotOld (int Slot, VStream *strm);
350 // `vwad` should be set
351 bool LoadSlotNew (int Slot, VVWadArchive *vwad);
353 bool SaveToSlotOld (int Slot, VStr &savefilename);
354 bool SaveToSlotNew (int Slot, VStr &savefilename);
356 public:
357 VSaveSlot () : SavedSkill(-1), newFormat(true) {}
358 ~VSaveSlot () { Clear(true); }
360 inline bool IsNewFormat () const noexcept { return newFormat; }
361 // DO NOT USE!
362 inline void ForceNewFormat () noexcept { newFormat = true; }
364 void Clear (bool asNewFormat);
365 bool LoadSlot (int Slot);
366 bool SaveToSlot (int Slot);
368 VSavedMap *FindMap (VName Name);
372 // ////////////////////////////////////////////////////////////////////////// //
373 static VSaveSlot BaseSlot;
376 // ////////////////////////////////////////////////////////////////////////// //
377 class VStreamIOStrMapperWriter : public VStreamIOStrMapper {
378 public:
379 TArray<VStr> strTable;
380 TMap<VStr, int> strMap;
382 public:
383 VV_DISABLE_COPY(VStreamIOStrMapperWriter)
385 inline VStreamIOStrMapperWriter () { strTable.append(VStr::EmptyString); }
386 virtual ~VStreamIOStrMapperWriter () override {}
388 // interface functions for objects and classes streams
389 virtual void io (VStream *strm, VStr &s) override {
390 vint32 sidx;
391 if (s.IsEmpty()) {
392 sidx = 0;
393 } else {
394 auto kp = strMap.get(s);
395 if (kp) {
396 sidx = *kp;
397 } else {
398 sidx = strTable.length();
399 if (sidx >= 1024 * 1024) {
400 strm->SetError();
401 return;
403 strTable.Append(s);
404 strMap.put(s, sidx);
407 //GCon->Logf(NAME_Debug, "WRSM: string #%d: <%s>", sidx, *s);
408 *strm << STRM_INDEX(sidx);
411 void WriteStrings (VStream *st) {
412 if (!st || st->IsError()) return;
413 vint32 tlen = strTable.length();
414 *st << tlen;
415 // string #0 is always empty
416 for (int f = 1; f < tlen; f += 1) {
417 *st << strTable[f];
418 if (st->IsError()) return;
424 class VStreamIOStrMapperLoader : public VStreamIOStrMapper {
425 public:
426 TArray<VStr> strTable;
428 public:
429 VV_DISABLE_COPY(VStreamIOStrMapperLoader)
431 inline VStreamIOStrMapperLoader () {}
432 virtual ~VStreamIOStrMapperLoader () override {}
434 // interface functions for objects and classes streams
435 virtual void io (VStream *strm, VStr &s) override {
436 vint32 sidx = -1;
437 *strm << STRM_INDEX(sidx);
438 if (strm->IsError() || sidx < 0 || sidx >= strTable.length()) {
439 strm->SetError();
440 } else {
441 s = strTable[sidx];
443 //GCon->Logf(NAME_Debug, "RDSM: string #%d: <%s>", sidx, *s);
446 void LoadStrings (VStream *strm) {
447 strTable.clear();
449 vint32 count = -1;
450 *strm << count;
451 if (strm->IsError() || count < 1 || count > 1024*1024) {
452 strm->SetError();
453 return;
456 // string #0 is always empty
457 strTable.setLength(count);
458 for (int f = 1; f < count; f += 1) {
459 *strm << strTable[f];
460 if (strm->IsError()) { strm->SetError(); return; }
466 // ////////////////////////////////////////////////////////////////////////// //
467 class VSaveLoaderStream : public VStream {
468 private:
469 VStream *Stream;
470 VVWadArchive *vwad;
471 VStreamIOStrMapperLoader *strMapper;
473 private:
474 // extended sections support
475 // name and position
476 TMap<VStrCI, int> esections;
477 // current section
478 VStr currsection;
479 VStream *currestream;
480 bool esecclosed;
482 inline VStream *GetCurrStream () const {
483 return (!esecclosed ? currestream : Stream);
486 private:
487 void LoadStringTable () {
488 if (!vwad->FileExists(NEWFMT_FNAME_MAP_STRTBL)) return;
490 VStream *sst = vwad->OpenFile(NEWFMT_FNAME_MAP_STRTBL);
491 if (!sst) { SetError(); return; }
493 strMapper = new VStreamIOStrMapperLoader();
494 strMapper->LoadStrings(sst);
495 const bool err = sst->IsError();
497 VStream::Destroy(sst);
498 AttachStringMapper(strMapper);
499 if (err) SetError();
502 public:
503 TArray<VName> NameRemap;
504 TArray<VObject *> Exports;
505 TArray<VLevelScriptThinker *> AcsExports;
507 public:
508 VV_DISABLE_COPY(VSaveLoaderStream)
510 VSaveLoaderStream (VStream *InStream)
511 : Stream(InStream)
512 , vwad(nullptr)
513 , strMapper(nullptr)
514 , esecclosed(true)
516 bLoading = true;
519 VSaveLoaderStream (VVWadArchive *avwad)
520 : Stream(nullptr)
521 , vwad(avwad)
522 , strMapper(nullptr)
523 , esecclosed(true)
525 bLoading = true;
526 LoadStringTable();
529 virtual ~VSaveLoaderStream () override {
530 AttachStringMapper(nullptr);
531 Close();
532 VStream::Destroy(currestream);
533 VStream::Destroy(Stream);
534 delete strMapper;
537 inline bool IsNewFormat () const noexcept { return !!vwad; }
539 // close current stream, open a new one, assign it to `Stream`
540 void OpenFile (VStr name) {
541 if (vwad) {
542 if (Stream) {
543 if (Stream->IsError()) SetError();
544 VStream::Destroy(Stream);
546 Stream = vwad->OpenFile(name);
547 if (!Stream) {
548 // memleak!
549 Host_Error("cannot find save part named '%s'", *name);
550 } else {
551 Stream->AttachStringMapper(strMapper);
553 } else {
554 // what a brilliant error message! also, memleak
555 //Host_Error("trying to read old save as new save (vfs: %s)", *name);
556 // nope, this may be called for old saves
560 // stream interface
561 virtual bool IsExtendedFormat () const noexcept override {
562 return IsNewFormat();
565 // opening non-existing section is error, so check if necessary
566 // asking for empty name still returns `false`
567 virtual bool HasExtendedSection (VStr name) override {
568 if (IsNewFormat() && !IsError()) {
569 if (name.IsEmpty()) return false;
570 if (currsection.strEquCI(name)) return true;
571 return vwad->FileExists(name);
573 return false;
576 virtual bool ExtendedSection (VStr name) override {
577 if (IsNewFormat() && !IsError()) {
578 if (name.isEmpty()) {
579 #ifdef VXX_DEBUG_SECTION_READER
580 if (!esecclosed) {
581 GCon->Logf(NAME_Debug, "RDX: closed section '%s'", *currsection);
583 #endif
584 esecclosed = true;
585 } else {
586 // check last used section
587 if (currsection.strEquCI(name)) {
588 vassert(currestream != nullptr);
589 #ifdef VXX_DEBUG_SECTION_READER
590 if (esecclosed) {
591 GCon->Logf(NAME_Debug, "RDX: opened section '%s'", *currsection);
593 #endif
594 esecclosed = false;
595 return true;
597 // save current section position
598 if (currestream) {
599 vassert(!currsection.IsEmpty());
600 int pos = currestream->Tell();
601 #ifdef VXX_DEBUG_SECTION_READER
602 GCon->Logf(NAME_Debug, "RDX: saving section '%s' state (pos=%d)", *currsection, pos);
603 #endif
604 if (currestream->IsError() || pos < 0) {
605 SetError();
606 return false;
608 VStream::Destroy(currestream);
609 esections.put(currsection, pos);
610 currsection.clear();
611 esecclosed = true;
613 // open new section stream
614 currestream = vwad->OpenFile(name);
615 if (currestream == nullptr) {
616 #ifdef VXX_DEBUG_SECTION_READER
617 GCon->Logf(NAME_Debug, "RDX: cannot open section '%s'", *name);
618 #endif
619 SetError();
620 return false;
622 currestream->AttachStringMapper(strMapper);
623 // restore section position, if there is any
624 auto kv = esections.get(name);
625 if (kv) {
626 vassert(*kv >= 0);
627 #ifdef VXX_DEBUG_SECTION_READER
628 GCon->Logf(NAME_Debug, "RDX: restored section '%s' state (pos=%d)", *name, *kv);
629 #endif
630 currestream->Seek(*kv);
631 if (currestream->IsError()) {
632 SetError();
633 return false;
636 #ifdef VXX_DEBUG_SECTION_READER
637 GCon->Logf(NAME_Debug, "RDX: opened section '%s'", *name);
638 #endif
639 esecclosed = false;
640 currsection = name;
641 return true;
644 return !IsError();
647 VStr CurrentExtendedSection () override {
648 return (!esecclosed ? currsection : VStr::EmptyString);
651 virtual void SetError () override {
652 VStream::Destroy(currestream);
653 currsection.clear();
654 esecclosed = true;
655 esections.clear();
656 VStream::Destroy(Stream);
658 VStream::SetError();
661 virtual bool IsError () const noexcept override {
662 if (bError) return true;
663 VStream *s = GetCurrStream();
664 return ((s && s->IsError()) || (Stream && Stream->IsError()));
667 virtual void Serialise (void *Data, int Len) override {
668 VStream *s = GetCurrStream();
669 if (s) {
670 s->Serialise(Data, Len);
671 if (s->IsError()) SetError();
672 } else {
673 SetError();
677 virtual void Seek (int Pos) override {
678 VStream *s = GetCurrStream();
679 if (s) {
680 s->Seek(Pos);
681 if (s->IsError()) SetError();
682 } else {
683 SetError();
687 virtual int Tell () override {
688 VStream *s = GetCurrStream();
689 int res = 0;
690 if (s) {
691 res = s->Tell();
692 if (s->IsError()) SetError();
693 } else {
694 SetError();
696 return res;
699 virtual int TotalSize () override {
700 VStream *s = GetCurrStream();
701 int res = 0;
702 if (s) {
703 res = s->TotalSize();
704 if (s->IsError()) SetError();
705 } else {
706 SetError();
708 return res;
711 virtual bool AtEnd () override {
712 VStream *s = GetCurrStream();
713 bool res = true;
714 if (s) {
715 res = s->AtEnd();
716 if (s->IsError()) SetError();
717 } else {
718 SetError();
720 return res;
723 virtual void Flush () override {
724 VStream *s = GetCurrStream();
725 if (s) {
726 s->Flush();
727 if (s->IsError()) SetError();
728 } else {
729 SetError();
733 virtual bool Close () override {
734 bool err = IsError();
735 if (Stream) {
736 if (!err) err = !Stream->Close();
737 VStream::Destroy(Stream);
739 VStream::Destroy(currestream);
740 currsection.clear();
741 esecclosed = true;
742 esections.clear();
743 if (vwad) {
744 if (!err) vwad->Close();
745 delete vwad; vwad = nullptr;
747 if (err) SetError(); // just in case
748 return !err;
751 virtual void io (VSerialisable *&Ref) override {
752 vint32 scpIndex;
753 *this << STRM_INDEX(scpIndex);
754 if (scpIndex == 0) {
755 Ref = nullptr;
756 } else {
757 Ref = AcsExports[scpIndex-1];
759 //GCon->Logf("LOADING: VSerialisable<%s>(%p); idx=%d", (Ref ? *Ref->GetClassName() : "[none]"), (void *)Ref, scpIndex);
762 virtual void io (VName &Name) override {
763 vint32 NameIndex;
764 *this << STRM_INDEX(NameIndex);
765 if (NameIndex < 0 || NameIndex >= NameRemap.length()) {
766 //GCon->Logf(NAME_Error, "SAVEGAME: invalid name index %d (max is %d)", NameIndex, NameRemap.length()-1);
767 Host_Error("SAVEGAME: invalid name index %d (max is %d)", NameIndex, NameRemap.length()-1);
769 Name = NameRemap[NameIndex];
772 virtual void io (VObject *&Ref) override {
773 vint32 TmpIdx;
774 *this << STRM_INDEX(TmpIdx);
775 if (TmpIdx == 0) {
776 Ref = nullptr;
777 } else if (TmpIdx > 0) {
778 if (TmpIdx > Exports.length()) Sys_Error("Bad index %d", TmpIdx);
779 Ref = Exports[TmpIdx-1];
780 //vassert(Ref);
781 //GCon->Logf(NAME_Debug, "IO OBJECT: '%s'", Ref->GetClass()->GetName());
782 } else {
783 GCon->Logf(NAME_Warning, "LOAD: playerbase %d", -TmpIdx-1);
784 Ref = GPlayersBase[-TmpIdx-1];
788 // need to expose others too
789 virtual void io (VStr &s) override { VStream::io(s); }
790 virtual void io (VMemberBase *&o) override { VStream::io(o); }
792 virtual void SerialiseStructPointer (void *&Ptr, VStruct *Struct) override {
793 vint32 TmpIdx;
794 *this << STRM_INDEX(TmpIdx);
795 if (Struct->Name == "sector_t") {
796 Ptr = (TmpIdx >= 0 ? &GLevel->Sectors[TmpIdx] : nullptr);
797 } else if (Struct->Name == "line_t") {
798 Ptr = (TmpIdx >= 0 ? &GLevel->Lines[TmpIdx] : nullptr);
799 } else {
800 if (developer) GCon->Logf(NAME_Warning, "Don't know how to handle pointer to %s", *Struct->Name);
801 Ptr = nullptr;
807 // ////////////////////////////////////////////////////////////////////////// //
808 class VSaveWriterStream : public VStream {
809 private:
810 VVWadNewArchive *vwad;
811 VStreamIOStrMapperWriter *strMapper;
812 VStream *Stream;
814 private:
815 // extended sections support
816 struct ExtSection {
817 VStr name;
818 VMemoryStream *est;
820 TArray<ExtSection *> esections;
821 int currsection;
823 public:
824 TArray<VName> Names;
825 TArray<VObject *> Exports;
826 TArray<vint32> NamesMap;
827 TMapNC<vuint32, vint32> ObjectsMap; // key: object uid; value: internal index
828 TArray</*VLevelScriptThinker*/VSerialisable *> AcsExports;
829 bool skipPlayers;
831 private:
832 inline VStream *GetCurrStream () const {
833 return (currsection >= 0 ? esections[currsection]->est : Stream);
836 void WipeESections () {
837 currsection = -1;
838 for (int f = 0; f < esections.length(); f += 1) {
839 ExtSection *es = esections[f];
840 esections[f] = nullptr;
841 if (es) {
842 if (es->est) { es->est->Close(); delete es->est; }
843 delete es;
846 esections.clear();
849 // flush and destroy extended sections
850 void FlushESections () {
851 if (IsError()) { WipeESections(); return; }
852 if (!vwad) { vassert(esections.length() == 0); return; }
854 for (int f = 0; f < esections.length(); f += 1) {
855 ExtSection *es = esections[f];
856 vassert(es != nullptr);
858 if (!es->est || es->est->IsError()) { SetError(); break; }
860 int level = save_compression_level.asInt();
861 if (level < VWADWR_COMP_DISABLE || level > VWADWR_COMP_BEST) {
862 level = VWADWR_COMP_FAST;
863 save_compression_level = level;
865 // just in case
866 if (es->name.endsWithNoCase(".vwad")) level = VWADWR_COMP_DISABLE;
868 #ifdef VXX_DEBUG_SECTION_WRITER
869 GCon->Logf(NAME_Debug, "WRX: [%d/%d] flushing section '%s' (%d bytes)",
870 f + 1, esections.length(), *es->name, es->est->TotalSize());
871 #endif
873 VStream *xst = vwad->CreateFileDirect(es->name, level);
874 if (!xst) {
875 GCon->Logf(NAME_Error, "cannot create save writer subsection '%s'", *es->name);
876 SetError();
877 break;
879 if (es->est->TotalSize() != 0) {
880 xst->Serialise(es->est->GetArray().Ptr(), es->est->GetArray().length());
882 const bool err = !xst->Close();
883 delete xst;
884 if (err) { SetError(); break; }
887 WipeESections();
890 void Init () {
891 bLoading = false;
892 NamesMap.setLength(VName::GetNumNames());
893 for (int i = 0; i < VName::GetNumNames(); ++i) NamesMap[i] = -1;
894 currsection = -1;
897 // this automatically closes old file
898 // it is safe to call this in non-vwad mode
899 bool CreateFile (VStr name, bool buffit) {
900 if (vwad) {
901 if (Stream != nullptr) CloseFile();
902 #if 0
903 GCon->Logf(NAME_Debug, "CREATE: %s", *name);
904 #endif
905 if (!IsError()) {
906 vassert(Stream == nullptr);
907 while (name.startsWith("/")) name.chopLeft(1);
909 int level = save_compression_level.asInt();
910 if (level < VWADWR_COMP_DISABLE || level > VWADWR_COMP_BEST) {
911 level = VWADWR_COMP_FAST;
912 save_compression_level = level;
914 // just in case
915 if (name.endsWithNoCase(".vwad")) level = VWADWR_COMP_DISABLE;
917 if (buffit) {
918 Stream = vwad->CreateFileBuffered(name, level);
919 } else {
920 Stream = vwad->CreateFileDirect(name, level);
922 if (Stream) {
923 Stream->AttachStringMapper(strMapper);
924 } else {
925 SetError();
929 return !IsError();
933 public:
934 VV_DISABLE_COPY(VSaveWriterStream)
936 // takes ownership of the passed stream
937 VSaveWriterStream (VStream *InStream)
938 : vwad(nullptr)
939 , strMapper(nullptr)
940 , Stream(InStream)
942 Init();
945 // takes ownership of the passed archive object
946 // will properly close and destroy the archive
947 VSaveWriterStream (VVWadNewArchive *avwad)
948 : vwad(avwad)
949 , strMapper(nullptr)
950 , Stream(nullptr)
952 Init();
953 #if 1
954 strMapper = new VStreamIOStrMapperWriter();
955 AttachStringMapper(strMapper);
956 #endif
959 virtual ~VSaveWriterStream () override {
960 Close();
961 delete Stream; Stream = nullptr;
962 delete strMapper; strMapper = nullptr;
965 inline bool IsNewFormat () const noexcept { return (vwad != nullptr); }
967 // it is safe to call this in non-vwad mode
968 void CloseFile () {
969 if (vwad && Stream) {
970 #if 0
971 GCon->Logf(NAME_Debug, "CLOSE: %s", *Stream->GetName());
972 #endif
973 bool err = IsError();
974 if (!err) {
975 Stream->Close();
976 err = Stream->IsError();
978 delete Stream; Stream = nullptr;
979 if (err) SetError();
983 // this automatically closes old file
984 // it is safe to call this in non-vwad mode
985 inline bool CreateFileBuffered (VStr name) { return CreateFile(name, true); }
986 inline bool CreateFileDirect (VStr name) { return CreateFile(name, false); }
988 // stream interface
989 virtual bool IsExtendedFormat () const noexcept override {
990 return IsNewFormat();
993 virtual bool ExtendedSection (VStr name) override {
994 if (IsNewFormat() && !IsError()) {
995 if (name.isEmpty()) {
996 #ifdef VXX_DEBUG_SECTION_READER
997 if (currsection >= 0) {
998 GCon->Logf(NAME_Debug, "WRX: closed section '%s'", *esections[currsection]->name);
1000 #endif
1001 currsection = -1;
1002 } else {
1003 int sidx = 0;
1004 // early exit for the same section
1005 if (currsection >= 0 && esections[currsection]->name.strEquCI(name)) return true;
1006 while (sidx < esections.length() && !esections[sidx]->name.strEquCI(name)) {
1007 sidx += 1;
1009 if (sidx >= esections.length()) {
1010 // new section
1011 ExtSection *es = new ExtSection();
1012 es->name = name;
1013 es->est = new VMemoryStream();
1014 es->est->BeginWrite();
1015 es->est->AttachStringMapper(strMapper);
1016 sidx = esections.length();
1017 esections.Append(es);
1019 currsection = sidx;
1020 #ifdef VXX_DEBUG_SECTION_READER
1021 if (currsection >= 0) {
1022 GCon->Logf(NAME_Debug, "WRX: opened section '%s'", *esections[currsection]->name);
1024 #endif
1025 return true;
1028 return !IsError();
1031 VStr CurrentExtendedSection () override {
1032 return (currsection >= 0 ? esections[currsection]->name : VStr::EmptyString);
1035 virtual void SetError () override {
1036 VStream::Destroy(Stream);
1037 WipeESections();
1038 if (vwad) { delete vwad; vwad = nullptr; }
1039 delete strMapper; strMapper = nullptr;
1040 VStream::SetError();
1043 virtual bool IsError () const noexcept override {
1044 if (bError) return true;
1045 VStream *s = GetCurrStream();
1046 return ((s && s->IsError()) || (Stream && Stream->IsError()));
1049 virtual bool Close () override {
1050 bool err = IsError();
1051 if (vwad) {
1052 if (!err && Stream) {
1053 Stream->Close();
1054 err = Stream->IsError();
1056 delete Stream; Stream = nullptr;
1057 if (err) {
1058 WipeESections();
1059 } else {
1060 FlushESections();
1061 err = IsError();
1063 if (!err) {
1064 if (strMapper) {
1065 int level = save_compression_level.asInt();
1066 if (level < VWADWR_COMP_DISABLE || level > VWADWR_COMP_BEST) {
1067 level = VWADWR_COMP_FAST;
1068 save_compression_level = level;
1070 VStream *wo = vwad->CreateFileDirect(NEWFMT_FNAME_MAP_STRTBL, level);
1071 if (wo) {
1072 strMapper->WriteStrings(wo);
1073 err = !wo->Close();
1074 VStream::Destroy(wo);
1075 } else {
1076 err = true;
1079 if (!err) err = !vwad->Close();
1081 delete vwad; vwad = nullptr;
1082 } else {
1083 if (Stream) { err = !Stream->Close(); Stream = nullptr; }
1085 if (err) SetError(); // just in case
1086 return !err;
1089 virtual void Serialise (void *Data, int Len) override {
1090 VStream *s = GetCurrStream();
1091 if (s) {
1092 s->Serialise(Data, Len);
1093 if (s->IsError()) SetError();
1094 } else {
1095 SetError();
1099 virtual void Seek (int Pos) override {
1100 VStream *s = GetCurrStream();
1101 if (s) {
1102 s->Seek(Pos);
1103 if (s->IsError()) SetError();
1104 } else {
1105 SetError();
1109 virtual int Tell () override {
1110 VStream *s = GetCurrStream();
1111 int res = 0;
1112 if (s) {
1113 res = s->Tell();
1114 if (s->IsError()) SetError();
1115 } else {
1116 SetError();
1118 return res;
1121 virtual int TotalSize () override {
1122 VStream *s = GetCurrStream();
1123 int res = 0;
1124 if (s) {
1125 res = s->TotalSize();
1126 if (s->IsError()) SetError();
1127 } else {
1128 SetError();
1130 return res;
1133 virtual bool AtEnd () override {
1134 VStream *s = GetCurrStream();
1135 bool res = true;
1136 if (s) {
1137 res = s->AtEnd();
1138 if (s->IsError()) SetError();
1139 } else {
1140 SetError();
1142 return res;
1145 virtual void Flush () override {
1146 VStream *s = GetCurrStream();
1147 if (s) {
1148 s->Flush();
1149 if (s->IsError()) SetError();
1150 } else {
1151 SetError();
1155 void RegisterObject (VObject *o) {
1156 if (!o) return;
1157 if (ObjectsMap.has(o->GetUniqueId())) return;
1158 if (skipPlayers) {
1159 VEntity *mobj = Cast<VEntity>(o);
1160 if (mobj != nullptr && (mobj->EntityFlags&VEntity::EF_IsPlayer)) {
1161 // skipping player mobjs
1162 if (dbg_save_verbose&0x01) GCon->Logf("*** SKIP(0) PLAYER MOBJ: <%s>", *mobj->GetClass()->GetFullName());
1163 return;
1166 if (dbg_save_verbose&0x02) GCon->Logf("*** unique object (%u : %s)", o->GetUniqueId(), *o->GetClass()->GetFullName());
1167 Exports.Append(o);
1168 ObjectsMap.put(o->GetUniqueId(), Exports.length());
1171 virtual void io (VSerialisable *&Ref) override {
1172 vint32 scpIndex = 0;
1173 if (Ref) {
1174 if (Ref->GetClassName() != "VAcs") Host_Error("trying to save unknown serialisable of class `%s`", *Ref->GetClassName());
1175 while (scpIndex < AcsExports.length() && AcsExports[scpIndex] != Ref) ++scpIndex;
1176 if (scpIndex >= AcsExports.length()) {
1177 scpIndex = AcsExports.length();
1178 AcsExports.append(Ref);
1180 ++scpIndex;
1182 //GCon->Logf("SAVING: VSerialisable<%s>(%p); idx=%d", (Ref ? *Ref->GetClassName() : "[none]"), (void *)Ref, scpIndex);
1183 *this << STRM_INDEX(scpIndex);
1186 virtual void io (VName &Name) override {
1187 int nidx = Name.GetIndex();
1188 const int olen = NamesMap.length();
1189 if (olen <= nidx) {
1190 NamesMap.setLength(nidx+1);
1191 for (int f = olen; f <= nidx; ++f) NamesMap[f] = -1;
1193 if (NamesMap[nidx] == -1) NamesMap[nidx] = Names.Append(Name);
1194 *this << STRM_INDEX(NamesMap[nidx]);
1197 virtual void io (VObject *&Ref) override {
1198 vint32 TmpIdx;
1199 if (!Ref /*|| !Ref->IsGoingToDie()*/) {
1200 TmpIdx = 0;
1201 } else {
1202 //TmpIdx = ObjectsMap[Ref->GetObjectIndex()];
1203 auto ppp = ObjectsMap.get(Ref->GetUniqueId());
1204 if (!ppp) {
1205 if (skipPlayers) {
1206 VEntity *mobj = Cast<VEntity>(Ref);
1207 if (mobj != nullptr && (mobj->EntityFlags&VEntity::EF_IsPlayer)) {
1208 // skipping player mobjs
1209 if (dbg_save_verbose&0x04) {
1210 GCon->Logf("*** SKIP(1) PLAYER MOBJ: <%s> -- THIS IS HARMLESS", *mobj->GetClass()->GetFullName());
1212 TmpIdx = 0;
1213 *this << STRM_INDEX(TmpIdx);
1214 return;
1217 if ((dbg_save_verbose&0x08) /*|| true*/) {
1218 GCon->Logf("*** unknown object (%u : %s) -- THIS IS HARMLESS", Ref->GetUniqueId(), *Ref->GetClass()->GetFullName());
1220 TmpIdx = 0; // that is how it was done in previous version of the code
1221 } else {
1222 TmpIdx = *ppp;
1224 //TmpIdx = *ObjectsMap.get(Ref->GetUniqueId());
1226 *this << STRM_INDEX(TmpIdx);
1229 // need to expose others too
1230 virtual void io (VStr &s) override { VStream::io(s); }
1231 virtual void io (VMemberBase *&o) override { VStream::io(o); }
1233 virtual void SerialiseStructPointer (void *&Ptr, VStruct *Struct) override {
1234 vint32 TmpIdx;
1235 if (Struct->Name == "sector_t") {
1236 TmpIdx = (Ptr ? (int)((sector_t *)Ptr-GLevel->Sectors) : -1);
1237 } else if (Struct->Name == "line_t") {
1238 TmpIdx = (Ptr ? (int)((line_t *)Ptr-GLevel->Lines) : -1);
1239 } else {
1240 if (developer) GCon->Logf(NAME_Dev, "Don't know how to handle pointer to %s", *Struct->Name);
1241 TmpIdx = -1;
1243 *this << STRM_INDEX(TmpIdx);
1248 // because dedicated server cannot save games yet
1249 #ifdef CLIENT
1250 static bool skipCallbackInited = false;
1251 static VName oldPlrClassName = NAME_None;
1252 static VName newPlrClassName = NAME_None;
1253 static VName clsPlayerExName = NAME_None;
1256 //==========================================================================
1258 // checkSkipClassCB
1260 //==========================================================================
1261 static bool checkSkipClassCB (VObject *self, VName clsname) {
1262 // as gore mod is build into the engine now, we don't need to subclass `VLevel` anymore
1263 // this allows loading of old saves: all old gore mod data will be simply ignored
1264 if (clsname == NAME_Level_K8BDW || clsname == NAME_Level_K8Gore) {
1265 // allow any level descendant
1266 for (VClass *cls = self->GetClass(); cls; cls = cls->GetSuperClass()) {
1267 //if (VStr::strEqu(cls->GetName(), "VLevel")) return true;
1268 if (cls->GetVName() == NAME_VLevel) {
1269 GCon->Logf(NAME_Debug, "VLevel subclass `%s` replaced with `%s` (this is normal gore fix).",
1270 *clsname, self->GetClass()->GetName());
1271 return true;
1274 return false;
1277 //TODO: skip any Gore Mod related things?
1278 if (VStr::strEqu(*clsname, "K8Gore_Blood_SplatterReplacer")) {
1279 // K8Gore_Blood_SplatterReplacer (it is removed)
1280 for (VClass *cls = self->GetClass(); cls; cls = cls->GetSuperClass()) {
1281 if (VStr::strEqu(cls->GetName(), "K8Gore_Blood")) return true;
1282 //if (VStr::strEqu(cls->GetName(), "K8Gore_BloodBase")) return true;
1286 if (ListLoaderCanSkipClass.has(clsname)) return true;
1288 return false;
1292 //==========================================================================
1294 // ldrTranslatePlayerClassName
1296 //==========================================================================
1297 static VName ldrTranslatePlayerClassName (VObject *self, VName clsname) {
1298 if (clsname != oldPlrClassName) return NAME_None;
1299 // check if it is a descendant of "PlayerEx"
1300 for (VClass *cls = self->GetClass(); cls; cls = cls->GetSuperClass()) {
1301 if (cls->GetVName() == clsPlayerExName) {
1302 GCon->Logf(NAME_Debug, "PlayerEx subclass `%s` translated to `%s` (this is normal loader fix).",
1303 *clsname, *newPlrClassName);
1304 return newPlrClassName;
1307 GCon->Logf(NAME_Debug, "class `%s` is not a child of `PlayerEx`, skipped translation (this is normal loader fix).",
1308 *clsname);
1309 return NAME_None;
1313 //==========================================================================
1315 // SV_SetupSkipCallback
1317 //==========================================================================
1318 static void SV_SetupSkipCallback () {
1319 if (skipCallbackInited) return;
1320 skipCallbackInited = true;
1321 VObject::CanSkipReadingClassCBList.append(&checkSkipClassCB);
1322 // translate `Player` to `K8VPlayer`
1323 oldPlrClassName = VName("Player");
1324 newPlrClassName = VName("K8VPlayer");
1325 clsPlayerExName = VName("PlayerEx");
1326 VObject::ClassNameTranslationCBList.append(&ldrTranslatePlayerClassName);
1327 // translate old `Level` class to `VLevel`
1328 // nope, don't do that
1329 //VObject::IOClassNameTranslation.put(VName("Level"), NAME_VLevel);
1331 #endif
1334 //==========================================================================
1336 // SV_GetSavesDir
1338 //==========================================================================
1339 static VStr SV_GetSavesDir () {
1340 return FL_GetSavesDir();
1344 //==========================================================================
1346 // GetSaveSlotDirectoryPrefixOld
1348 //==========================================================================
1349 static VStr GetSaveSlotCommonDirectoryPrefixOld0 () {
1350 vuint32 hash;
1351 (void)SV_GetModListHashOld(&hash);
1352 VStr pfx = VStr::buf2hex(&hash, 4);
1353 return pfx;
1357 //==========================================================================
1359 // GetSaveSlotCommonDirectoryPrefixOld1
1361 //==========================================================================
1362 static VStr GetSaveSlotCommonDirectoryPrefixOld1 () {
1363 vuint64 hash = SV_GetModListHashOld(nullptr);
1364 VStr pfx = VStr::buf2hex(&hash, 8);
1365 return pfx;
1369 //==========================================================================
1371 // GetSaveSlotDirectoryPrefix
1373 //==========================================================================
1374 static VStr GetSaveSlotCommonDirectoryPrefix () {
1375 vuint64 hash = SV_GetModListHash(nullptr);
1376 VStr pfx = VStr::buf2hex(&hash, 8);
1377 return pfx;
1381 //==========================================================================
1383 // UpgradeSaveDirectories
1385 // rename old save directory
1387 //==========================================================================
1388 static void UpgradeSaveDirectories () {
1389 VStr xdir = SV_GetSavesDir();
1390 VStr newpath = xdir.appendPath(GetSaveSlotCommonDirectoryPrefix());
1392 if (!newpath.IsEmpty()) {
1393 VStr oldpath;
1395 oldpath = xdir.appendPath(GetSaveSlotCommonDirectoryPrefixOld0());
1396 //GCon->Logf("OLD0: <%s> -> <%s>", *oldpath, *newpath);
1397 if (!oldpath.IsEmpty() && oldpath != newpath) rename(*oldpath, *newpath);
1399 oldpath = xdir.appendPath(GetSaveSlotCommonDirectoryPrefixOld1());
1400 //GCon->Logf("OLD1: <%s> -> <%s>", *oldpath, *newpath);
1401 if (!oldpath.IsEmpty() && oldpath != newpath) rename(*oldpath, *newpath);
1406 //==========================================================================
1408 // GetDiskSavesPath
1410 //==========================================================================
1411 static VStr GetDiskSavesPath () {
1412 UpgradeSaveDirectories();
1413 return SV_GetSavesDir().appendPath(GetSaveSlotCommonDirectoryPrefix());
1417 //==========================================================================
1419 // GetDiskSavesPathNoHash
1421 //==========================================================================
1422 static VStr GetDiskSavesPathNoHash () {
1423 UpgradeSaveDirectories();
1424 return SV_GetSavesDir();
1428 //==========================================================================
1430 // SV_GetSaveHash
1432 // returns hash of savegame directory
1434 //==========================================================================
1435 VStr SV_GetSaveHash () {
1436 return GetSaveSlotCommonDirectoryPrefix();
1440 //==========================================================================
1442 // isBadSlotIndex
1444 // checking for "bad" index is more common
1446 //==========================================================================
1447 static inline bool isBadSlotIndex (int slot) {
1448 return (slot != QUICKSAVE_SLOT && (slot < -64 || slot > 63));
1452 //==========================================================================
1454 // UpdateSaveDirWadList
1456 // writes text file with list of active wads.
1457 // this file is not used by the engine, and is written solely for user.
1459 //==========================================================================
1460 static void UpdateSaveDirWadList () {
1461 VStr svpfx = GetDiskSavesPath();
1462 FL_CreatePath(svpfx); // just in case
1463 //GCon->Logf(NAME_Debug, "UpdateSaveDirWadList: svpfx=<%s>", *svpfx);
1464 svpfx = svpfx.appendPath("wadlist.txt");
1465 VStream *res = FL_OpenSysFileWrite(svpfx);
1466 if (res) {
1467 res->writef("%s\n", "# automatically generated, and purely informational");
1468 auto wadlist = FL_GetWadPk3ListSmall();
1469 for (auto &&wadname : wadlist) {
1470 //GCon->Logf(NAME_Debug, " wad=<%s>", *wadname);
1471 res->writef("%s\n", *wadname);
1473 VStream::Destroy(res);
1478 //==========================================================================
1480 // GetSaveSlotBaseFileName
1482 // if slot is < 0, this is autosave slot
1483 // QUICKSAVE_SLOT is quicksave slot
1484 // returns empty string for invalid slot
1486 //==========================================================================
1487 static VStr GetSaveSlotBaseFileName (int slot) {
1488 if (isBadSlotIndex(slot)) return VStr();
1489 VStr pfx = GetSaveSlotCommonDirectoryPrefix();
1490 if (slot == QUICKSAVE_SLOT) pfx += "/quicksave";
1491 else if (slot < 0) pfx += va("/autosave_%02d", -slot);
1492 else pfx += va("/normsave_%02d", slot+1);
1493 return pfx;
1497 //==========================================================================
1499 // SV_OpenSlotFileRead
1501 // open savegame slot file if it exists
1502 // sets `saveFileBase`
1504 //==========================================================================
1505 static VStream *SV_OpenSlotFileRead (int slot) {
1506 saveFileBase.clear();
1507 if (isBadSlotIndex(slot)) return nullptr;
1509 VStr seenVWad, seenVsg;
1511 // search save subdir
1512 auto svdir = GetDiskSavesPath();
1513 auto dir = Sys_OpenDir(svdir);
1514 if (dir) {
1515 auto svpfx = GetSaveSlotBaseFileName(slot).extractFileName();
1516 for (;;) {
1517 VStr fname = Sys_ReadDir(dir);
1518 if (fname.isEmpty()) break;
1519 if (fname.startsWithNoCase(svpfx)) {
1520 if (seenVsg.isEmpty() && fname.endsWithNoCase(".vsg")) seenVsg = fname;
1521 else if (seenVWad.isEmpty() && fname.endsWithNoCase(".vwad")) seenVWad = fname;
1522 if (!seenVWad.isEmpty()) break;
1525 Sys_CloseDir(dir);
1526 if (!seenVWad.isEmpty()) {
1527 saveFileBase = svdir.appendPath(seenVWad);
1528 } else if (!seenVsg.isEmpty()) {
1529 saveFileBase = svdir.appendPath(seenVsg);
1530 } else {
1531 saveFileBase.clear();
1533 if (!saveFileBase.isEmpty()) {
1534 VStream *st = FL_OpenSysFileRead(saveFileBase);
1535 if (st != nullptr) return st;
1536 saveFileBase.clear();
1540 return nullptr;
1544 //==========================================================================
1546 // SV_OpenSlotFileReadWithFmt
1548 // sets `arc` to `nullptr` for old-style saves
1549 // old format header is skipped
1551 // return `nullptr` on error, otherwise a stream
1552 // WARNING! returned stream is owned by `arc`, if `arc` is not `nullptr`!
1554 //==========================================================================
1555 static VStream *SV_OpenSlotFileReadWithFmt (int Slot, VVWadArchive *&vwad) {
1556 VStream *Strm = nullptr;
1557 char VersionText[SAVE_VERSION_TEXT_LENGTH+1];
1558 vwad = nullptr;
1560 #if 0
1561 GCon->Logf(NAME_Debug, "SV_OpenSlotFileReadWithFmt: Slot=%d...", Slot);
1562 #endif
1563 Strm = SV_OpenSlotFileRead(Slot);
1565 if (Strm) {
1566 #if 0
1567 GCon->Log(NAME_Debug, "...checking");
1568 #endif
1570 // is it a vwad?
1571 memset(VersionText, 0, 4);
1573 Strm->Serialise(VersionText, 4);
1574 if (Strm->IsError()) {
1575 VStream::Destroy(Strm);
1576 saveFileBase.clear();
1577 return nullptr;
1580 if (memcmp(VersionText, "VWAD", 4) == 0) {
1581 #if 0
1582 GCon->Log(NAME_Debug, "....VWAD");
1583 #endif
1584 Strm->Seek(0);
1585 if (Strm->IsError()) { VStream::Destroy(Strm); return nullptr; }
1586 vwad = new VVWadArchive(VStr(va("slot_%02d_main", Slot)), Strm, true);
1587 if (!vwad->IsOpen()) {
1588 // the stream is already destroyed
1589 delete vwad; vwad = nullptr;
1590 saveFileBase.clear();
1591 return nullptr;
1593 // check author
1594 if (vwad->GetAuthor() != NEWFMT_VWAD_AUTHOR) {
1595 #if 0
1596 GCon->Log(NAME_Debug, "invalid save VWAD signature");
1597 #endif
1598 // the stream will be destroyed automatically
1599 delete vwad; vwad = nullptr;
1600 saveFileBase.clear();
1601 return nullptr;
1603 } else {
1604 #if 0
1605 GCon->Log(NAME_Debug, "....VSG!");
1606 #endif
1607 vassert(SAVE_VERSION_TEXT_LENGTH > 4);
1608 memset(VersionText + 4, 0, sizeof(VersionText) - 4);
1609 // check the version text
1610 Strm->Serialise(VersionText + 4, SAVE_VERSION_TEXT_LENGTH - 4);
1611 if (VStr::Cmp(VersionText, SAVE_VERSION_TEXT) /*&& VStr::Cmp(VersionText, SAVE_VERSION_TEXT_NO_DATE)*/) {
1612 VStream::Destroy(Strm);
1613 saveFileBase.clear();
1614 return nullptr;
1619 return Strm;
1623 //==========================================================================
1625 // removeSlotSaveFiles
1627 // user can rename file to different case
1628 // kill 'em all!
1630 //==========================================================================
1631 static bool removeSlotSaveFiles (int slot, VStr keepFileName) {
1632 TArray<VStr> tokill;
1633 if (isBadSlotIndex(slot)) return false;
1635 // search save subdir
1636 auto svdir = GetDiskSavesPath();
1637 auto dir = Sys_OpenDir(svdir);
1638 if (dir) {
1639 auto svpfx = GetSaveSlotBaseFileName(slot).extractFileName();
1640 for (;;) {
1641 VStr fname = Sys_ReadDir(dir);
1642 if (fname.isEmpty()) break;
1643 if (fname.startsWithNoCase(svpfx) &&
1644 (fname.endsWithNoCase(".vsg") || fname.endsWithNoCase(".vwad") ||
1645 fname.endsWithNoCase(".lmap")))
1647 VStr fn = svdir.appendPath(fname);
1648 if (fn != keepFileName) tokill.append(fn);
1649 } else if (fname.endsWith(".$$$")) {
1650 // various broken temp saves
1651 VStr fn = svdir.appendPath(fname);
1652 tokill.append(fn);
1655 Sys_CloseDir(dir);
1658 for (int f = 0; f < tokill.length(); ++f) Sys_FileDelete(tokill[f]);
1659 return true;
1663 //==========================================================================
1665 // SV_CreateSlotFileWrite
1667 // create new savegame slot file
1668 // also, removes any existing savegame file for the same slot
1669 // sets `saveFileBase`
1671 // returned stream name should be proper disk file name
1672 // it is temporary, and should be renamed on success
1674 //==========================================================================
1675 static VStream *SV_CreateSlotFileWrite (int slot, VStr descr, bool asNew) {
1676 saveFileBase.clear();
1677 if (isBadSlotIndex(slot)) return nullptr;
1678 if (slot == QUICKSAVE_SLOT) descr = VStr();
1680 auto svpfx = GetDiskSavesPathNoHash().appendPath(GetSaveSlotBaseFileName(slot));
1681 FL_CreatePath(svpfx.extractFilePath()); // just in case
1683 // normalize description
1684 VStr newdesc;
1685 for (int f = 0; f < descr.length(); ++f) {
1686 char ch = descr[f];
1687 if (!ch) continue;
1688 if (ch >= '0' && ch <= '9') { newdesc += ch; continue; }
1689 if (ch >= 'A' && ch <= 'Z') { newdesc += ch-'A'+'a'; continue; } // poor man's tolower()
1690 if (ch >= 'a' && ch <= 'z') { newdesc += ch; continue; }
1691 // replace with underscore
1692 if (newdesc.length() == 0) continue;
1693 if (newdesc[newdesc.length()-1] == '_') continue;
1694 newdesc += "_";
1696 while (newdesc.length() && newdesc[0] == '_') newdesc.chopLeft(1);
1697 while (newdesc.length() && newdesc[newdesc.length()-1] == '_') newdesc.chopRight(1);
1698 // finalize file name
1699 if (newdesc.length()) { svpfx += "_"; svpfx += newdesc; }
1701 if (asNew) svpfx += ".vwad"; else svpfx += ".vsg";
1702 //GCon->Logf(NAME_Debug, "SAVE: <%s>", *svpfx);
1703 saveFileBase = svpfx;
1704 VStream *res = FL_OpenSysFileWrite(svpfx + ".$$$");
1706 if (res) {
1707 VStream::Destroy(res);
1708 removeSlotSaveFiles(slot);
1709 res = FL_OpenSysFileWrite(svpfx);
1710 if (res) UpdateSaveDirWadList();
1713 return res;
1717 //==========================================================================
1719 // SV_SaveFailed
1721 // call this to properly close the stream, and remove temp file
1723 //==========================================================================
1724 static void SV_SaveFailed (VStr fname, int Slot) {
1725 vassert(!fname.IsEmpty());
1726 unlink(*fname);
1730 //==========================================================================
1732 // SV_SaveSuccess
1734 // call this to properly close the stream, and rename temp file
1736 //==========================================================================
1737 static void SV_SaveSuccess (VStr fname, int Slot) {
1738 vassert(!fname.IsEmpty());
1739 vassert(!saveFileBase.IsEmpty());
1740 if (fname != saveFileBase) {
1741 #ifdef WIN32
1742 unlink(*saveFileBase);
1743 #endif
1744 if (rename(*fname, *saveFileBase) != 0) {
1745 GCon->Logf(NAME_Error, "Cannot rename save file for slot #%d!", Slot);
1746 return;
1749 removeSlotSaveFiles(Slot, saveFileBase);
1750 UpdateSaveDirWadList();
1754 #ifdef CLIENT
1755 //==========================================================================
1757 // SV_DeleteSlotFile
1759 //==========================================================================
1760 static bool SV_DeleteSlotFile (int slot) {
1761 if (isBadSlotIndex(slot)) return false;
1762 return removeSlotSaveFiles(slot, VStr::EmptyString);
1764 #endif
1767 // ////////////////////////////////////////////////////////////////////////// //
1768 struct TTimeVal {
1769 int secs; // actually, unsigned
1770 int usecs;
1771 // for 2030+
1772 int secshi;
1774 inline bool operator < (const TTimeVal &tv) const {
1775 if (secshi < tv.secshi) return true;
1776 if (secshi > tv.secshi) return false;
1777 if (secs < tv.secs) return true;
1778 if (secs > tv.secs) return false;
1779 return false;
1784 //==========================================================================
1786 // GetTimeOfDay
1788 //==========================================================================
1789 static void GetTimeOfDay (TTimeVal *tvres) {
1790 if (!tvres) return;
1791 tvres->secshi = 0;
1792 timeval tv;
1793 if (gettimeofday(&tv, nullptr)) {
1794 tvres->secs = 0;
1795 tvres->usecs = 0;
1796 } else {
1797 tvres->secs = (int)(tv.tv_sec&0xffffffff);
1798 tvres->usecs = (int)tv.tv_usec;
1799 tvres->secshi = (int)(((uint64_t)tv.tv_sec)>>32);
1804 //==========================================================================
1806 // TimeVal2Str
1808 //==========================================================================
1809 static VStr TimeVal2Str (const TTimeVal *tvin, bool forAutosave=false) {
1810 timeval tv;
1811 tv.tv_sec = (((uint64_t)tvin->secs)&0xffffffff)|(((uint64_t)tvin->secshi)<<32);
1812 //tv.tv_usec = tvin->usecs;
1813 tm ctm;
1814 #ifndef STK_TIMET_FIX
1815 if (localtime_r(&tv.tv_sec, &ctm)) {
1816 #else
1817 time_t tsec = tv.tv_sec;
1818 if (localtime_r(&tsec, &ctm)) {
1819 #endif
1820 if (forAutosave) {
1821 // for autosave
1822 return VStr(va("%02d:%02d", (int)ctm.tm_hour, (int)ctm.tm_min));
1823 } else {
1824 // full
1825 return VStr(va("%04d/%02d/%02d %02d:%02d:%02d",
1826 (int)(ctm.tm_year+1900),
1827 (int)ctm.tm_mon+1,
1828 (int)ctm.tm_mday,
1829 (int)ctm.tm_hour,
1830 (int)ctm.tm_min,
1831 (int)ctm.tm_sec));
1833 } else {
1834 return VStr("unknown");
1839 //==========================================================================
1841 // SkipExtData
1843 // skip extended data
1845 //==========================================================================
1846 static bool SkipExtData (VStream *Strm) {
1847 for (;;) {
1848 vint32 id, size;
1849 *Strm << STRM_INDEX(id);
1850 if (id == SAVE_EXTDATA_ID_END) break;
1851 *Strm << STRM_INDEX(size);
1852 if (size < 0 || size > 65536) return false;
1853 // skip data
1854 Strm->Seek(Strm->Tell()+size);
1856 return true;
1860 //==========================================================================
1862 // LoadDateStrExtData
1864 // get date string, or use timeval to build it
1865 // empty string means i/o error
1867 //==========================================================================
1868 static VStr LoadDateStrExtData (VStream *Strm) {
1869 bool tvvalid = false;
1870 TTimeVal tv;
1871 memset((void *)&tv, 0, sizeof(tv));
1872 VStr res;
1873 for (;;) {
1874 vint32 id, size;
1875 *Strm << STRM_INDEX(id);
1876 if (id == SAVE_EXTDATA_ID_END) break;
1877 *Strm << STRM_INDEX(size);
1878 if (size < 0 || size > 65536) return VStr();
1880 if (id == SAVE_EXTDATA_ID_DATEVAL && size == (vint32)sizeof(tv)) {
1881 tvvalid = true;
1882 Strm->Serialise(&tv, sizeof(tv));
1883 continue;
1886 if (id == SAVE_EXTDATA_ID_DATESTR && size > 0 && size < 64) {
1887 char buf[65];
1888 memset(buf, 0, sizeof(buf));
1889 Strm->Serialise(buf, size);
1890 if (buf[0]) res = VStr(buf);
1891 continue;
1894 // skip unknown data
1895 Strm->Seek(Strm->Tell()+size);
1897 if (res.length() == 0) {
1898 if (tvvalid) {
1899 res = TimeVal2Str(&tv);
1900 } else {
1901 res = VStr("UNKNOWN");
1904 return res;
1908 //==========================================================================
1910 // LoadDateTValExtData
1912 //==========================================================================
1913 static bool LoadDateTValExtData (VStream *Strm, TTimeVal *tv) {
1914 memset((void *)tv, 0, sizeof(*tv));
1915 for (;;) {
1916 vint32 id, size;
1917 *Strm << STRM_INDEX(id);
1918 if (id == SAVE_EXTDATA_ID_END) break;
1919 //fprintf(stderr, " id=%d\n", id);
1920 *Strm << STRM_INDEX(size);
1921 if (size < 0 || size > 65536) break;
1923 if (id == SAVE_EXTDATA_ID_DATEVAL && size == (vint32)sizeof(*tv)) {
1924 Strm->Serialise(tv, sizeof(tv));
1925 //fprintf(stderr, " found TV[%s] (%s)\n", *TimeVal2Str(tv), (Strm->IsError() ? "ERROR" : "OK"));
1926 return !Strm->IsError();
1929 // skip unknown data
1930 Strm->Seek(Strm->Tell()+size);
1932 return false;
1936 //==========================================================================
1938 // VSaveSlot::Clear
1940 //==========================================================================
1941 void VSaveSlot::Clear (bool asNewFormat) {
1942 Description.Clean();
1943 CurrentMap = NAME_None;
1944 SavedSkill = -1;
1945 for (int i = 0; i < Maps.length(); ++i) { delete Maps[i]; Maps[i] = nullptr; }
1946 Maps.Clear();
1947 CheckPoint.Clear();
1948 //if (vwad) { delete vwad; vwad = nullptr; }
1949 newFormat = asNewFormat;
1953 //==========================================================================
1955 // CheckWadCompName
1957 //==========================================================================
1958 static bool CheckWadCompName (VStr s, VStr wl) {
1959 if (s == wl) return true;
1960 if (s.strEquCI(wl)) return true;
1961 VStr sext = s.ExtractFileExtension();
1962 if (!s.strEquCI(".pk3") && !s.strEquCI(".vwad")) return false;
1963 VStr wext = wl.ExtractFileExtension();
1964 if (!wext.strEquCI(".pk3") && !wext.strEquCI(".vwad")) return false;
1965 s = s.StripExtension();
1966 wl = wl.StripExtension();
1967 return s.strEquCI(wl);
1971 //==========================================================================
1973 // CheckModList
1975 // load mod list, and compare with the current one
1977 //==========================================================================
1978 static bool CheckModList (VStream *Strm, int Slot, bool oldFormat=false, bool silent=false) {
1979 vassert(Strm != nullptr);
1981 vint32 wcount = -1;
1982 *Strm << wcount;
1984 if (Strm->IsError()) {
1985 if (!silent) {
1986 GCon->Logf(NAME_Error, "Invalid savegame #%d (error reading modlist)", Slot);
1988 return !(oldFormat || !dbg_load_ignore_wadlist.asBool());
1991 if (wcount < 1 || wcount > 8192) {
1992 if (!silent) {
1993 GCon->Logf(NAME_Error, "Invalid savegame #%d (bad number of mods: %d)", Slot, wcount);
1995 return false;
1998 TArray<VStr> xwadlist;
1999 for (int f = 0; f < wcount; ++f) {
2000 VStr s;
2001 *Strm << s;
2002 if (Strm->IsError()) {
2003 if (!silent) {
2004 GCon->Logf(NAME_Error, "Invalid savegame #%d (error reading modlist)", Slot);
2006 return !(oldFormat || !dbg_load_ignore_wadlist.asBool());
2008 xwadlist.Append(s);
2011 if (!dbg_load_ignore_wadlist.asBool()) {
2012 bool ok = false;
2013 auto wadlist = FL_GetWadPk3List();
2015 if (wcount == wadlist.length()) {
2016 ok = true;
2017 for (int f = 0; ok && f < wcount; ++f) ok = CheckWadCompName(xwadlist[f], wadlist[f]);
2020 if (!ok) {
2021 auto wadlistNew = FL_GetWadPk3ListSmall();
2022 if (wcount == wadlistNew.length()) {
2023 ok = true;
2024 for (int f = 0; ok && f < wcount; ++f) ok = CheckWadCompName(xwadlist[f], wadlistNew[f]);
2028 if (!ok) {
2029 if (!silent) {
2030 GCon->Logf(NAME_Error, "Invalid savegame #%d (bad modlist)", Slot);
2032 return false;
2036 return true;
2040 //==========================================================================
2042 // VSaveSlot::LoadSlotOld
2044 // doesn't destroy stream on error
2046 //==========================================================================
2047 bool VSaveSlot::LoadSlotOld (int Slot, VStream *Strm) {
2048 Clear(false);
2050 *Strm << Description;
2051 if (Strm->IsError()) return false;
2053 // skip extended data
2054 if (true/*VStr::Cmp(VersionText, SAVE_VERSION_TEXT) == 0*/) {
2055 if (!SkipExtData(Strm) || Strm->IsError()) {
2056 return false;
2060 // check list of loaded modules
2061 if (!CheckModList(Strm, Slot, true)) return false;
2063 VStr TmpName;
2064 *Strm << TmpName;
2065 CurrentMap = *TmpName;
2067 vint32 NumMaps;
2068 *Strm << STRM_INDEX(NumMaps);
2069 for (int i = 0; i < NumMaps; ++i) {
2070 VSavedMap *Map = new VSavedMap(false);
2071 Maps.Append(Map);
2072 Map->Index = Maps.length() - 1;
2073 vassert(Map->Index == i);
2074 vassert(!Map->IsNewFormat());
2075 vint32 DataLen;
2076 *Strm << TmpName << Map->Compressed << STRM_INDEX(Map->DecompressedSize) << STRM_INDEX(DataLen);
2077 Map->Name = *TmpName;
2078 Map->Data.setLength(DataLen);
2079 Strm->Serialise(Map->Data.Ptr(), Map->Data.length());
2080 #if 0
2081 GCon->Logf(NAME_Debug, "Map #%d: %s (cp:%d; ds:%d; uds:%d)", i, *Map->Name,
2082 Map->Compressed, DataLen, Map->DecompressedSize);
2083 #endif
2086 //HACK: if `NumMaps` is 0, we're loading a checkpoint
2087 if (NumMaps == 0) {
2088 // load players inventory
2089 VSavedCheckpoint &cp = CheckPoint;
2090 cp.Serialise(Strm);
2091 SavedSkill = cp.Skill;
2092 } else {
2093 VSavedCheckpoint &cp = CheckPoint;
2094 cp.Clear();
2095 if (!Strm->AtEnd()) {
2096 vuint32 seg = 0xffffffff;
2097 *Strm << STRM_INDEX_U(seg);
2098 if (!Strm->IsError() && seg == GSLOT_DATA_START) {
2099 for (;;) {
2100 seg = 0xffffffff;
2101 *Strm << STRM_INDEX_U(seg);
2102 if (Strm->IsError()) break;
2103 if (seg == GSLOT_DATA_END) break;
2104 if (seg == GSLOT_DATA_SKILL) {
2105 vint32 sk;
2106 *Strm << STRM_INDEX(sk);
2107 if (sk < 0 || sk > 31) {
2108 GCon->Logf(NAME_Warning, "Invalid savegame #%d skill (%d)", Slot, sk);
2109 } else {
2110 SavedSkill = sk;
2112 continue;
2114 GCon->Logf(NAME_Warning, "Invalid savegame #%d extra segment (%u)", Slot, seg);
2115 return false;
2121 bool err = Strm->IsError();
2123 Host_ResetSkipFrames();
2125 if (err) {
2126 GCon->Logf(NAME_Error, "Error loading savegame #%d data", Slot);
2127 return false;
2130 vassert(!saveFileBase.isEmpty());
2131 return true;
2135 //==========================================================================
2137 // VSaveSlot::LoadSlotNew
2139 // `vwad` should be set
2141 //==========================================================================
2142 bool VSaveSlot::LoadSlotNew (int Slot, VVWadArchive *vwad) {
2143 Clear(true);
2145 VStream *Strm = vwad->OpenFile(NEWFMT_FNAME_SAVE_HEADER);
2146 if (!Strm) return false;
2148 if (Strm->GetGroupName() != "<savegame>") {
2149 VStream::Destroy(Strm);
2150 GCon->Logf(NAME_Error, "Invalid savegame #%d header", Slot);
2151 return false;
2155 vuint8 savever = 255;
2156 *Strm << savever;
2157 if (Strm->IsError()) { VStream::Destroy(Strm); return false; }
2158 VStream::Destroy(Strm);
2159 if (savever != 0) {
2160 GCon->Logf(NAME_Error, "Invalid savegame #%d version", Slot);
2161 return false;
2164 Strm = vwad->OpenFile(NEWFMT_FNAME_SAVE_DESCR);
2165 if (!Strm) return false;
2166 *Strm << Description;
2167 if (Strm->IsError()) { VStream::Destroy(Strm); return false; }
2168 VStream::Destroy(Strm);
2170 // check list of loaded modules
2171 if (!dbg_load_ignore_wadlist.asBool()) {
2172 Strm = vwad->OpenFile(NEWFMT_FNAME_SAVE_WADLIST);
2173 if (!Strm) return false;
2174 const bool modok = CheckModList(Strm, Slot);
2175 VStream::Destroy(Strm);
2176 if (!modok) return false;
2179 // load current map name
2180 Strm = vwad->OpenFile(NEWFMT_FNAME_SAVE_CURRMAP);
2181 if (!Strm) return false;
2182 VStr TmpName;
2183 *Strm << TmpName;
2184 if (Strm->IsError()) { VStream::Destroy(Strm); return false; }
2185 VStream::Destroy(Strm);
2187 CurrentMap = *TmpName;
2189 // load map list
2190 vint32 NumMaps = -1;
2191 Strm = vwad->OpenFile(NEWFMT_FNAME_SAVE_MAPLIST);
2192 if (Strm) {
2193 *Strm << STRM_INDEX(NumMaps);
2194 if (Strm->IsError() || NumMaps < 0 || NumMaps > 1024) { VStream::Destroy(Strm); return false; }
2195 for (int i = 0; i < NumMaps; ++i) {
2196 *Strm << TmpName;
2197 if (Strm->IsError()) { VStream::Destroy(Strm); return false; }
2198 VSavedMap *Map = new VSavedMap(true);
2199 if (Strm->IsError()) { VStream::Destroy(Strm); return false; }
2200 Map->Name = *TmpName;
2201 Maps.Append(Map);
2202 Map->Index = Maps.length() - 1;
2204 VStream::Destroy(Strm);
2206 // now load map vwads
2207 for (int i = 0; i < NumMaps; ++i) {
2208 VSavedMap *Map = Maps[i];
2209 vassert(Map->Index == i);
2210 vassert(Map->IsNewFormat());
2211 VStr vname = Map->GenVWadName();
2212 Strm = vwad->OpenFile(vname);
2213 if (Strm->IsError() || Strm->TotalSize() < 16) { VStream::Destroy(Strm); return false; }
2214 Map->Data.setLength(Strm->TotalSize());
2215 Strm->Serialise(Map->Data.Ptr(), Map->Data.length());
2216 if (Strm->IsError()) { VStream::Destroy(Strm); return false; }
2217 VStream::Destroy(Strm);
2219 } else {
2220 // for checkpoint, we don't have map list at all
2221 NumMaps = 0;
2224 //HACK: if `NumMaps` is 0, we're loading a checkpoint
2225 if (NumMaps == 0) {
2226 // load players inventory
2227 VSavedCheckpoint &cp = CheckPoint;
2228 cp.Clear();
2229 Strm = vwad->OpenFile(NEWFMT_FNAME_SAVE_CPOINT);
2230 if (!Strm) return false;
2231 cp.Serialise(Strm);
2232 if (Strm->IsError()) { VStream::Destroy(Strm); return false; }
2233 VStream::Destroy(Strm);
2234 SavedSkill = cp.Skill;
2235 } else {
2236 VSavedCheckpoint &cp = CheckPoint;
2237 cp.Clear();
2238 // load skill level
2239 SavedSkill = -1;
2240 Strm = vwad->OpenFile(NEWFMT_FNAME_SAVE_SKILL);
2241 if (Strm) {
2242 vint32 sk = -1;
2243 *Strm << sk;
2244 if (Strm->IsError()) { VStream::Destroy(Strm); return false; }
2245 VStream::Destroy(Strm);
2246 if (sk < 0 || sk > 31) {
2247 GCon->Logf(NAME_Warning, "Invalid savegame #%d skill (%d)", Slot, sk);
2248 } else {
2249 SavedSkill = sk;
2254 Host_ResetSkipFrames();
2256 vassert(!saveFileBase.isEmpty());
2257 return true;
2261 //==========================================================================
2263 // VSaveSlot::LoadSlot
2265 //==========================================================================
2266 bool VSaveSlot::LoadSlot (int Slot) {
2267 Clear(!dbg_save_in_old_format.asBool());
2268 saveFileBase.clear();
2270 VVWadArchive *vwad = nullptr;
2271 VStream *Strm = SV_OpenSlotFileReadWithFmt(Slot, vwad);
2272 if (!Strm) {
2273 saveFileBase.clear();
2274 GCon->Logf(NAME_Error, "Savegame #%d file doesn't exist", Slot);
2275 return false;
2278 bool res;
2280 if (vwad) {
2281 res = LoadSlotNew(Slot, vwad);
2282 delete vwad;
2283 } else {
2284 res = LoadSlotOld(Slot, Strm);
2285 if (res && Strm->IsError()) res = false;
2286 VStream::Destroy(Strm);
2289 if (!res) {
2290 saveFileBase.clear();
2291 GCon->Logf(NAME_Error, "Savegame #%d could not be read", Slot);
2292 Clear(!dbg_save_in_old_format.asBool());
2295 return res;
2299 //==========================================================================
2301 // VSaveSlot::SaveToSlotOld
2303 //==========================================================================
2304 bool VSaveSlot::SaveToSlotOld (int Slot, VStr &savefilename) {
2305 saveFileBase.clear();
2306 savefilename.clear();
2308 VStream *Strm = SV_CreateSlotFileWrite(Slot, Description, false);
2309 if (!Strm) {
2310 GCon->Logf(NAME_Error, "cannot save to slot #%d!", Slot);
2311 return false;
2313 savefilename = Strm->GetName();
2315 // write version info
2316 char VersionText[SAVE_VERSION_TEXT_LENGTH+1];
2317 memset(VersionText, 0, SAVE_VERSION_TEXT_LENGTH);
2318 VStr::Cpy(VersionText, SAVE_VERSION_TEXT);
2319 Strm->Serialise(VersionText, SAVE_VERSION_TEXT_LENGTH);
2321 // write game save description
2322 *Strm << Description;
2324 // extended data: date value and date string
2326 // date value
2327 TTimeVal tv;
2328 GetTimeOfDay(&tv);
2329 vint32 id = SAVE_EXTDATA_ID_DATEVAL;
2330 *Strm << STRM_INDEX(id);
2331 vint32 size = (vint32)sizeof(tv);
2332 *Strm << STRM_INDEX(size);
2333 Strm->Serialise(&tv, sizeof(tv));
2335 // date string
2336 VStr dstr = TimeVal2Str(&tv);
2337 id = SAVE_EXTDATA_ID_DATESTR;
2338 *Strm << STRM_INDEX(id);
2339 size = dstr.length();
2340 *Strm << STRM_INDEX(size);
2341 Strm->Serialise(*dstr, size);
2343 // end of data marker
2344 id = SAVE_EXTDATA_ID_END;
2345 *Strm << STRM_INDEX(id);
2348 // write list of loaded modules
2349 auto wadlist = FL_GetWadPk3ListSmall();
2350 //GCon->Logf("====================="); for (int f = 0; f < wadlist.length(); ++f) GCon->Logf(" %d: %s", f, *wadlist[f]);
2351 vint32 wcount = wadlist.length();
2352 *Strm << wcount;
2353 for (int f = 0; f < wadlist.length(); ++f) *Strm << wadlist[f];
2355 // write current map
2356 VStr TmpName(CurrentMap);
2357 *Strm << TmpName;
2359 // write map list
2360 vint32 NumMaps = Maps.length();
2361 *Strm << STRM_INDEX(NumMaps);
2362 for (int i = 0; i < Maps.length(); ++i) {
2363 VSavedMap *Map = Maps[i];
2364 vassert(Map->Index == i);
2365 vassert(!Map->IsNewFormat());
2366 TmpName = VStr(Map->Name);
2367 vint32 DataLen = Map->Data.length();
2368 *Strm << TmpName << Map->Compressed << STRM_INDEX(Map->DecompressedSize) << STRM_INDEX(DataLen);
2369 Strm->Serialise(Map->Data.Ptr(), Map->Data.length());
2372 //HACK: if `NumMaps` is 0, we're saving checkpoint
2373 if (NumMaps == 0) {
2374 // save players inventory
2375 VSavedCheckpoint &cp = CheckPoint;
2376 cp.Serialise(Strm);
2377 SavedSkill = cp.Skill;
2378 } else {
2379 if (SavedSkill >= 0 && SavedSkill < 32) {
2380 // save extra data
2381 vuint32 seg = GSLOT_DATA_START;
2382 *Strm << STRM_INDEX_U(seg);
2383 // skill
2384 seg = GSLOT_DATA_SKILL;
2385 *Strm << STRM_INDEX_U(seg);
2386 vint32 sk = SavedSkill;
2387 *Strm << STRM_INDEX(sk);
2388 // done
2389 seg = GSLOT_DATA_END;
2390 *Strm << STRM_INDEX_U(seg);
2394 bool err = Strm->IsError();
2395 Strm->Close();
2396 err = err || Strm->IsError();
2397 delete Strm; // done in failed
2399 Host_ResetSkipFrames();
2401 if (err) {
2402 GCon->Logf(NAME_Error, "error saving to slot %d, savegame is corrupted!", Slot);
2403 return false;
2406 vassert(!saveFileBase.isEmpty());
2407 return true;
2411 #define CLOSE_VWAD_FILE() do { \
2412 vassert(Strm != nullptr); \
2413 Strm->Close(); \
2414 const bool xserr = Strm->IsError(); \
2415 delete Strm; Strm = nullptr; \
2416 if (xserr || vmain->IsError()) { \
2417 delete vmain; vmain = nullptr; \
2418 return false; \
2420 } while (0)
2423 #define CREATE_VWAD_FILE(xxfname) do { \
2424 vassert(Strm == nullptr); \
2425 int level = save_compression_level.asInt(); \
2426 if (level < VWADWR_COMP_DISABLE || level > VWADWR_COMP_BEST) { \
2427 level = VWADWR_COMP_FAST; \
2428 save_compression_level = level; \
2430 VStr xyname = VStr(xxfname); \
2431 if (xyname.endsWithNoCase(".vwad")) level = VWADWR_COMP_DISABLE; \
2432 Strm = vmain->CreateFileDirect(xyname, level); \
2433 vassert(Strm != nullptr); \
2434 } while (0)
2437 //==========================================================================
2439 // VSaveSlot::SaveToSlotNew
2441 //==========================================================================
2442 bool VSaveSlot::SaveToSlotNew (int Slot, VStr &savefilename) {
2443 TTimeVal tv;
2444 GetTimeOfDay(&tv);
2446 saveFileBase.clear();
2447 savefilename.clear();
2449 VStream *ArcStrm = SV_CreateSlotFileWrite(Slot, Description, true);
2450 if (!ArcStrm) {
2451 GCon->Logf(NAME_Error, "cannot save to slot %d!", Slot);
2452 return false;
2454 savefilename = ArcStrm->GetName();
2456 VVWadNewArchive *vmain = new VVWadNewArchive("<main-save>",
2457 NEWFMT_VWAD_AUTHOR,
2458 Description + " | "+TimeVal2Str(&tv),
2459 ArcStrm, true/*owned*/);
2460 if (vmain->IsError()) {
2461 delete vmain;
2462 GCon->Logf(NAME_Error, "cannot create save archive for slot %d!", Slot);
2463 return false;
2466 VStream *Strm = nullptr;
2468 // version
2469 CREATE_VWAD_FILE(NEWFMT_FNAME_SAVE_HEADER);
2470 vuint8 savever = 0;
2471 *Strm << savever;
2472 CLOSE_VWAD_FILE();
2474 CREATE_VWAD_FILE(NEWFMT_FNAME_SAVE_DESCR);
2475 *Strm << Description;
2476 CLOSE_VWAD_FILE();
2478 // extended data: date value and date string
2479 // date value
2480 CREATE_VWAD_FILE(NEWFMT_FNAME_SAVE_DATE);
2481 *Strm << tv.secs << tv.usecs << tv.secshi;
2482 // date string (unused, but nice to have)
2483 VStr dstr = TimeVal2Str(&tv);
2484 *Strm << dstr;
2485 CLOSE_VWAD_FILE();
2487 // write list of loaded modules
2489 CREATE_VWAD_FILE(NEWFMT_FNAME_SAVE_WADLIST);
2490 auto wadlist = FL_GetWadPk3ListSmall();
2491 vint32 wcount = wadlist.length();
2492 *Strm << wcount;
2493 for (int f = 0; f < wcount; ++f) *Strm << wadlist[f];
2494 CLOSE_VWAD_FILE();
2496 // write human-readable list of loaded modules
2497 // (it is purely informative)
2498 VStr sres;
2499 sres = "# automatically generated, and purely informational\n";
2500 for (VStr w : wadlist) { sres += w; sres += "\n"; }
2501 CREATE_VWAD_FILE(NEWFMT_FNAME_SAVE_HWADLIST);
2502 Strm->Serialise((void *)sres.getCStr(), sres.length());
2503 CLOSE_VWAD_FILE();
2506 // write current map name
2507 CREATE_VWAD_FILE(NEWFMT_FNAME_SAVE_CURRMAP);
2508 VStr TmpName(CurrentMap);
2509 *Strm << TmpName;
2510 CLOSE_VWAD_FILE();
2512 // write map list
2513 CREATE_VWAD_FILE(NEWFMT_FNAME_SAVE_MAPLIST);
2514 vint32 NumMaps = Maps.length();
2515 *Strm << STRM_INDEX(NumMaps);
2516 for (int i = 0; i < Maps.length(); ++i) {
2517 VSavedMap *Map = Maps[i];
2518 vassert(Map->Index == i);
2519 vassert(Map->IsNewFormat());
2520 TmpName = VStr(Map->Name);
2521 *Strm << TmpName;
2523 CLOSE_VWAD_FILE();
2525 // write map vwads
2526 for (int i = 0; i < NumMaps; ++i) {
2527 VSavedMap *Map = Maps[i];
2528 vassert(Map->Index == i);
2529 vassert(Map->IsNewFormat());
2530 vassert(Map->Data.length() >= 16);
2531 VStr vname = Map->GenVWadName();
2532 CREATE_VWAD_FILE(vname);
2533 Strm->Serialise(Map->Data.Ptr(), Map->Data.length());
2534 CLOSE_VWAD_FILE();
2537 //HACK: if `NumMaps` is 0, we're loading a checkpoint
2538 if (NumMaps == 0) {
2539 // save players inventory
2540 VSavedCheckpoint &cp = CheckPoint;
2541 CREATE_VWAD_FILE(NEWFMT_FNAME_SAVE_CPOINT);
2542 cp.Serialise(Strm);
2543 CLOSE_VWAD_FILE();
2544 SavedSkill = cp.Skill;
2545 } else {
2546 VSavedCheckpoint &cp = CheckPoint;
2547 cp.Clear();
2548 // write skill level
2549 if (SavedSkill >= 0 && SavedSkill < 32) {
2550 CREATE_VWAD_FILE(NEWFMT_FNAME_SAVE_SKILL);
2551 vint32 sk = SavedSkill;
2552 *Strm << sk;
2553 CLOSE_VWAD_FILE();
2557 // finish vwad
2558 const bool xres = vmain->Close();
2559 delete vmain;
2560 #if 0
2561 GCon->Logf(NAME_Debug, "finished VWAD archive! xres=%d (%s)", (int)xres, *ArcStrm->GetName());
2562 #endif
2563 if (!xres) {
2564 GCon->Logf(NAME_Error, "cannot finalize savegame archive");
2567 return xres;
2571 //==========================================================================
2573 // VSaveSlot::SaveToSlot
2575 //==========================================================================
2576 bool VSaveSlot::SaveToSlot (int Slot) {
2577 VStr savefilename;
2579 const bool res = (IsNewFormat() ? SaveToSlotNew(Slot, savefilename)
2580 : SaveToSlotOld(Slot, savefilename));
2581 if (!res) {
2582 SV_SaveFailed(savefilename, Slot);
2583 saveFileBase.clear();
2584 //removeSlotSaveFiles(Slot);
2585 } else {
2586 SV_SaveSuccess(savefilename, Slot);
2589 return res;
2593 //==========================================================================
2595 // VSaveSlot::FindMap
2597 //==========================================================================
2598 VSavedMap *VSaveSlot::FindMap (VName Name) {
2599 for (int i = 0; i < Maps.length(); ++i) if (Maps[i]->Name == Name) return Maps[i];
2600 return nullptr;
2604 //==========================================================================
2606 // SV_GetSaveStringOld
2608 //==========================================================================
2609 static bool SV_GetSaveStringOld (int Slot, VStr &Desc, VStream *Strm) {
2610 bool goodSave = true;
2611 Desc = "???";
2612 *Strm << Desc;
2613 // skip extended data
2614 if (true/*VStr::Cmp(VersionText, SAVE_VERSION_TEXT) == 0*/) {
2615 if (!SkipExtData(Strm) || Strm->IsError()) goodSave = false;
2617 if (goodSave) {
2618 // check list of loaded modules
2619 goodSave = CheckModList(Strm, Slot, true, true);
2621 if (!goodSave) Desc = "*"+Desc;
2622 return /*true*/goodSave;
2626 //==========================================================================
2628 // SV_GetSaveStringNew
2630 //==========================================================================
2631 static bool SV_GetSaveStringNew (int Slot, VStr &Desc, VVWadArchive *vwad) {
2632 VStream *Strm;
2634 Strm = vwad->OpenFile(NEWFMT_FNAME_SAVE_DESCR);
2635 if (!Strm) return false;
2636 *Strm << Desc;
2637 if (Strm->IsError()) { VStream::Destroy(Strm); return false; }
2638 VStream::Destroy(Strm);
2640 #if 0
2641 const bool goodSave = true;
2642 #else
2643 Strm = vwad->OpenFile(NEWFMT_FNAME_SAVE_WADLIST);
2644 if (!Strm) return false;
2645 const bool goodSave = CheckModList(Strm, Slot, false, true);
2646 VStream::Destroy(Strm);
2647 #endif
2649 if (!goodSave) Desc = "*"+Desc;
2650 return /*true*/goodSave;
2654 //==========================================================================
2656 // SV_GetSaveString
2658 //==========================================================================
2659 bool SV_GetSaveString (int Slot, VStr &Desc) {
2660 VVWadArchive *vwad = nullptr;
2661 VStream *Strm = SV_OpenSlotFileReadWithFmt(Slot, vwad);
2662 bool res;
2663 if (vwad) {
2664 res = SV_GetSaveStringNew(Slot, Desc, vwad);
2665 delete vwad;
2666 } else if (Strm) {
2667 res = SV_GetSaveStringOld(Slot, Desc, Strm);
2668 if (res && Strm->IsError()) res = false;
2669 VStream::Destroy(Strm);
2670 } else {
2671 res = false;
2673 if (!res) Desc = EMPTYSTRING;
2674 return res;
2679 //==========================================================================
2681 // SV_GetSaveDateString
2683 //==========================================================================
2684 void SV_GetSaveDateString (int Slot, VStr &datestr) {
2685 VVWadArchive *vwad = nullptr;
2686 VStream *Strm = SV_OpenSlotFileReadWithFmt(Slot, vwad);
2687 bool res;
2688 if (vwad) {
2689 res = false;
2690 Strm = vwad->OpenFile(NEWFMT_FNAME_SAVE_DATE);
2691 if (Strm) {
2692 TTimeVal tv;
2693 memset((void *)&tv, 0, sizeof(tv));
2694 *Strm << tv.secs << tv.usecs << tv.secshi;
2695 res = !Strm->IsError();
2696 VStream::Destroy(Strm);
2697 if (res) datestr = TimeVal2Str(&tv);
2699 delete vwad;
2700 } else if (Strm) {
2701 VStr Desc;
2702 *Strm << Desc;
2703 res = !Strm->IsError();
2704 if (res) {
2705 datestr = LoadDateStrExtData(Strm);
2706 if (datestr.length() == 0) datestr = "UNKNOWN";
2707 if (res && Strm->IsError()) res = false;
2709 VStream::Destroy(Strm);
2710 } else {
2711 res = false;
2713 if (!res) datestr = "UNKNOWN";
2717 //==========================================================================
2719 // SV_GetSaveDateTVal
2721 // false: slot is empty or invalid
2723 //==========================================================================
2724 static bool SV_GetSaveDateTVal (int Slot, TTimeVal *tv) {
2725 //memset((void *)tv, 0, sizeof(*tv));
2727 VVWadArchive *vwad = nullptr;
2728 VStream *Strm = SV_OpenSlotFileReadWithFmt(Slot, vwad);
2729 bool res;
2730 if (vwad) {
2731 res = false;
2732 Strm = vwad->OpenFile(NEWFMT_FNAME_SAVE_DATE);
2733 if (Strm) {
2734 TTimeVal tv;
2735 memset((void *)&tv, 0, sizeof(tv));
2736 *Strm << tv.secs << tv.usecs << tv.secshi;
2737 res = !Strm->IsError();
2738 VStream::Destroy(Strm);
2740 delete vwad;
2741 } else if (Strm) {
2742 VStr Desc;
2743 *Strm << Desc;
2744 res = !Strm->IsError();
2745 if (res) {
2746 res = LoadDateTValExtData(Strm, tv);
2748 VStream::Destroy(Strm);
2749 } else {
2750 res = false;
2752 if (!res) memset((void *)tv, 0, sizeof(*tv));
2753 return res;
2757 //==========================================================================
2759 // SV_FindAutosaveSlot
2761 // returns 0 on error
2763 //==========================================================================
2764 static int SV_FindAutosaveSlot () {
2765 TTimeVal tv, besttv;
2766 int bestslot = 0;
2767 memset((void *)&tv, 0, sizeof(tv));
2768 memset((void *)&besttv, 0, sizeof(besttv));
2769 for (int slot = 1; slot <= NUM_AUTOSAVES; ++slot) {
2770 if (!SV_GetSaveDateTVal(-slot, &tv)) {
2771 //fprintf(stderr, "AUTOSAVE: free slot #%d found!\n", slot);
2772 bestslot = -slot;
2773 break;
2775 if (!bestslot || tv < besttv) {
2776 //GCon->Logf("AUTOSAVE: better slot #%d found [%s] : old id #%d [%s]!", slot, *TimeVal2Str(&tv), -bestslot, (bestslot ? *TimeVal2Str(&besttv) : ""));
2777 bestslot = -slot;
2778 besttv = tv;
2779 } else {
2780 //GCon->Logf("AUTOSAVE: skipped slot #%d [%s] (%d,%d,%d)!", slot, *TimeVal2Str(&tv), tv.secshi, tv.secs, tv.usecs);
2783 return bestslot;
2787 //==========================================================================
2789 // AssertSegment
2791 //==========================================================================
2792 static inline void AssertSegment (VStream &Strm, gameArchiveSegment_t segType) {
2793 if (Streamer<vint32>(Strm) != (int)segType) {
2794 Host_Error("Corrupted save game: Segment [%d] failed alignment check", segType);
2799 //==========================================================================
2801 // ArchiveNames
2803 //==========================================================================
2804 static void ArchiveNames (VSaveWriterStream *Saver) {
2805 if (Saver->IsNewFormat()) {
2806 if (!Saver->CreateFileDirect(NEWFMT_FNAME_MAP_NAMES)) return;
2807 } else {
2808 // write offset to the names in the beginning of the file
2809 vint32 NamesOffset = Saver->Tell();
2810 Saver->Seek(0);
2811 *Saver << NamesOffset;
2812 Saver->Seek(NamesOffset);
2815 // serialise names
2816 vint32 Count = Saver->Names.length();
2817 *Saver << STRM_INDEX(Count);
2818 for (int i = 0; i < Count; ++i) {
2819 //*Saver << *VName::GetEntry(Saver->Names[i].GetIndex());
2820 const char *EName = *Saver->Names[i];
2821 vuint8 len = (vuint8)VStr::Length(EName);
2822 *Saver << len;
2823 if (len) Saver->Serialise((void *)EName, len);
2825 Saver->CloseFile();
2827 if (!Saver->CreateFileDirect(NEWFMT_FNAME_MAP_ACSEXPT)) return;
2828 // serialise number of ACS exports
2829 vint32 numScripts = Saver->AcsExports.length();
2830 *Saver << STRM_INDEX(numScripts);
2831 Saver->CloseFile();
2835 //==========================================================================
2837 // UnarchiveNames
2839 //==========================================================================
2840 static void UnarchiveNames (VSaveLoaderStream *Loader) {
2841 vint32 NamesOffset = -1;
2842 vint32 TmpOffset = 0;
2844 if (Loader->IsNewFormat()) {
2845 Loader->OpenFile(NEWFMT_FNAME_MAP_NAMES);
2846 } else {
2847 *Loader << NamesOffset;
2848 TmpOffset = Loader->Tell();
2849 Loader->Seek(NamesOffset);
2852 vint32 Count;
2853 *Loader << STRM_INDEX(Count);
2854 Loader->NameRemap.setLength(Count);
2855 for (int i = 0; i < Count; ++i) {
2856 char EName[NAME_SIZE+1];
2857 vuint8 len = 0;
2858 *Loader << len;
2859 vassert(len <= NAME_SIZE);
2860 if (len) Loader->Serialise(EName, len);
2861 EName[len] = 0;
2862 Loader->NameRemap[i] = VName(EName);
2865 // unserialise number of ACS exports
2866 Loader->OpenFile(NEWFMT_FNAME_MAP_ACSEXPT);
2867 vint32 numScripts = -1;
2868 *Loader << STRM_INDEX(numScripts);
2869 if (numScripts < 0 || numScripts >= 1024*1024*2) Host_Error("invalid number of ACS scripts (%d)", numScripts);
2870 Loader->AcsExports.setLength(numScripts);
2872 // create empty script objects
2873 for (vint32 f = 0; f < numScripts; ++f) Loader->AcsExports[f] = AcsCreateEmptyThinker();
2875 if (!Loader->IsNewFormat()) {
2876 Loader->Seek(TmpOffset);
2881 //==========================================================================
2883 // ArchiveThinkers
2885 //==========================================================================
2886 static void ArchiveThinkers (VSaveWriterStream *Saver, bool SavingPlayers) {
2887 if (!Saver->IsNewFormat()) {
2888 vint32 Seg = ASEG_WORLD;
2889 *Saver << Seg;
2892 Saver->skipPlayers = !SavingPlayers;
2894 // add level
2895 Saver->RegisterObject(GLevel);
2897 // add world info
2898 if (!Saver->CreateFileDirect(NEWFMT_FNAME_MAP_WORDINFO)) return;
2899 vuint8 WorldInfoSaved = (vuint8)SavingPlayers;
2900 *Saver << WorldInfoSaved;
2901 if (WorldInfoSaved) Saver->RegisterObject(GGameInfo->WorldInfo);
2902 Saver->CloseFile();
2904 if (!Saver->CreateFileDirect(NEWFMT_FNAME_MAP_ACTPLYS)) return;
2905 // add players
2907 vassert(MAXPLAYERS >= 0 && MAXPLAYERS <= 254);
2908 vuint8 mpl = MAXPLAYERS;
2909 *Saver << mpl;
2911 for (int i = 0; i < MAXPLAYERS; ++i) {
2912 vuint8 Active = (vuint8)(SavingPlayers && GGameInfo->Players[i]);
2913 *Saver << Active;
2914 if (!Active) continue;
2915 Saver->RegisterObject(GGameInfo->Players[i]);
2917 Saver->CloseFile();
2919 // add thinkers
2920 int ThinkersStart = Saver->Exports.length();
2921 for (TThinkerIterator<VThinker> Th(GLevel); Th; ++Th) {
2922 // players will be skipped by `Saver`
2923 Saver->RegisterObject(*Th);
2925 if ((*Th)->IsA(VEntity::StaticClass())) {
2926 VEntity *e = (VEntity *)(*Th);
2927 if (e->EntityFlags&VEntity::EF_IsPlayer) GCon->Logf("PLRSAV: FloorZ=%f; CeilingZ=%f; Floor=%p; Ceiling=%p", e->FloorZ, e->CeilingZ, e->Floor, e->Ceiling);
2932 if (!Saver->CreateFileDirect(NEWFMT_FNAME_MAP_EXPOBJNS)) return;
2933 // write exported object names
2934 vint32 NumObjects = Saver->Exports.length()-ThinkersStart;
2935 *Saver << STRM_INDEX(NumObjects);
2936 for (int i = ThinkersStart; i < Saver->Exports.length(); ++i) {
2937 VName CName = Saver->Exports[i]->GetClass()->GetVName();
2938 *Saver << CName;
2940 Saver->CloseFile();
2942 #if 0
2943 // purely informational
2944 if (!Saver->CreateFileDirect("obj_classes.txt")) return;
2945 for (int i = 0; i < Saver->Exports.length(); ++i) {
2946 VStr s = VStr(va("%d: ", i)) + Saver->Exports[i]->GetClass()->GetFullName() +
2947 " : " + Saver->Exports[i]->GetClass()->Loc.toStringNoCol() +
2948 "\n";
2949 Saver->Serialise((void *)s.getCStr(), s.length());
2951 Saver->CloseFile();
2952 #endif
2954 if (!Saver->CreateFileBuffered(NEWFMT_FNAME_MAP_THINKERS)) return;
2955 // serialise objects
2956 for (int i = 0; i < Saver->Exports.length(); ++i) {
2957 if (dbg_save_verbose&0x10) GCon->Logf("** SR #%d: <%s>", i, *Saver->Exports[i]->GetClass()->GetFullName());
2958 Saver->Exports[i]->Serialise(*Saver);
2960 Saver->CloseFile();
2962 //GCon->Logf("dbg_save_verbose=0x%04x (%s) %d", dbg_save_verbose.asInt(), *dbg_save_verbose.asStr(), dbg_save_verbose.asInt());
2964 if (!Saver->CreateFileBuffered(NEWFMT_FNAME_MAP_ACS)) return;
2965 // collect acs scripts, serialize acs level
2966 GLevel->Acs->Serialise(*Saver);
2967 Saver->CloseFile();
2969 // save collected VAcs objects contents
2970 if (!Saver->CreateFileBuffered(NEWFMT_FNAME_MAP_ACS_DATA)) return;
2971 for (vint32 f = 0; f < Saver->AcsExports.length(); ++f) {
2972 Saver->AcsExports[f]->Serialise(*Saver);
2974 Saver->CloseFile();
2978 //==========================================================================
2980 // UnarchiveThinkers
2982 //==========================================================================
2983 static void UnarchiveThinkers (VSaveLoaderStream *Loader) {
2984 VObject *Obj = nullptr;
2986 if (!Loader->IsNewFormat()) {
2987 AssertSegment(*Loader, ASEG_WORLD);
2990 // add level
2991 Loader->Exports.Append(GLevel);
2993 // add world info
2994 Loader->OpenFile(NEWFMT_FNAME_MAP_WORDINFO);
2995 vuint8 WorldInfoSaved;
2996 *Loader << WorldInfoSaved;
2997 if (WorldInfoSaved) Loader->Exports.Append(GGameInfo->WorldInfo);
2999 // add players
3000 Loader->OpenFile(NEWFMT_FNAME_MAP_ACTPLYS);
3002 vuint8 mpl = 255;
3003 *Loader << mpl;
3004 if (mpl != MAXPLAYERS) Host_Error("Invalid number of players in save");
3006 sv_load_num_players = 0;
3007 for (int i = 0; i < MAXPLAYERS; ++i) {
3008 vuint8 Active;
3009 *Loader << Active;
3010 if (Active) {
3011 ++sv_load_num_players;
3012 Loader->Exports.Append(GPlayersBase[i]);
3016 TArray<VEntity *> elist;
3017 #ifdef VAVOOM_LOADER_CAN_SKIP_CLASSES
3018 TMapNC<VObject *, bool> deadThinkers;
3019 TMapNC<VName, bool> deadThinkersWarned;
3020 #endif
3022 bool hasSomethingToRemove = false;
3024 Loader->OpenFile(NEWFMT_FNAME_MAP_EXPOBJNS);
3025 vint32 NumObjects;
3026 *Loader << STRM_INDEX(NumObjects);
3027 if (NumObjects < 0) Host_Error("invalid number of VM objects");
3028 for (int i = 0; i < NumObjects; ++i) {
3029 // get params
3030 VName CName;
3031 *Loader << CName;
3032 VClass *Class = VClass::FindClass(*CName);
3033 if (!Class) {
3034 #ifdef VAVOOM_LOADER_CAN_SKIP_CLASSES
3035 if (ListLoaderCanSkipClass.has(CName)) {
3036 if (!deadThinkersWarned.put(CName, true)) {
3037 GCon->Logf("I/O WARNING: No such class '%s'", *CName);
3039 //Loader->Exports.Append(nullptr);
3040 Class = VThinker::StaticClass();
3041 Obj = VObject::StaticSpawnNoReplace(Class);
3042 //deadThinkers.append((VThinker *)Obj);
3043 deadThinkers.put(Obj, false);
3044 } else {
3045 Sys_Error("I/O ERROR: No such class '%s'", *CName);
3047 #else
3048 Sys_Error("I/O ERROR: No such class '%s'", *CName);
3049 #endif
3050 } else {
3051 // allocate object and copy data
3052 Obj = VObject::StaticSpawnNoReplace(Class);
3054 // reassign server uids
3055 if (Obj && Obj->IsA(VThinker::StaticClass())) ((VThinker *)Obj)->ServerUId = Obj->GetUniqueId();
3057 // handle level info
3058 if (Obj->IsA(VLevelInfo::StaticClass())) {
3059 GLevelInfo = (VLevelInfo *)Obj;
3060 GLevelInfo->Game = GGameInfo;
3061 GLevelInfo->World = GGameInfo->WorldInfo;
3062 GLevel->LevelInfo = GLevelInfo;
3063 } else if (Obj->IsA(VEntity::StaticClass())) {
3064 VEntity *e = (VEntity *)Obj;
3065 if (!hasSomethingToRemove && (e->EntityFlags&VEntity::EF_KillOnUnarchive) != 0) hasSomethingToRemove = true;
3066 elist.append(e);
3069 Loader->Exports.Append(Obj);
3072 GLevelInfo->Game = GGameInfo;
3073 GLevelInfo->World = GGameInfo->WorldInfo;
3075 Loader->OpenFile(NEWFMT_FNAME_MAP_THINKERS);
3076 for (int i = 0; i < Loader->Exports.length(); ++i) {
3077 vassert(Loader->Exports[i]);
3078 #ifdef VAVOOM_LOADER_CAN_SKIP_CLASSES
3079 auto dpp = deadThinkers.get(Loader->Exports[i]);
3080 if (dpp) {
3081 //GCon->Logf(NAME_Debug, "!!! %d: %s", i, Loader->Exports[i]->GetClass()->GetName());
3082 Loader->Exports[i]->Serialise(*Loader);
3083 } else
3084 #endif
3086 Loader->Exports[i]->Serialise(*Loader);
3089 #ifdef VAVOOM_LOADER_CAN_SKIP_CLASSES
3090 for (auto it = deadThinkers.first(); it; ++it) {
3091 ((VThinker *)it.getKey())->DestroyThinker();
3093 #endif
3095 Loader->OpenFile(NEWFMT_FNAME_MAP_ACS);
3096 // unserialise acs script
3097 GLevel->Acs->Serialise(*Loader);
3099 Loader->OpenFile(NEWFMT_FNAME_MAP_ACS_DATA);
3100 // load collected VAcs objects contents
3101 for (vint32 f = 0; f < Loader->AcsExports.length(); ++f) {
3102 Loader->AcsExports[f]->Serialise(*Loader);
3105 // `LinkToWorld()` in `VEntity::SerialiseOther()` will find the correct floor
3108 for (int i = 0; i < elist.length(); ++i) {
3109 VEntity *e = elist[i];
3110 GCon->Logf("ENTITY <%s>: org=(%f,%f,%f); flags=0x%08x", *e->GetClass()->GetFullName(), e->Origin.x, e->Origin.y, e->Origin.z, e->EntityFlags);
3114 // remove unnecessary entities
3115 if (hasSomethingToRemove && !loader_ignore_kill_on_unarchive) {
3116 for (int i = 0; i < elist.length(); ++i) if (elist[i]->EntityFlags&VEntity::EF_KillOnUnarchive) elist[i]->DestroyThinker();
3119 GLevelInfo->eventAfterUnarchiveThinkers();
3120 GLevel->eventAfterUnarchiveThinkers();
3124 //==========================================================================
3126 // ArchiveSounds
3128 //==========================================================================
3129 static void ArchiveSounds (VSaveWriterStream *Saver) {
3130 if (Saver->IsNewFormat()) {
3131 #ifdef CLIENT
3132 if (!Saver->CreateFileDirect(NEWFMT_FNAME_MAP_SOUNDS)) return;
3133 GAudio->SerialiseSounds(*Saver);
3134 Saver->CloseFile();
3135 #endif
3136 } else {
3137 vint32 Seg = ASEG_SOUNDS;
3138 *Saver << Seg;
3139 #ifdef CLIENT
3140 GAudio->SerialiseSounds(*Saver);
3141 #else
3142 vint32 Dummy = 0;
3143 *Saver << Dummy;
3144 #endif
3149 //==========================================================================
3151 // UnarchiveSounds
3153 //==========================================================================
3154 static void UnarchiveSounds (VSaveLoaderStream *Loader) {
3155 if (Loader->IsNewFormat()) {
3156 #ifdef CLIENT
3157 Loader->OpenFile(NEWFMT_FNAME_MAP_SOUNDS);
3158 GAudio->SerialiseSounds(*Loader);
3159 #endif
3160 } else {
3161 AssertSegment(*Loader, ASEG_SOUNDS);
3162 #ifdef CLIENT
3163 GAudio->SerialiseSounds(*Loader);
3164 #else
3165 vint32 count = 0;
3166 *Loader << count;
3167 //FIXME: keep this in sync with VAudio
3168 //Strm.Seek(Strm.Tell()+Dummy*36); //FIXME!
3169 if (count < 0) Sys_Error("invalid sound sequence data");
3170 while (count-- > 0) {
3171 vuint8 xver = 0; // current version is 0
3172 *Loader << xver;
3173 if (xver != 0) Sys_Error("invalid sound sequence data");
3174 vint32 Sequence;
3175 vint32 OriginId;
3176 TVec Origin;
3177 vint32 CurrentSoundID;
3178 float DelayTime;
3179 vuint32 DidDelayOnce;
3180 float Volume;
3181 float Attenuation;
3182 vint32 ModeNum;
3183 *Loader << STRM_INDEX(Sequence)
3184 << STRM_INDEX(OriginId)
3185 << Origin
3186 << STRM_INDEX(CurrentSoundID)
3187 << DelayTime
3188 << STRM_INDEX(DidDelayOnce)
3189 << Volume
3190 << Attenuation
3191 << STRM_INDEX(ModeNum);
3193 vint32 Offset;
3194 *Loader << STRM_INDEX(Offset);
3196 vint32 Count;
3197 *Loader << STRM_INDEX(Count);
3198 if (Count < 0) Sys_Error("invalid sound sequence data");
3199 for (int i = 0; i < Count; ++i) {
3200 VName SeqName;
3201 *Loader << SeqName;
3204 vint32 ParentSeqIdx;
3205 vint32 ChildSeqIdx;
3206 *Loader << STRM_INDEX(ParentSeqIdx) << STRM_INDEX(ChildSeqIdx);
3208 #endif
3213 //==========================================================================
3215 // SV_SaveMap
3217 //==========================================================================
3218 static void SV_SaveMap (bool savePlayers) {
3219 // make sure we don't have any garbage
3220 Host_CollectGarbage(true);
3222 VSaveWriterStream *Saver;
3224 // if we have no maps, or only one map that we will replace,
3225 // convert the save to the new format
3227 if (!BaseSlot.IsNewFormat()) {
3228 if (BaseSlot.Maps.length() == 0 ||
3229 (BaseSlot.Maps.length() == 1 && BaseSlot.Maps[0]->Name == GLevel->MapName))
3231 GCon->Logf(NAME_Debug, "converted save game to new format.");
3232 BaseSlot.ForceNewFormat();
3236 // open the output file
3237 if (!BaseSlot.IsNewFormat()) {
3238 // old format
3239 VSavedMap *Map = BaseSlot.FindMap(GLevel->MapName);
3240 if (!Map) {
3241 Map = new VSavedMap(false);
3242 BaseSlot.Maps.Append(Map);
3243 Map->Name = GLevel->MapName;
3244 } else {
3245 Map->ClearData(false);
3248 VMemoryStream *InStrm = new VMemoryStream();
3249 Saver = new VSaveWriterStream(InStrm);
3251 vint32 NamesOffset = 0;
3252 *Saver << NamesOffset;
3254 // place a header marker
3255 vint32 Seg = ASEG_MAP_HEADER;
3256 *Saver << Seg;
3258 // write the level timer
3259 *Saver << GLevel->Time << GLevel->TicTime;
3261 // write main data
3262 ArchiveThinkers(Saver, savePlayers);
3263 ArchiveSounds(Saver);
3265 // place a termination marker
3266 Seg = ASEG_END;
3267 *Saver << Seg;
3269 ArchiveNames(Saver);
3271 // close the output file
3272 Saver->Close();
3274 TArrayNC<vuint8> &Buf = InStrm->GetArray();
3276 // compress map data
3277 Map->DecompressedSize = Buf.length();
3278 Map->Data.Clear();
3279 if (save_compression_level.asInt() < 0) {
3280 Map->Compressed = 0;
3281 Map->Data.setLength(Buf.length());
3282 if (Buf.length()) memcpy(Map->Data.ptr(), Buf.ptr(), Buf.length());
3283 } else {
3284 Map->Compressed = 1;
3285 VArrayStream *ArrStrm = new VArrayStream("<savemap>", Map->Data);
3286 ArrStrm->BeginWrite();
3287 int level = save_compression_level.asInt() * 3;
3288 if (level < 3) level = 3;
3289 VZLibStreamWriter *ZipStrm = new VZLibStreamWriter(ArrStrm, level);
3290 ZipStrm->Serialise(Buf.Ptr(), Buf.length());
3291 bool wasErr = ZipStrm->IsError();
3292 if (!ZipStrm->Close()) wasErr = true;
3293 delete ZipStrm;
3294 ArrStrm->Close();
3295 delete ArrStrm;
3296 if (wasErr) Host_Error("error compressing savegame data");
3299 delete Saver;
3300 } else {
3301 vassert(BaseSlot.IsNewFormat());
3303 VMemoryStream *InStrm = new VMemoryStream();
3304 VVWadNewArchive *vwad = new VVWadNewArchive("<map-data>", "k8vavoom engine", "saved map data",
3305 InStrm, false/*not owned*/);
3306 if (vwad->IsError()) {
3307 delete vwad;
3308 delete InStrm;
3309 Host_Error("error creating saved map archive");
3312 // remove old map
3313 VSavedMap *Map = BaseSlot.FindMap(GLevel->MapName);
3314 if (!Map) {
3315 Map = new VSavedMap(true);
3316 BaseSlot.Maps.Append(Map);
3317 Map->Name = GLevel->MapName;
3318 } else {
3319 Map->ClearData(true);
3322 // create saver
3323 Saver = new VSaveWriterStream(vwad);
3325 // write the level timer
3326 if (Saver->CreateFileDirect(NEWFMT_FNAME_MAP_GINFO)) {
3327 *Saver << GLevel->Time << GLevel->TicTime;
3328 Saver->CloseFile();
3331 // write main data
3332 ArchiveThinkers(Saver, savePlayers);
3333 ArchiveSounds(Saver);
3334 ArchiveNames(Saver);
3336 // close the output file
3337 const bool ok = Saver->Close();
3338 delete Saver;
3340 if (ok) {
3341 vassert(Map->IsNewFormat());
3342 TArrayNC<vuint8> &Buf = InStrm->GetArray();
3343 Map->Data.Clear();
3344 Map->Data.setLength(Buf.length());
3345 if (Buf.length()) memcpy(Map->Data.ptr(), Buf.ptr(), Buf.length());
3346 delete InStrm;
3347 } else {
3348 Map->Data.Clear();
3349 delete InStrm;
3350 Host_Error("error writing saved map archive");
3356 //==========================================================================
3358 // SV_SaveCheckpoint
3360 //==========================================================================
3361 static bool SV_SaveCheckpoint () {
3362 if (!GGameInfo) return false;
3363 if (GGameInfo->NetMode != NM_Standalone) return false; // oops
3364 // do not create checkpoints if we have several player classes
3365 if (GGameInfo->PlayerClasses.length() != 1) return false;
3367 VBasePlayer *plr = nullptr;
3368 // check if checkpoints are possible
3369 for (int i = 0; i < MAXPLAYERS; ++i) {
3370 if (GGameInfo->Players[i]) {
3371 if (!GGameInfo->Players[i]->IsCheckpointPossible()) return false;
3372 if (plr) return false;
3373 plr = GGameInfo->Players[i];
3376 if (!plr || !plr->MO) return false;
3378 QS_StartPhase(QSPhase::QSP_Save);
3379 VSavedCheckpoint &cp = BaseSlot.CheckPoint;
3380 cp.Clear();
3381 cp.Skill = GGameInfo->WorldInfo->GameSkill;
3382 VEntity *rwe = plr->eventGetReadyWeapon();
3384 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: === creating ===");
3385 for (VEntity *invFirst = plr->MO->QS_GetEntityInventory();
3386 invFirst;
3387 invFirst = invFirst->QS_GetEntityInventory())
3389 cp.AddEntity(invFirst);
3390 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: inventory item '%s'", invFirst->GetClass()->GetName());
3392 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: getting properties");
3394 plr->QS_Save();
3395 for (auto &&qse : cp.EList) qse.ent->QS_Save();
3396 cp.QSList = QS_GetCurrentArray();
3398 // count entities, build entity list
3399 for (int f = 0; f < cp.QSList.length(); ++f) {
3400 QSValue &qv = cp.QSList[f];
3401 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: property #%d of '%s': %s", f, (qv.ent ? qv.ent->GetClass()->GetName() : "player"), *qv.toString());
3402 if (!qv.ent) {
3403 qv.objidx = 0;
3404 } else {
3405 qv.objidx = cp.FindEntity(qv.ent);
3406 if (rwe == qv.ent) cp.ReadyWeapon = cp.FindEntity(rwe);
3410 QS_StartPhase(QSPhase::QSP_None);
3412 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: game skill is %d, cp skill is %d", GGameInfo->WorldInfo->GameSkill, cp.Skill);
3413 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: === complete ===");
3415 return true;
3419 //==========================================================================
3421 // SV_LoadMap
3423 // returns `true` if checkpoint was loaded
3425 //==========================================================================
3426 static bool SV_LoadMap (VName MapName, bool allowCheckpoints, bool hubTeleport) {
3427 bool isCheckpoint = (BaseSlot.Maps.length() == 0);
3428 if (isCheckpoint && !allowCheckpoints) {
3429 Host_Error("Trying to load checkpoint in hub game!");
3431 #ifdef CLIENT
3432 if (isCheckpoint && svs.max_clients != 1) {
3433 Host_Error("Checkpoints aren't supported in networked games!");
3435 // if we are loading a checkpoint, simulate normal map start
3436 if (isCheckpoint) sv_loading = false;
3437 #else
3438 // standalone server
3439 if (isCheckpoint) {
3440 Host_Error("Checkpoints aren't supported on dedicated servers!");
3442 #endif
3444 // load a base level (spawn thinkers if this is checkpoint save)
3445 if (!hubTeleport) SV_ResetPlayers();
3446 try {
3447 VBasePlayer::isCheckpointSpawn = isCheckpoint;
3448 #ifdef CLIENT
3449 // setup skill for server here, so checkpoints will start with a right one
3450 if (isCheckpoint) {
3451 VSavedCheckpoint &cp = BaseSlot.CheckPoint;
3452 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "*** CP SKILL: %d", cp.Skill);
3453 if (cp.Skill >= 0) {
3454 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "*** setting skill from a checkpoint: %d", cp.Skill);
3455 Skill = cp.Skill;
3458 #endif
3459 SV_SpawnServer(*MapName, isCheckpoint/*spawn thinkers*/);
3460 } catch (...) {
3461 VBasePlayer::isCheckpointSpawn = false;
3462 throw;
3465 #ifdef CLIENT
3466 if (isCheckpoint) {
3467 sv_loading = false; // just in case
3468 try {
3469 VBasePlayer::isCheckpointSpawn = true;
3470 CL_SetupLocalPlayer();
3471 } catch (...) {
3472 VBasePlayer::isCheckpointSpawn = false;
3473 throw;
3475 VBasePlayer::isCheckpointSpawn = false;
3477 Host_ResetSkipFrames();
3479 // do this here so that clients have loaded info, not initial one
3480 SV_SendServerInfoToClients();
3482 VSavedCheckpoint &cp = BaseSlot.CheckPoint;
3484 // put inventory
3485 VBasePlayer *plr = nullptr;
3486 for (int i = 0; i < MAXPLAYERS; ++i) {
3487 if (!GGameInfo->Players[i] || !GGameInfo->Players[i]->MO) continue;
3488 plr = GGameInfo->Players[i];
3489 break;
3491 if (!plr) Host_Error("active player not found");
3492 VEntity *rwe = nullptr; // ready weapon
3494 QS_StartPhase(QSPhase::QSP_Load);
3496 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: === loading ===");
3497 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: --- (starting inventory)");
3498 if (dbg_checkpoints) plr->CallDumpInventory();
3499 plr->MO->QS_ClearEntityInventory();
3500 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: --- (cleared inventory)");
3501 if (dbg_checkpoints) plr->CallDumpInventory();
3502 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: ---");
3504 // create inventory items
3505 // have to do it backwards due to the way `AttachToOwner()` works
3506 for (int f = cp.EList.length()-1; f >= 0; --f) {
3507 VSavedCheckpoint::EntityInfo &ei = cp.EList[f];
3508 VEntity *inv = plr->MO->QS_SpawnEntityInventory(VName(*ei.ClassName));
3509 if (!inv) Host_Error("cannot spawn inventory item '%s'", *ei.ClassName);
3510 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: spawned '%s'", inv->GetClass()->GetName());
3511 ei.ent = inv;
3512 if (cp.ReadyWeapon == f+1) rwe = inv;
3514 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: --- (spawned inventory)");
3515 if (dbg_checkpoints) plr->CallDumpInventory();
3516 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: ---");
3518 for (int f = 0; f < cp.QSList.length(); ++f) {
3519 QSValue &qv = cp.QSList[f];
3520 if (qv.objidx == 0) {
3521 qv.ent = nullptr;
3522 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: #%d:player: %s", f, *qv.toString());
3523 } else {
3524 qv.ent = cp.EList[qv.objidx-1].ent;
3525 vassert(qv.ent);
3526 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: #%d:%s: %s", f, qv.ent->GetClass()->GetName(), *qv.toString());
3528 QS_EnterValue(qv);
3531 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: --- (inventory before setting properties)");
3532 if (dbg_checkpoints) plr->CallDumpInventory();
3533 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: ---");
3534 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: calling loaders");
3535 // call player loader, then entity loaders
3536 plr->QS_Load();
3537 for (int f = 0; f < cp.EList.length(); ++f) {
3538 VSavedCheckpoint::EntityInfo &ei = cp.EList[f];
3539 ei.ent->QS_Load();
3542 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: --- (final inventory)");
3543 if (dbg_checkpoints) plr->CallDumpInventory();
3544 if (dbg_checkpoints) GCon->Logf(NAME_Debug, "QS: === done ===");
3545 QS_StartPhase(QSPhase::QSP_None);
3547 plr->eventAfterUnarchiveThinkers();
3549 plr->PlayerState = PST_LIVE;
3550 if (rwe) plr->eventSetReadyWeapon(rwe, true); // instant
3552 Host_ResetSkipFrames();
3553 return true;
3555 #endif
3557 Host_ResetSkipFrames();
3559 VSavedMap *Map = BaseSlot.FindMap(MapName);
3560 vassert(Map);
3562 VSaveLoaderStream *Loader = nullptr;
3564 // oops, it should be here
3565 TArrayNC<vuint8> DecompressedData;
3567 if (Map->IsNewFormat()) {
3568 VMemoryStreamRO *mst = new VMemoryStreamRO("<savemap:mapdata>",
3569 Map->Data.ptr(), Map->Data.length(), false);
3570 VVWadArchive *vmap = new VVWadArchive("<savemap:mapdata>", mst, true);
3571 if (!vmap->IsOpen()) {
3572 Host_Error("error opening savegame arhive");
3573 } else {
3574 Loader = new VSaveLoaderStream(vmap);
3576 } else {
3577 // decompress map data
3578 if (!Map->Compressed) {
3579 DecompressedData.setLength(Map->Data.length());
3580 if (Map->Data.length()) memcpy(DecompressedData.ptr(), Map->Data.ptr(), Map->Data.length());
3581 } else {
3582 VArrayStream *ArrStrm = new VArrayStream("<savemap:mapdata>", Map->Data);
3583 /*VZLibStreamReader*/
3584 VStream *ZipStrm = new VZLibStreamReader(ArrStrm,
3585 Map->Data.length()/*VZLibStreamReader::UNKNOWN_SIZE*/,
3586 Map->DecompressedSize);
3587 DecompressedData.setLength(Map->DecompressedSize);
3588 ZipStrm->Serialise(DecompressedData.Ptr(), DecompressedData.length());
3589 const bool wasErr = ZipStrm->IsError();
3590 VStream::Destroy(ZipStrm);
3591 ArrStrm->Close(); delete ArrStrm;
3592 if (wasErr) Host_Error("error decompressing savegame data");
3594 Loader = new VSaveLoaderStream(new VArrayStream("<savemap:mapdata>", DecompressedData));
3597 // load names
3598 UnarchiveNames(Loader);
3600 // read the level timer
3601 if (Loader->IsNewFormat()) {
3602 Loader->OpenFile(NEWFMT_FNAME_MAP_GINFO);
3603 } else {
3604 AssertSegment(*Loader, ASEG_MAP_HEADER);
3606 *Loader << GLevel->Time << GLevel->TicTime;
3608 UnarchiveThinkers(Loader);
3609 UnarchiveSounds(Loader);
3611 if (!Loader->IsNewFormat()) {
3612 AssertSegment(*Loader, ASEG_END);
3615 // free save buffer
3616 Loader->Close();
3617 delete Loader;
3619 Host_ResetSkipFrames();
3621 // do this here so that clients have loaded info, not initial one
3622 SV_SendServerInfoToClients();
3624 Host_ResetSkipFrames();
3626 return false;
3630 //==========================================================================
3632 // SV_SaveGame
3634 //==========================================================================
3635 static void SV_SaveGame (int slot, VStr Description, bool checkpoint, bool isAutosave) {
3636 BaseSlot.Description = Description;
3637 BaseSlot.CurrentMap = GLevel->MapName;
3638 BaseSlot.SavedSkill = GGameInfo->WorldInfo->GameSkill;
3640 // save out the current map
3641 if (checkpoint) {
3642 // if we have no maps in our base slot, checkpoints are enabled
3643 if (BaseSlot.Maps.length() != 0) {
3644 GCon->Logf("AUTOSAVE: cannot use checkpoints, perform a full save sequence (this is not a bug!)");
3645 checkpoint = false;
3646 } else {
3647 GCon->Logf("AUTOSAVE: checkpoints might be allowed.");
3651 #ifdef CLIENT
3652 // perform full update, so lightmap cache will be valid
3653 if (!checkpoint && GLevel->Renderer && GLevel->Renderer->isNeedLightmapCache()) {
3654 GLevel->Renderer->FullWorldUpdate(true);
3656 #endif
3658 SV_SendBeforeSaveEvent(isAutosave, checkpoint);
3660 if (checkpoint) {
3661 // player state save
3662 if (!SV_SaveCheckpoint()) {
3663 GCon->Logf("AUTOSAVE: cannot use checkpoints, perform a full save sequence (this is not a bug!)");
3664 checkpoint = false;
3665 SV_SaveMap(true); // true = save player info
3667 } else {
3668 // full save
3669 SV_SaveMap(true); // true = save player info
3672 // write data to destination slot
3673 if (BaseSlot.SaveToSlot(slot)) {
3674 // checkpoints using normal map cache
3675 if (!checkpoint && !saveFileBase.isEmpty()) {
3676 VStr ccfname = saveFileBase+".lmap";
3677 #ifdef CLIENT
3678 bool doPrecalc = (r_precalc_static_lights_override >= 0 ? !!r_precalc_static_lights_override : r_precalc_static_lights);
3679 #else
3680 enum { doPrecalc = false };
3681 #endif
3682 #ifdef CLIENT
3683 if (!GLevel->Renderer || !GLevel->Renderer->isNeedLightmapCache() || !loader_cache_data || !doPrecalc) {
3684 // no rendered usually means that this is some kind of server (the thing that should not be, but...)
3685 Sys_FileDelete(ccfname);
3686 } else {
3687 GLevel->cacheFileBase = saveFileBase;
3688 GLevel->cacheFlags &= ~VLevel::CacheFlag_Ignore;
3689 VStream *lmc = FL_OpenSysFileWrite(ccfname);
3690 if (lmc) {
3691 GCon->Logf("writing lightmap cache to '%s'", *ccfname);
3692 GLevel->Renderer->saveLightmaps(lmc);
3693 bool err = lmc->IsError();
3694 lmc->Close();
3695 err = (err || lmc->IsError());
3696 delete lmc;
3697 if (err) {
3698 GCon->Logf(NAME_Warning, "removed broken lightmap cache '%s'", *ccfname);
3699 Sys_FileDelete(ccfname);
3701 } else {
3702 GCon->Logf(NAME_Warning, "cannot create lightmap cache file '%s'", *ccfname);
3705 #endif
3707 SV_SendAfterSaveEvent(isAutosave, checkpoint);
3710 Host_ResetSkipFrames();
3714 #ifdef CLIENT
3715 //==========================================================================
3717 // SV_LoadGame
3719 //==========================================================================
3720 static void SV_LoadGame (int slot) {
3721 SV_ShutdownGame();
3723 // temp hack
3724 SV_SetupSkipCallback();
3726 if (!BaseSlot.LoadSlot(slot)) return;
3728 if (BaseSlot.SavedSkill >= 0) {
3729 //GCon->Logf(NAME_Debug, "*** SAVED SKILL: %d", BaseSlot.SavedSkill);
3730 Skill = BaseSlot.SavedSkill;
3733 sv_loading = true;
3735 // load the current map
3736 if (!SV_LoadMap(BaseSlot.CurrentMap, true/*allowCheckpoints*/, false/*hubTeleport*/)) {
3737 // not a checkpoint
3738 GLevel->cacheFileBase = saveFileBase;
3739 GLevel->cacheFlags &= ~VLevel::CacheFlag_Ignore;
3740 //GCon->Logf(NAME_Debug, "**********************: <%s>", *GLevel->cacheFileBase);
3741 #ifdef CLIENT
3742 if (GGameInfo->NetMode != NM_DedicatedServer) CL_SetupLocalPlayer();
3743 #endif
3744 // launch waiting scripts
3745 if (!svs.deathmatch) GLevel->Acs->CheckAcsStore();
3747 //GCon->Logf(NAME_Debug, "************************** (%d)", svs.max_clients);
3748 for (int i = 0; i < MAXPLAYERS; ++i) {
3749 VBasePlayer *Player = GGameInfo->Players[i];
3750 if (!Player) {
3751 //GCon->Logf(NAME_Debug, "*** no player #%d", i);
3752 continue;
3754 Player->eventAfterUnarchiveThinkers();
3756 } else {
3757 // checkpoint
3758 GLevel->cacheFlags &= ~VLevel::CacheFlag_Ignore;
3761 SV_SendLoadedEvent();
3763 #endif
3766 //==========================================================================
3768 // SV_ClearBaseSlot
3770 //==========================================================================
3771 void SV_ClearBaseSlot () {
3772 BaseSlot.Clear(!dbg_save_in_old_format.asBool());
3776 //==========================================================================
3778 // SV_MapTeleport
3780 //==========================================================================
3782 CHANGELEVEL_KEEPFACING = 0x00000001,
3783 CHANGELEVEL_RESETINVENTORY = 0x00000002,
3784 CHANGELEVEL_NOMONSTERS = 0x00000004,
3785 CHANGELEVEL_CHANGESKILL = 0x00000008,
3786 CHANGELEVEL_NOINTERMISSION = 0x00000010,
3787 CHANGELEVEL_RESETHEALTH = 0x00000020,
3788 CHANGELEVEL_PRERAISEWEAPON = 0x00000040,
3790 void SV_MapTeleport (VName mapname, int flags, int newskill) {
3791 TArray<VThinker *> TravelObjs;
3793 if (newskill >= 0 && (flags&CHANGELEVEL_CHANGESKILL) != 0) {
3794 GCon->Logf("SV_MapTeleport: new skill is %d", newskill);
3795 Skill = newskill;
3796 flags &= ~CHANGELEVEL_CHANGESKILL; // clear flag
3799 // we won't show intermission anyway, so remove this flag
3800 flags &= ~CHANGELEVEL_NOINTERMISSION;
3802 if (flags&~(CHANGELEVEL_KEEPFACING|CHANGELEVEL_RESETINVENTORY|CHANGELEVEL_RESETHEALTH|CHANGELEVEL_PRERAISEWEAPON|CHANGELEVEL_REMOVEKEYS)) {
3803 GCon->Logf("SV_MapTeleport: unimplemented flag set: 0x%04x", (unsigned)flags);
3806 TAVec plrAngles[MAXPLAYERS];
3807 memset((void *)plrAngles, 0, sizeof(plrAngles));
3809 // call PreTravel event
3810 for (int i = 0; i < MAXPLAYERS; ++i) {
3811 if (!GGameInfo->Players[i]) continue;
3812 plrAngles[i] = GGameInfo->Players[i]->ViewAngles;
3813 GGameInfo->Players[i]->eventPreTravel();
3816 // collect list of thinkers that will go to the new level (player inventory)
3817 for (VThinker *Th = GLevel->ThinkerHead; Th; Th = Th->Next) {
3818 VEntity *vent = Cast<VEntity>(Th);
3819 if (vent && vent->Owner && vent->Owner->IsPlayer()) {
3820 TravelObjs.Append(vent);
3821 GLevel->RemoveThinker(vent);
3822 vent->UnlinkFromWorld();
3823 GLevel->DelSectorList();
3824 vent->StopSound(0); // stop all sounds
3825 //GCon->Logf(NAME_Debug, "SV_MapTeleport: saved player inventory item '%s'", vent->GetClass()->GetName());
3826 continue;
3828 if (Th->IsA(VPlayerReplicationInfo::StaticClass())) {
3829 TravelObjs.Append(Th);
3830 GLevel->RemoveThinker(Th);
3834 if (!svs.deathmatch) {
3835 const VMapInfo &old_info = P_GetMapInfo(GLevel->MapName);
3836 const VMapInfo &new_info = P_GetMapInfo(mapname);
3837 // all maps in cluster 0 are treated as in different clusters
3838 if (old_info.Cluster && old_info.Cluster == new_info.Cluster &&
3839 (P_GetClusterDef(old_info.Cluster)->Flags&CLUSTERF_Hub))
3841 // same cluster: save map without saving player mobjs
3842 SV_SaveMap(false);
3843 } else {
3844 // entering new cluster: clear base slot
3845 if (dbg_save_verbose&0x20) GCon->Logf("**** NEW CLUSTER ****");
3846 BaseSlot.Clear(!dbg_save_in_old_format.asBool());
3850 vuint8 oldNoMonsters = GGameInfo->nomonsters;
3851 if (flags&CHANGELEVEL_NOMONSTERS) GGameInfo->nomonsters = 1;
3853 sv_map_travel = true;
3854 if (!svs.deathmatch && BaseSlot.FindMap(mapname)) {
3855 // unarchive map
3856 SV_LoadMap(mapname, false/*allowCheckpoints*/, true/*hubTeleport*/); // don't allow checkpoints
3857 } else {
3858 // new map
3859 SV_SpawnServer(*mapname, true/*spawn thinkers*/);
3860 // if we spawned a new server, there is no need to reset inventory, health or keys
3861 flags &= ~(CHANGELEVEL_RESETINVENTORY|CHANGELEVEL_RESETHEALTH|CHANGELEVEL_REMOVEKEYS);
3864 if (flags&CHANGELEVEL_NOMONSTERS) GGameInfo->nomonsters = oldNoMonsters;
3866 // add traveling thinkers to the new level
3867 for (int i = 0; i < TravelObjs.length(); ++i) {
3868 //GCon->Logf(NAME_Debug, "SV_MapTeleport: adding back player inventory item '%s'", TravelObjs[i]->GetClass()->GetName());
3869 GLevel->AddThinker(TravelObjs[i]);
3870 VEntity *Ent = Cast<VEntity>(TravelObjs[i]);
3871 if (Ent) Ent->LinkToWorld(true);
3874 Host_ResetSkipFrames();
3876 #ifdef CLIENT
3877 bool doSaveGame = false;
3878 if (GGameInfo->NetMode == NM_TitleMap ||
3879 GGameInfo->NetMode == NM_Standalone ||
3880 GGameInfo->NetMode == NM_ListenServer)
3882 CL_SetupStandaloneClient();
3883 doSaveGame = sv_new_map_autosave;
3886 if (doSaveGame && fsys_hasMapPwads && fsys_PWadMaps.length()) {
3887 // do not autosave on iwad maps
3888 doSaveGame = false;
3889 for (auto &&lmp : fsys_PWadMaps) {
3890 if (lmp.mapname.strEquCI(*mapname)) {
3891 doSaveGame = true;
3892 break;
3895 if (!doSaveGame) GCon->Logf(NAME_Warning, "autosave skipped due to iwad map");
3897 #else
3898 const bool doSaveGame = false;
3899 #endif
3901 if (flags&(CHANGELEVEL_KEEPFACING|CHANGELEVEL_RESETINVENTORY|CHANGELEVEL_RESETHEALTH|CHANGELEVEL_PRERAISEWEAPON|CHANGELEVEL_REMOVEKEYS)) {
3902 for (int i = 0; i < MAXPLAYERS; ++i) {
3903 VBasePlayer *plr = GGameInfo->Players[i];
3904 if (!plr) continue;
3906 if (flags&CHANGELEVEL_KEEPFACING) {
3907 plr->ViewAngles = plrAngles[i];
3908 plr->eventClientSetAngles(plrAngles[i]);
3909 plr->PlayerFlags &= ~VBasePlayer::PF_FixAngle;
3911 if (flags&CHANGELEVEL_RESETINVENTORY) plr->eventResetInventory();
3912 if (flags&CHANGELEVEL_RESETHEALTH) plr->eventResetHealth();
3913 if (flags&CHANGELEVEL_PRERAISEWEAPON) plr->eventPreraiseWeapon();
3914 if (flags&CHANGELEVEL_REMOVEKEYS) plr->eventRemoveKeys();
3918 // launch waiting scripts
3919 if (!svs.deathmatch) GLevel->Acs->CheckAcsStore();
3921 if (doSaveGame) GCmdBuf << "AutoSaveEnter\n";
3924 for (int i = 0; i < MAXPLAYERS; ++i) {
3925 VBasePlayer *plr = GGameInfo->Players[i];
3926 if (!plr) continue;
3927 VEntity *sve = plr->ehGetSavedInventory();
3928 if (!sve) continue;
3929 GCon->Logf(NAME_Debug, "+++ player #%d saved inventory +++", i);
3930 sve->DebugDumpInventory(true);
3936 #ifdef CLIENT
3937 void Draw_SaveIcon ();
3938 void Draw_LoadIcon ();
3941 //==========================================================================
3943 // CheckIfLoadIsAllowed
3945 //==========================================================================
3946 static bool CheckIfLoadIsAllowed () {
3947 if (svs.deathmatch) {
3948 GCon->Log("Can't load in deathmatch game");
3949 return false;
3952 return true;
3954 #endif
3957 //==========================================================================
3959 // CheckIfSaveIsAllowed
3961 //==========================================================================
3962 static bool CheckIfSaveIsAllowed () {
3963 if (svs.deathmatch) {
3964 GCon->Log("Can't save in deathmatch game");
3965 return false;
3968 if (GGameInfo->NetMode == NM_None || GGameInfo->NetMode == NM_TitleMap || GGameInfo->NetMode == NM_Client) {
3969 GCon->Log("You can't save if you aren't playing!");
3970 return false;
3973 if (sv.intermission) {
3974 GCon->Log("You can't save while in intermission!");
3975 return false;
3978 return true;
3982 //==========================================================================
3984 // BroadcastSaveText
3986 //==========================================================================
3987 static void BroadcastSaveText (const char *msg) {
3988 if (!msg || !msg[0]) return;
3989 if (sv_save_messages) {
3990 for (int i = 0; i < MAXPLAYERS; ++i) {
3991 VBasePlayer *plr = GGameInfo->Players[i];
3992 if (!plr) continue;
3993 if ((plr->PlayerFlags&VBasePlayer::PF_Spawned) == 0) continue;
3994 plr->eventClientPrint(msg);
3996 } else {
3997 GCon->Log(msg);
4002 //==========================================================================
4004 // SV_AutoSave
4006 //==========================================================================
4007 void SV_AutoSave (bool checkpoint) {
4008 if (!CheckIfSaveIsAllowed()) return;
4010 int aslot = SV_FindAutosaveSlot();
4011 if (!aslot) {
4012 BroadcastSaveText("Cannot find autosave slot (this should not happen)!");
4013 return;
4016 #ifdef CLIENT
4017 Draw_SaveIcon();
4018 #endif
4020 TTimeVal tv;
4021 GetTimeOfDay(&tv);
4022 VStr svname = TimeVal2Str(&tv, true)+": "+VStr("AUTO: ")+(*GLevel->MapName);
4024 SV_SaveGame(aslot, svname, checkpoint, true);
4025 Host_ResetSkipFrames();
4027 BroadcastSaveText(va("Game autosaved to slot #%d", -aslot));
4031 #ifdef CLIENT
4032 //==========================================================================
4034 // SV_AutoSaveOnLevelExit
4036 //==========================================================================
4037 void SV_AutoSaveOnLevelExit () {
4038 if (!dbg_save_on_level_exit) return;
4040 if (!CheckIfSaveIsAllowed()) return;
4042 int aslot = SV_FindAutosaveSlot();
4043 if (!aslot) {
4044 BroadcastSaveText("Cannot find autosave slot (this should not happen)!");
4045 return;
4048 Draw_SaveIcon();
4050 TTimeVal tv;
4051 GetTimeOfDay(&tv);
4052 VStr svname = TimeVal2Str(&tv, true)+": "+VStr("OUT: ")+(*GLevel->MapName);
4054 SV_SaveGame(aslot, svname, false, true); // not a checkpoint, obviously
4055 Host_ResetSkipFrames();
4057 BroadcastSaveText(va("Game autosaved to slot #%d", -aslot));
4061 //==========================================================================
4063 // COMMAND Save
4065 // Called by the menu task. Description is a 24 byte text string
4067 //==========================================================================
4068 COMMAND(Save) {
4069 if (Args.length() != 3) {
4070 GCon->Log("usage: save slotindex description");
4071 return;
4074 if (!CheckIfSaveIsAllowed()) return;
4076 if (Args[2].Length() >= 32) {
4077 BroadcastSaveText("Description too long!");
4078 return;
4081 Draw_SaveIcon();
4083 SV_SaveGame(VStr::atoi(*Args[1]), Args[2], false, false); // not a checkpoint
4084 Host_ResetSkipFrames();
4086 BroadcastSaveText("Game saved.");
4090 //==========================================================================
4092 // COMMAND DeleteSavedGame <slotidx|quick>
4094 //==========================================================================
4095 COMMAND(DeleteSavedGame) {
4096 //GCon->Logf("DeleteSavedGame: argc=%d", Args.length());
4098 if (Args.length() != 2) return;
4100 if (!CheckIfLoadIsAllowed()) return;
4102 VStr numstr = Args[1].xstrip();
4103 if (numstr.isEmpty()) return;
4105 //GCon->Logf("DeleteSavedGame: <%s>", *numstr);
4107 if (/*numstr.ICmp("quick") == 0*/numstr.startsWithCI("q")) {
4108 if (SV_DeleteSlotFile(QUICKSAVE_SLOT)) BroadcastSaveText("Quicksave deleted.");
4109 return;
4112 int pos = 0;
4113 while (pos < numstr.length() && (vuint8)numstr[pos] <= ' ') ++pos;
4114 if (pos >= numstr.length()) return;
4116 bool neg = false;
4117 if (numstr[pos] == '-') {
4118 neg = true;
4119 ++pos;
4120 if (pos >= numstr.length()) return;
4123 int slot = 0;
4124 while (pos < numstr.length()) {
4125 char ch = numstr[pos++];
4126 if (ch < '0' || ch > '9') return;
4127 slot = slot*10+ch-'0';
4129 //GCon->Logf("DeleteSavedGame: slot=%d (neg=%d)", slot, (neg ? 1 : 0));
4130 if (neg && slot == abs(QUICKSAVE_SLOT)) {
4131 slot = QUICKSAVE_SLOT;
4132 } else {
4133 if (slot < 0 || slot > 99) return;
4134 if (neg) slot = -slot;
4137 if (SV_DeleteSlotFile(slot)) {
4138 if (slot == QUICKSAVE_SLOT) BroadcastSaveText("Quicksave deleted.");
4139 else if (slot < 0) BroadcastSaveText(va("Autosave #%d deleted", -slot));
4140 else BroadcastSaveText(va("Savegame #%d deleted", slot));
4145 //==========================================================================
4147 // COMMAND Load
4149 //==========================================================================
4150 COMMAND(Load) {
4151 if (Args.length() != 2) return;
4153 if (!CheckIfLoadIsAllowed()) return;
4155 int slot = VStr::atoi(*Args[1]);
4156 VStr desc;
4157 if (!SV_GetSaveString(slot, desc)) {
4158 BroadcastSaveText("Empty slot!");
4159 return;
4161 GCon->Logf("Loading \"%s\"", *desc);
4163 Draw_LoadIcon();
4164 SV_LoadGame(slot);
4165 Host_ResetSkipFrames();
4167 //if (GGameInfo->NetMode == NM_Standalone) SV_UpdateRebornSlot(); // copy the base slot to the reborn slot
4168 BroadcastSaveText(va("Loaded save \"%s\".", *desc));
4172 //==========================================================================
4174 // COMMAND QuickSave
4176 //==========================================================================
4177 COMMAND(QuickSave) {
4178 if (!CheckIfSaveIsAllowed()) return;
4180 Draw_SaveIcon();
4182 SV_SaveGame(QUICKSAVE_SLOT, "quicksave", false, false); // not a checkpoint
4183 Host_ResetSkipFrames();
4185 BroadcastSaveText("Game quicksaved.");
4189 //==========================================================================
4191 // COMMAND QuickLoad
4193 //==========================================================================
4194 COMMAND(QuickLoad) {
4195 if (!CheckIfLoadIsAllowed()) return;
4197 VStr desc;
4198 if (!SV_GetSaveString(QUICKSAVE_SLOT, desc)) {
4199 BroadcastSaveText("Empty quicksave slot");
4200 return;
4202 GCon->Log("Loading quicksave");
4204 Draw_LoadIcon();
4205 SV_LoadGame(QUICKSAVE_SLOT);
4206 Host_ResetSkipFrames();
4207 // don't copy to reborn slot -- this is quickload after all!
4209 BroadcastSaveText("Quicksave loaded.");
4213 //==========================================================================
4215 // COMMAND AutoSaveEnter
4217 //==========================================================================
4218 COMMAND(AutoSaveEnter) {
4219 // there is no reason to autosave on standard maps when we have pwads
4220 if (!CheckIfSaveIsAllowed()) return;
4222 int aslot = SV_FindAutosaveSlot();
4223 if (!aslot) {
4224 BroadcastSaveText("Cannot find autosave slot (this should not happen)!");
4225 return;
4228 Draw_SaveIcon();
4230 TTimeVal tv;
4231 GetTimeOfDay(&tv);
4232 VStr svname = TimeVal2Str(&tv, true)+": "+(*GLevel->MapName);
4234 SV_SaveGame(aslot, svname, sv_autoenter_checkpoints, true);
4235 Host_ResetSkipFrames();
4237 BroadcastSaveText(va("Game autosaved to slot #%d", -aslot));
4241 //==========================================================================
4243 // COMMAND AutoSaveLeave
4245 //==========================================================================
4246 COMMAND(AutoSaveLeave) {
4247 if (GGameInfo->NetMode == NM_None || GGameInfo->NetMode == NM_Client) return;
4248 SV_AutoSaveOnLevelExit();
4249 Host_ResetSkipFrames();
4253 //==========================================================================
4255 // COMMAND ShowSavePrefix
4257 //==========================================================================
4258 COMMAND(ShowSavePrefix) {
4259 auto wadlist = FL_GetWadPk3ListSmall();
4260 GCon->Log("==== MODS ====");
4261 for (auto &&mname: wadlist) GCon->Logf(" %s", *mname);
4262 GCon->Log("----");
4263 wadlist = FL_GetWadPk3List();
4264 GCon->Log("==== MODS (full) ====");
4265 for (auto &&mname: wadlist) GCon->Logf(" %s", *mname);
4266 GCon->Log("----");
4267 vuint64 hash = SV_GetModListHash(nullptr);
4268 VStr pfx = VStr::buf2hex(&hash, 8);
4269 GCon->Logf("save prefix: %s", *pfx);
4271 #endif
4274 #endif