1 //**************************************************************************
3 //** ## ## ## ## ## #### #### ### ###
4 //** ## ## ## ## ## ## ## ## ## ## #### ####
5 //** ## ## ## ## ## ## ## ## ## ## ## ## ## ##
6 //** ## ## ######## ## ## ## ## ## ## ## ### ##
7 //** ### ## ## ### ## ## ## ## ## ##
8 //** # ## ## # #### #### ## ##
10 //** Copyright (C) 1999-2006 Jānis Legzdiņš
11 //** Copyright (C) 2018-2023 Ketmar Dark
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.
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.
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/>.
25 //**************************************************************************
27 //** Archiving: SaveGame I/O.
29 //**************************************************************************
30 //#define USE_OLD_SAVE_CODE
32 #ifdef USE_OLD_SAVE_CODE
33 # include "sv_save.old.cpp"
36 //#define VXX_DEBUG_SECTION_READER
37 //#define VXX_DEBUG_SECTION_WRITER
39 #include "../gamedefs.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"
48 # include "../drawer.h" /* VRenderLevelPublic */
49 # include "../client/client.h"
50 # include "../sound/sound.h"
52 #include "../utils/qs_data.h"
53 #include "../decorate/vc_decorate.h" /* ListLoaderCanSkipClass */
64 static inline struct tm
*localtime_r (const time_t *_Time
, struct tm
*_Tm
) {
65 return (localtime_s(_Tm
, _Time
) ? NULL
: _Tm
);
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 // ////////////////////////////////////////////////////////////////////////// //
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,
163 // extra gameslot data
165 GSLOT_DATA_SKILL
= 202,
167 GSLOT_DATA_START
= 667,
168 GSLOT_DATA_END
= 666,
172 // ////////////////////////////////////////////////////////////////////////// //
173 class VSavedCheckpoint
{
178 VStr ClassName
; // used only in loader
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
) {
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;
199 ei
.ClassName
= VStr(ent
->GetClass()->GetName());
202 int FindEntity (VEntity
*ent
) const {
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
217 Skill
= -1; // this should be "no skill"
220 void Serialise (VStream
*strm
) {
222 // note that we cannot use `VNTValueIOEx` here, because names are already written!
223 if (strm
->IsLoading()) {
224 // load players inventory
228 *strm
<< STRM_INDEX(rw
);
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();
238 *strm
<< ei
.ClassName
;
239 if (dbg_save_verbose
&0x20) GCon
->Logf(" ent #%d: '%s'", f
+1, *ei
.ClassName
);
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();
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");
253 QSList
.removeAt(QSList
.length()-1);
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
);
262 // save players inventory
264 vint32 rw
= ReadyWeapon
;
265 *strm
<< STRM_INDEX(rw
);
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;
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
);
282 for (auto &&v
: QSList
) v
.Serialise(*strm
);
283 //GCon->Logf("*** SAVE: rw=%d; entCount=%d", rw, entCount);
289 // ////////////////////////////////////////////////////////////////////////// //
295 // for new format, we are keeping map vwads here
296 TArrayNC
<vuint8
> Data
;
297 // only for old format
299 vint32 DecompressedSize
;
302 VSavedMap (bool asNewFormat
) : Compressed(asNewFormat
? 69 : 0), DecompressedSize(0) {}
304 inline void ClearData (bool asNewFormat
) {
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 {
316 uint8_t hash[RIPEMD160_BYTES];
317 ripemd160_init(&ctx);
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
339 TArray
<VSavedMap
*> Maps
; // if there are no maps, it is a checkpoint
341 VSavedCheckpoint CheckPoint
;
342 vint32 SavedSkill
; // -1: don't change/unknown
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
);
357 VSaveSlot () : SavedSkill(-1), newFormat(true) {}
358 ~VSaveSlot () { Clear(true); }
360 inline bool IsNewFormat () const noexcept
{ return newFormat
; }
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
{
379 TArray
<VStr
> strTable
;
380 TMap
<VStr
, int> strMap
;
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
{
394 auto kp
= strMap
.get(s
);
398 sidx
= strTable
.length();
399 if (sidx
>= 1024 * 1024) {
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();
415 // string #0 is always empty
416 for (int f
= 1; f
< tlen
; f
+= 1) {
418 if (st
->IsError()) return;
424 class VStreamIOStrMapperLoader
: public VStreamIOStrMapper
{
426 TArray
<VStr
> strTable
;
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
{
437 *strm
<< STRM_INDEX(sidx
);
438 if (strm
->IsError() || sidx
< 0 || sidx
>= strTable
.length()) {
443 //GCon->Logf(NAME_Debug, "RDSM: string #%d: <%s>", sidx, *s);
446 void LoadStrings (VStream
*strm
) {
451 if (strm
->IsError() || count
< 1 || count
> 1024*1024) {
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
{
471 VStreamIOStrMapperLoader
*strMapper
;
474 // extended sections support
476 TMap
<VStrCI
, int> esections
;
479 VStream
*currestream
;
482 inline VStream
*GetCurrStream () const {
483 return (!esecclosed
? currestream
: Stream
);
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
);
503 TArray
<VName
> NameRemap
;
504 TArray
<VObject
*> Exports
;
505 TArray
<VLevelScriptThinker
*> AcsExports
;
508 VV_DISABLE_COPY(VSaveLoaderStream
)
510 VSaveLoaderStream (VStream
*InStream
)
519 VSaveLoaderStream (VVWadArchive
*avwad
)
529 virtual ~VSaveLoaderStream () override
{
530 AttachStringMapper(nullptr);
532 VStream::Destroy(currestream
);
533 VStream::Destroy(Stream
);
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
) {
543 if (Stream
->IsError()) SetError();
544 VStream::Destroy(Stream
);
546 Stream
= vwad
->OpenFile(name
);
549 Host_Error("cannot find save part named '%s'", *name
);
551 Stream
->AttachStringMapper(strMapper
);
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
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
);
576 virtual bool ExtendedSection (VStr name
) override
{
577 if (IsNewFormat() && !IsError()) {
578 if (name
.isEmpty()) {
579 #ifdef VXX_DEBUG_SECTION_READER
581 GCon
->Logf(NAME_Debug
, "RDX: closed section '%s'", *currsection
);
586 // check last used section
587 if (currsection
.strEquCI(name
)) {
588 vassert(currestream
!= nullptr);
589 #ifdef VXX_DEBUG_SECTION_READER
591 GCon
->Logf(NAME_Debug
, "RDX: opened section '%s'", *currsection
);
597 // save current section position
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
);
604 if (currestream
->IsError() || pos
< 0) {
608 VStream::Destroy(currestream
);
609 esections
.put(currsection
, pos
);
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
);
622 currestream
->AttachStringMapper(strMapper
);
623 // restore section position, if there is any
624 auto kv
= esections
.get(name
);
627 #ifdef VXX_DEBUG_SECTION_READER
628 GCon
->Logf(NAME_Debug
, "RDX: restored section '%s' state (pos=%d)", *name
, *kv
);
630 currestream
->Seek(*kv
);
631 if (currestream
->IsError()) {
636 #ifdef VXX_DEBUG_SECTION_READER
637 GCon
->Logf(NAME_Debug
, "RDX: opened section '%s'", *name
);
647 VStr
CurrentExtendedSection () override
{
648 return (!esecclosed
? currsection
: VStr::EmptyString
);
651 virtual void SetError () override
{
652 VStream::Destroy(currestream
);
656 VStream::Destroy(Stream
);
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();
670 s
->Serialise(Data
, Len
);
671 if (s
->IsError()) SetError();
677 virtual void Seek (int Pos
) override
{
678 VStream
*s
= GetCurrStream();
681 if (s
->IsError()) SetError();
687 virtual int Tell () override
{
688 VStream
*s
= GetCurrStream();
692 if (s
->IsError()) SetError();
699 virtual int TotalSize () override
{
700 VStream
*s
= GetCurrStream();
703 res
= s
->TotalSize();
704 if (s
->IsError()) SetError();
711 virtual bool AtEnd () override
{
712 VStream
*s
= GetCurrStream();
716 if (s
->IsError()) SetError();
723 virtual void Flush () override
{
724 VStream
*s
= GetCurrStream();
727 if (s
->IsError()) SetError();
733 virtual bool Close () override
{
734 bool err
= IsError();
736 if (!err
) err
= !Stream
->Close();
737 VStream::Destroy(Stream
);
739 VStream::Destroy(currestream
);
744 if (!err
) vwad
->Close();
745 delete vwad
; vwad
= nullptr;
747 if (err
) SetError(); // just in case
751 virtual void io (VSerialisable
*&Ref
) override
{
753 *this << STRM_INDEX(scpIndex
);
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
{
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
{
774 *this << STRM_INDEX(TmpIdx
);
777 } else if (TmpIdx
> 0) {
778 if (TmpIdx
> Exports
.length()) Sys_Error("Bad index %d", TmpIdx
);
779 Ref
= Exports
[TmpIdx
-1];
781 //GCon->Logf(NAME_Debug, "IO OBJECT: '%s'", Ref->GetClass()->GetName());
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
{
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);
800 if (developer
) GCon
->Logf(NAME_Warning
, "Don't know how to handle pointer to %s", *Struct
->Name
);
807 // ////////////////////////////////////////////////////////////////////////// //
808 class VSaveWriterStream
: public VStream
{
810 VVWadNewArchive
*vwad
;
811 VStreamIOStrMapperWriter
*strMapper
;
815 // extended sections support
820 TArray
<ExtSection
*> esections
;
825 TArray
<VObject
*> Exports
;
826 TArray
<vint32
> NamesMap
;
827 TMapNC
<vuint32
, vint32
> ObjectsMap
; // key: object uid; value: internal index
828 TArray
</*VLevelScriptThinker*/VSerialisable
*> AcsExports
;
832 inline VStream
*GetCurrStream () const {
833 return (currsection
>= 0 ? esections
[currsection
]->est
: Stream
);
836 void WipeESections () {
838 for (int f
= 0; f
< esections
.length(); f
+= 1) {
839 ExtSection
*es
= esections
[f
];
840 esections
[f
] = nullptr;
842 if (es
->est
) { es
->est
->Close(); delete es
->est
; }
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
;
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());
873 VStream
*xst
= vwad
->CreateFileDirect(es
->name
, level
);
875 GCon
->Logf(NAME_Error
, "cannot create save writer subsection '%s'", *es
->name
);
879 if (es
->est
->TotalSize() != 0) {
880 xst
->Serialise(es
->est
->GetArray().Ptr(), es
->est
->GetArray().length());
882 const bool err
= !xst
->Close();
884 if (err
) { SetError(); break; }
892 NamesMap
.setLength(VName::GetNumNames());
893 for (int i
= 0; i
< VName::GetNumNames(); ++i
) NamesMap
[i
] = -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
) {
901 if (Stream
!= nullptr) CloseFile();
903 GCon
->Logf(NAME_Debug
, "CREATE: %s", *name
);
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
;
915 if (name
.endsWithNoCase(".vwad")) level
= VWADWR_COMP_DISABLE
;
918 Stream
= vwad
->CreateFileBuffered(name
, level
);
920 Stream
= vwad
->CreateFileDirect(name
, level
);
923 Stream
->AttachStringMapper(strMapper
);
934 VV_DISABLE_COPY(VSaveWriterStream
)
936 // takes ownership of the passed stream
937 VSaveWriterStream (VStream
*InStream
)
945 // takes ownership of the passed archive object
946 // will properly close and destroy the archive
947 VSaveWriterStream (VVWadNewArchive
*avwad
)
954 strMapper
= new VStreamIOStrMapperWriter();
955 AttachStringMapper(strMapper
);
959 virtual ~VSaveWriterStream () override
{
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
969 if (vwad
&& Stream
) {
971 GCon
->Logf(NAME_Debug
, "CLOSE: %s", *Stream
->GetName());
973 bool err
= IsError();
976 err
= Stream
->IsError();
978 delete Stream
; Stream
= nullptr;
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); }
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
);
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
)) {
1009 if (sidx
>= esections
.length()) {
1011 ExtSection
*es
= new ExtSection();
1013 es
->est
= new VMemoryStream();
1014 es
->est
->BeginWrite();
1015 es
->est
->AttachStringMapper(strMapper
);
1016 sidx
= esections
.length();
1017 esections
.Append(es
);
1020 #ifdef VXX_DEBUG_SECTION_READER
1021 if (currsection
>= 0) {
1022 GCon
->Logf(NAME_Debug
, "WRX: opened section '%s'", *esections
[currsection
]->name
);
1031 VStr
CurrentExtendedSection () override
{
1032 return (currsection
>= 0 ? esections
[currsection
]->name
: VStr::EmptyString
);
1035 virtual void SetError () override
{
1036 VStream::Destroy(Stream
);
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();
1052 if (!err
&& Stream
) {
1054 err
= Stream
->IsError();
1056 delete Stream
; Stream
= nullptr;
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
);
1072 strMapper
->WriteStrings(wo
);
1074 VStream::Destroy(wo
);
1079 if (!err
) err
= !vwad
->Close();
1081 delete vwad
; vwad
= nullptr;
1083 if (Stream
) { err
= !Stream
->Close(); Stream
= nullptr; }
1085 if (err
) SetError(); // just in case
1089 virtual void Serialise (void *Data
, int Len
) override
{
1090 VStream
*s
= GetCurrStream();
1092 s
->Serialise(Data
, Len
);
1093 if (s
->IsError()) SetError();
1099 virtual void Seek (int Pos
) override
{
1100 VStream
*s
= GetCurrStream();
1103 if (s
->IsError()) SetError();
1109 virtual int Tell () override
{
1110 VStream
*s
= GetCurrStream();
1114 if (s
->IsError()) SetError();
1121 virtual int TotalSize () override
{
1122 VStream
*s
= GetCurrStream();
1125 res
= s
->TotalSize();
1126 if (s
->IsError()) SetError();
1133 virtual bool AtEnd () override
{
1134 VStream
*s
= GetCurrStream();
1138 if (s
->IsError()) SetError();
1145 virtual void Flush () override
{
1146 VStream
*s
= GetCurrStream();
1149 if (s
->IsError()) SetError();
1155 void RegisterObject (VObject
*o
) {
1157 if (ObjectsMap
.has(o
->GetUniqueId())) return;
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());
1166 if (dbg_save_verbose
&0x02) GCon
->Logf("*** unique object (%u : %s)", o
->GetUniqueId(), *o
->GetClass()->GetFullName());
1168 ObjectsMap
.put(o
->GetUniqueId(), Exports
.length());
1171 virtual void io (VSerialisable
*&Ref
) override
{
1172 vint32 scpIndex
= 0;
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
);
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();
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
{
1199 if (!Ref
/*|| !Ref->IsGoingToDie()*/) {
1202 //TmpIdx = ObjectsMap[Ref->GetObjectIndex()];
1203 auto ppp
= ObjectsMap
.get(Ref
->GetUniqueId());
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());
1213 *this << STRM_INDEX(TmpIdx
);
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
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
{
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);
1240 if (developer
) GCon
->Logf(NAME_Dev
, "Don't know how to handle pointer to %s", *Struct
->Name
);
1243 *this << STRM_INDEX(TmpIdx
);
1248 // because dedicated server cannot save games yet
1250 static bool skipCallbackInited
= false;
1251 static VName oldPlrClassName
= NAME_None
;
1252 static VName newPlrClassName
= NAME_None
;
1253 static VName clsPlayerExName
= NAME_None
;
1256 //==========================================================================
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());
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;
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).",
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);
1334 //==========================================================================
1338 //==========================================================================
1339 static VStr
SV_GetSavesDir () {
1340 return FL_GetSavesDir();
1344 //==========================================================================
1346 // GetSaveSlotDirectoryPrefixOld
1348 //==========================================================================
1349 static VStr
GetSaveSlotCommonDirectoryPrefixOld0 () {
1351 (void)SV_GetModListHashOld(&hash
);
1352 VStr pfx
= VStr::buf2hex(&hash
, 4);
1357 //==========================================================================
1359 // GetSaveSlotCommonDirectoryPrefixOld1
1361 //==========================================================================
1362 static VStr
GetSaveSlotCommonDirectoryPrefixOld1 () {
1363 vuint64 hash
= SV_GetModListHashOld(nullptr);
1364 VStr pfx
= VStr::buf2hex(&hash
, 8);
1369 //==========================================================================
1371 // GetSaveSlotDirectoryPrefix
1373 //==========================================================================
1374 static VStr
GetSaveSlotCommonDirectoryPrefix () {
1375 vuint64 hash
= SV_GetModListHash(nullptr);
1376 VStr pfx
= VStr::buf2hex(&hash
, 8);
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()) {
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 //==========================================================================
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 //==========================================================================
1432 // returns hash of savegame directory
1434 //==========================================================================
1435 VStr
SV_GetSaveHash () {
1436 return GetSaveSlotCommonDirectoryPrefix();
1440 //==========================================================================
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
);
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);
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
);
1515 auto svpfx
= GetSaveSlotBaseFileName(slot
).extractFileName();
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;
1526 if (!seenVWad
.isEmpty()) {
1527 saveFileBase
= svdir
.appendPath(seenVWad
);
1528 } else if (!seenVsg
.isEmpty()) {
1529 saveFileBase
= svdir
.appendPath(seenVsg
);
1531 saveFileBase
.clear();
1533 if (!saveFileBase
.isEmpty()) {
1534 VStream
*st
= FL_OpenSysFileRead(saveFileBase
);
1535 if (st
!= nullptr) return st
;
1536 saveFileBase
.clear();
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];
1561 GCon
->Logf(NAME_Debug
, "SV_OpenSlotFileReadWithFmt: Slot=%d...", Slot
);
1563 Strm
= SV_OpenSlotFileRead(Slot
);
1567 GCon
->Log(NAME_Debug
, "...checking");
1571 memset(VersionText
, 0, 4);
1573 Strm
->Serialise(VersionText
, 4);
1574 if (Strm
->IsError()) {
1575 VStream::Destroy(Strm
);
1576 saveFileBase
.clear();
1580 if (memcmp(VersionText
, "VWAD", 4) == 0) {
1582 GCon
->Log(NAME_Debug
, "....VWAD");
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();
1594 if (vwad
->GetAuthor() != NEWFMT_VWAD_AUTHOR
) {
1596 GCon
->Log(NAME_Debug
, "invalid save VWAD signature");
1598 // the stream will be destroyed automatically
1599 delete vwad
; vwad
= nullptr;
1600 saveFileBase
.clear();
1605 GCon
->Log(NAME_Debug
, "....VSG!");
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();
1623 //==========================================================================
1625 // removeSlotSaveFiles
1627 // user can rename file to different case
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
);
1639 auto svpfx
= GetSaveSlotBaseFileName(slot
).extractFileName();
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
);
1658 for (int f
= 0; f
< tokill
.length(); ++f
) Sys_FileDelete(tokill
[f
]);
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
1685 for (int f
= 0; f
< descr
.length(); ++f
) {
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;
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
+ ".$$$");
1707 VStream::Destroy(res);
1708 removeSlotSaveFiles(slot);
1709 res = FL_OpenSysFileWrite(svpfx);
1710 if (res) UpdateSaveDirWadList();
1717 //==========================================================================
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());
1730 //==========================================================================
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
) {
1742 unlink(*saveFileBase
);
1744 if (rename(*fname
, *saveFileBase
) != 0) {
1745 GCon
->Logf(NAME_Error
, "Cannot rename save file for slot #%d!", Slot
);
1749 removeSlotSaveFiles(Slot
, saveFileBase
);
1750 UpdateSaveDirWadList();
1755 //==========================================================================
1757 // SV_DeleteSlotFile
1759 //==========================================================================
1760 static bool SV_DeleteSlotFile (int slot
) {
1761 if (isBadSlotIndex(slot
)) return false;
1762 return removeSlotSaveFiles(slot
, VStr::EmptyString
);
1767 // ////////////////////////////////////////////////////////////////////////// //
1769 int secs
; // actually, unsigned
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;
1784 //==========================================================================
1788 //==========================================================================
1789 static void GetTimeOfDay (TTimeVal
*tvres
) {
1793 if (gettimeofday(&tv
, nullptr)) {
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 //==========================================================================
1808 //==========================================================================
1809 static VStr
TimeVal2Str (const TTimeVal
*tvin
, bool forAutosave
=false) {
1811 tv
.tv_sec
= (((uint64_t)tvin
->secs
)&0xffffffff)|(((uint64_t)tvin
->secshi
)<<32);
1812 //tv.tv_usec = tvin->usecs;
1814 #ifndef STK_TIMET_FIX
1815 if (localtime_r(&tv
.tv_sec
, &ctm
)) {
1817 time_t tsec
= tv
.tv_sec
;
1818 if (localtime_r(&tsec
, &ctm
)) {
1822 return VStr(va("%02d:%02d", (int)ctm
.tm_hour
, (int)ctm
.tm_min
));
1825 return VStr(va("%04d/%02d/%02d %02d:%02d:%02d",
1826 (int)(ctm
.tm_year
+1900),
1834 return VStr("unknown");
1839 //==========================================================================
1843 // skip extended data
1845 //==========================================================================
1846 static bool SkipExtData (VStream
*Strm
) {
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;
1854 Strm
->Seek(Strm
->Tell()+size
);
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;
1871 memset((void *)&tv
, 0, sizeof(tv
));
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
)) {
1882 Strm
->Serialise(&tv
, sizeof(tv
));
1886 if (id
== SAVE_EXTDATA_ID_DATESTR
&& size
> 0 && size
< 64) {
1888 memset(buf
, 0, sizeof(buf
));
1889 Strm
->Serialise(buf
, size
);
1890 if (buf
[0]) res
= VStr(buf
);
1894 // skip unknown data
1895 Strm
->Seek(Strm
->Tell()+size
);
1897 if (res
.length() == 0) {
1899 res
= TimeVal2Str(&tv
);
1901 res
= VStr("UNKNOWN");
1908 //==========================================================================
1910 // LoadDateTValExtData
1912 //==========================================================================
1913 static bool LoadDateTValExtData (VStream
*Strm
, TTimeVal
*tv
) {
1914 memset((void *)tv
, 0, sizeof(*tv
));
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
);
1936 //==========================================================================
1940 //==========================================================================
1941 void VSaveSlot::Clear (bool asNewFormat
) {
1942 Description
.Clean();
1943 CurrentMap
= NAME_None
;
1945 for (int i
= 0; i
< Maps
.length(); ++i
) { delete Maps
[i
]; Maps
[i
] = nullptr; }
1948 //if (vwad) { delete vwad; vwad = nullptr; }
1949 newFormat
= asNewFormat
;
1953 //==========================================================================
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 //==========================================================================
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);
1984 if (Strm
->IsError()) {
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) {
1993 GCon
->Logf(NAME_Error
, "Invalid savegame #%d (bad number of mods: %d)", Slot
, wcount
);
1998 TArray
<VStr
> xwadlist
;
1999 for (int f
= 0; f
< wcount
; ++f
) {
2002 if (Strm
->IsError()) {
2004 GCon
->Logf(NAME_Error
, "Invalid savegame #%d (error reading modlist)", Slot
);
2006 return !(oldFormat
|| !dbg_load_ignore_wadlist
.asBool());
2011 if (!dbg_load_ignore_wadlist
.asBool()) {
2013 auto wadlist
= FL_GetWadPk3List();
2015 if (wcount
== wadlist
.length()) {
2017 for (int f
= 0; ok
&& f
< wcount
; ++f
) ok
= CheckWadCompName(xwadlist
[f
], wadlist
[f
]);
2021 auto wadlistNew
= FL_GetWadPk3ListSmall();
2022 if (wcount
== wadlistNew
.length()) {
2024 for (int f
= 0; ok
&& f
< wcount
; ++f
) ok
= CheckWadCompName(xwadlist
[f
], wadlistNew
[f
]);
2030 GCon
->Logf(NAME_Error
, "Invalid savegame #%d (bad modlist)", Slot
);
2040 //==========================================================================
2042 // VSaveSlot::LoadSlotOld
2044 // doesn't destroy stream on error
2046 //==========================================================================
2047 bool VSaveSlot::LoadSlotOld (int Slot
, VStream
*Strm
) {
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()) {
2060 // check list of loaded modules
2061 if (!CheckModList(Strm
, Slot
, true)) return false;
2065 CurrentMap
= *TmpName
;
2068 *Strm
<< STRM_INDEX(NumMaps
);
2069 for (int i
= 0; i
< NumMaps
; ++i
) {
2070 VSavedMap
*Map
= new VSavedMap(false);
2072 Map
->Index
= Maps
.length() - 1;
2073 vassert(Map
->Index
== i
);
2074 vassert(!Map
->IsNewFormat());
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());
2081 GCon
->Logf(NAME_Debug
, "Map #%d: %s (cp:%d; ds:%d; uds:%d)", i
, *Map
->Name
,
2082 Map
->Compressed
, DataLen
, Map
->DecompressedSize
);
2086 //HACK: if `NumMaps` is 0, we're loading a checkpoint
2088 // load players inventory
2089 VSavedCheckpoint
&cp
= CheckPoint
;
2091 SavedSkill
= cp
.Skill
;
2093 VSavedCheckpoint
&cp
= CheckPoint
;
2095 if (!Strm
->AtEnd()) {
2096 vuint32 seg
= 0xffffffff;
2097 *Strm
<< STRM_INDEX_U(seg
);
2098 if (!Strm
->IsError() && seg
== GSLOT_DATA_START
) {
2101 *Strm
<< STRM_INDEX_U(seg
);
2102 if (Strm
->IsError()) break;
2103 if (seg
== GSLOT_DATA_END
) break;
2104 if (seg
== GSLOT_DATA_SKILL
) {
2106 *Strm
<< STRM_INDEX(sk
);
2107 if (sk
< 0 || sk
> 31) {
2108 GCon
->Logf(NAME_Warning
, "Invalid savegame #%d skill (%d)", Slot
, sk
);
2114 GCon
->Logf(NAME_Warning
, "Invalid savegame #%d extra segment (%u)", Slot
, seg
);
2121 bool err
= Strm
->IsError();
2123 Host_ResetSkipFrames();
2126 GCon
->Logf(NAME_Error
, "Error loading savegame #%d data", Slot
);
2130 vassert(!saveFileBase
.isEmpty());
2135 //==========================================================================
2137 // VSaveSlot::LoadSlotNew
2139 // `vwad` should be set
2141 //==========================================================================
2142 bool VSaveSlot::LoadSlotNew (int Slot
, VVWadArchive
*vwad
) {
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);
2155 vuint8 savever
= 255;
2157 if (Strm
->IsError()) { VStream::Destroy(Strm
); return false; }
2158 VStream::Destroy(Strm
);
2160 GCon
->Logf(NAME_Error
, "Invalid savegame #%d version", Slot
);
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;
2184 if (Strm
->IsError()) { VStream::Destroy(Strm
); return false; }
2185 VStream::Destroy(Strm
);
2187 CurrentMap
= *TmpName
;
2190 vint32 NumMaps
= -1;
2191 Strm
= vwad
->OpenFile(NEWFMT_FNAME_SAVE_MAPLIST
);
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
) {
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
;
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
);
2220 // for checkpoint, we don't have map list at all
2224 //HACK: if `NumMaps` is 0, we're loading a checkpoint
2226 // load players inventory
2227 VSavedCheckpoint
&cp
= CheckPoint
;
2229 Strm
= vwad
->OpenFile(NEWFMT_FNAME_SAVE_CPOINT
);
2230 if (!Strm
) return false;
2232 if (Strm
->IsError()) { VStream::Destroy(Strm
); return false; }
2233 VStream::Destroy(Strm
);
2234 SavedSkill
= cp
.Skill
;
2236 VSavedCheckpoint
&cp
= CheckPoint
;
2240 Strm
= vwad
->OpenFile(NEWFMT_FNAME_SAVE_SKILL
);
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
);
2254 Host_ResetSkipFrames();
2256 vassert(!saveFileBase
.isEmpty());
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
);
2273 saveFileBase
.clear();
2274 GCon
->Logf(NAME_Error
, "Savegame #%d file doesn't exist", Slot
);
2281 res
= LoadSlotNew(Slot
, vwad
);
2284 res
= LoadSlotOld(Slot
, Strm
);
2285 if (res
&& Strm
->IsError()) res
= false;
2286 VStream::Destroy(Strm
);
2290 saveFileBase
.clear();
2291 GCon
->Logf(NAME_Error
, "Savegame #%d could not be read", Slot
);
2292 Clear(!dbg_save_in_old_format
.asBool());
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);
2310 GCon
->Logf(NAME_Error
, "cannot save to slot #%d!", Slot
);
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
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
));
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();
2353 for (int f
= 0; f
< wadlist
.length(); ++f
) *Strm
<< wadlist
[f
];
2355 // write current map
2356 VStr
TmpName(CurrentMap
);
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
2374 // save players inventory
2375 VSavedCheckpoint
&cp
= CheckPoint
;
2377 SavedSkill
= cp
.Skill
;
2379 if (SavedSkill
>= 0 && SavedSkill
< 32) {
2381 vuint32 seg
= GSLOT_DATA_START
;
2382 *Strm
<< STRM_INDEX_U(seg
);
2384 seg
= GSLOT_DATA_SKILL
;
2385 *Strm
<< STRM_INDEX_U(seg
);
2386 vint32 sk
= SavedSkill
;
2387 *Strm
<< STRM_INDEX(sk
);
2389 seg
= GSLOT_DATA_END
;
2390 *Strm
<< STRM_INDEX_U(seg
);
2394 bool err
= Strm
->IsError();
2396 err
= err
|| Strm
->IsError();
2397 delete Strm
; // done in failed
2399 Host_ResetSkipFrames();
2402 GCon
->Logf(NAME_Error
, "error saving to slot %d, savegame is corrupted!", Slot
);
2406 vassert(!saveFileBase
.isEmpty());
2411 #define CLOSE_VWAD_FILE() do { \
2412 vassert(Strm != nullptr); \
2414 const bool xserr = Strm->IsError(); \
2415 delete Strm; Strm = nullptr; \
2416 if (xserr || vmain->IsError()) { \
2417 delete vmain; vmain = nullptr; \
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); \
2437 //==========================================================================
2439 // VSaveSlot::SaveToSlotNew
2441 //==========================================================================
2442 bool VSaveSlot::SaveToSlotNew (int Slot
, VStr
&savefilename
) {
2446 saveFileBase
.clear();
2447 savefilename
.clear();
2449 VStream
*ArcStrm
= SV_CreateSlotFileWrite(Slot
, Description
, true);
2451 GCon
->Logf(NAME_Error
, "cannot save to slot %d!", Slot
);
2454 savefilename
= ArcStrm
->GetName();
2456 VVWadNewArchive
*vmain
= new VVWadNewArchive("<main-save>",
2458 Description
+ " | "+TimeVal2Str(&tv
),
2459 ArcStrm
, true/*owned*/);
2460 if (vmain
->IsError()) {
2462 GCon
->Logf(NAME_Error
, "cannot create save archive for slot %d!", Slot
);
2466 VStream
*Strm
= nullptr;
2469 CREATE_VWAD_FILE(NEWFMT_FNAME_SAVE_HEADER
);
2474 CREATE_VWAD_FILE(NEWFMT_FNAME_SAVE_DESCR
);
2475 *Strm
<< Description
;
2478 // extended data: date value and date string
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
);
2487 // write list of loaded modules
2489 CREATE_VWAD_FILE(NEWFMT_FNAME_SAVE_WADLIST
);
2490 auto wadlist
= FL_GetWadPk3ListSmall();
2491 vint32 wcount
= wadlist
.length();
2493 for (int f
= 0; f
< wcount
; ++f
) *Strm
<< wadlist
[f
];
2496 // write human-readable list of loaded modules
2497 // (it is purely informative)
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());
2506 // write current map name
2507 CREATE_VWAD_FILE(NEWFMT_FNAME_SAVE_CURRMAP
);
2508 VStr
TmpName(CurrentMap
);
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
);
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());
2537 //HACK: if `NumMaps` is 0, we're loading a checkpoint
2539 // save players inventory
2540 VSavedCheckpoint
&cp
= CheckPoint
;
2541 CREATE_VWAD_FILE(NEWFMT_FNAME_SAVE_CPOINT
);
2544 SavedSkill
= cp
.Skill
;
2546 VSavedCheckpoint
&cp
= CheckPoint
;
2548 // write skill level
2549 if (SavedSkill
>= 0 && SavedSkill
< 32) {
2550 CREATE_VWAD_FILE(NEWFMT_FNAME_SAVE_SKILL
);
2551 vint32 sk
= SavedSkill
;
2558 const bool xres
= vmain
->Close();
2561 GCon
->Logf(NAME_Debug
, "finished VWAD archive! xres=%d (%s)", (int)xres
, *ArcStrm
->GetName());
2564 GCon
->Logf(NAME_Error
, "cannot finalize savegame archive");
2571 //==========================================================================
2573 // VSaveSlot::SaveToSlot
2575 //==========================================================================
2576 bool VSaveSlot::SaveToSlot (int Slot
) {
2579 const bool res
= (IsNewFormat() ? SaveToSlotNew(Slot
, savefilename
)
2580 : SaveToSlotOld(Slot
, savefilename
));
2582 SV_SaveFailed(savefilename
, Slot
);
2583 saveFileBase
.clear();
2584 //removeSlotSaveFiles(Slot);
2586 SV_SaveSuccess(savefilename
, Slot
);
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
];
2604 //==========================================================================
2606 // SV_GetSaveStringOld
2608 //==========================================================================
2609 static bool SV_GetSaveStringOld (int Slot
, VStr
&Desc
, VStream
*Strm
) {
2610 bool goodSave
= true;
2613 // skip extended data
2614 if (true/*VStr::Cmp(VersionText, SAVE_VERSION_TEXT) == 0*/) {
2615 if (!SkipExtData(Strm
) || Strm
->IsError()) goodSave
= false;
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
) {
2634 Strm
= vwad
->OpenFile(NEWFMT_FNAME_SAVE_DESCR
);
2635 if (!Strm
) return false;
2637 if (Strm
->IsError()) { VStream::Destroy(Strm
); return false; }
2638 VStream::Destroy(Strm
);
2641 const bool goodSave
= true;
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
);
2649 if (!goodSave
) Desc
= "*"+Desc
;
2650 return /*true*/goodSave
;
2654 //==========================================================================
2658 //==========================================================================
2659 bool SV_GetSaveString (int Slot
, VStr
&Desc
) {
2660 VVWadArchive
*vwad
= nullptr;
2661 VStream
*Strm
= SV_OpenSlotFileReadWithFmt(Slot
, vwad
);
2664 res
= SV_GetSaveStringNew(Slot
, Desc
, vwad
);
2667 res
= SV_GetSaveStringOld(Slot
, Desc
, Strm
);
2668 if (res
&& Strm
->IsError()) res
= false;
2669 VStream::Destroy(Strm
);
2673 if (!res
) Desc
= EMPTYSTRING
;
2679 //==========================================================================
2681 // SV_GetSaveDateString
2683 //==========================================================================
2684 void SV_GetSaveDateString (int Slot
, VStr
&datestr
) {
2685 VVWadArchive
*vwad
= nullptr;
2686 VStream
*Strm
= SV_OpenSlotFileReadWithFmt(Slot
, vwad
);
2690 Strm
= vwad
->OpenFile(NEWFMT_FNAME_SAVE_DATE
);
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
);
2703 res
= !Strm
->IsError();
2705 datestr
= LoadDateStrExtData(Strm
);
2706 if (datestr
.length() == 0) datestr
= "UNKNOWN";
2707 if (res
&& Strm
->IsError()) res
= false;
2709 VStream::Destroy(Strm
);
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
);
2732 Strm
= vwad
->OpenFile(NEWFMT_FNAME_SAVE_DATE
);
2735 memset((void *)&tv
, 0, sizeof(tv
));
2736 *Strm
<< tv
.secs
<< tv
.usecs
<< tv
.secshi
;
2737 res
= !Strm
->IsError();
2738 VStream::Destroy(Strm
);
2744 res
= !Strm
->IsError();
2746 res
= LoadDateTValExtData(Strm
, tv
);
2748 VStream::Destroy(Strm
);
2752 if (!res
) memset((void *)tv
, 0, sizeof(*tv
));
2757 //==========================================================================
2759 // SV_FindAutosaveSlot
2761 // returns 0 on error
2763 //==========================================================================
2764 static int SV_FindAutosaveSlot () {
2765 TTimeVal tv
, besttv
;
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);
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) : ""));
2780 //GCon->Logf("AUTOSAVE: skipped slot #%d [%s] (%d,%d,%d)!", slot, *TimeVal2Str(&tv), tv.secshi, tv.secs, tv.usecs);
2787 //==========================================================================
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 //==========================================================================
2803 //==========================================================================
2804 static void ArchiveNames (VSaveWriterStream
*Saver
) {
2805 if (Saver
->IsNewFormat()) {
2806 if (!Saver
->CreateFileDirect(NEWFMT_FNAME_MAP_NAMES
)) return;
2808 // write offset to the names in the beginning of the file
2809 vint32 NamesOffset
= Saver
->Tell();
2811 *Saver
<< NamesOffset
;
2812 Saver
->Seek(NamesOffset
);
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
);
2823 if (len
) Saver
->Serialise((void *)EName
, len
);
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
);
2835 //==========================================================================
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
);
2847 *Loader
<< NamesOffset
;
2848 TmpOffset
= Loader
->Tell();
2849 Loader
->Seek(NamesOffset
);
2853 *Loader
<< STRM_INDEX(Count
);
2854 Loader
->NameRemap
.setLength(Count
);
2855 for (int i
= 0; i
< Count
; ++i
) {
2856 char EName
[NAME_SIZE
+1];
2859 vassert(len
<= NAME_SIZE
);
2860 if (len
) Loader
->Serialise(EName
, len
);
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 //==========================================================================
2885 //==========================================================================
2886 static void ArchiveThinkers (VSaveWriterStream
*Saver
, bool SavingPlayers
) {
2887 if (!Saver
->IsNewFormat()) {
2888 vint32 Seg
= ASEG_WORLD
;
2892 Saver
->skipPlayers
= !SavingPlayers
;
2895 Saver
->RegisterObject(GLevel
);
2898 if (!Saver
->CreateFileDirect(NEWFMT_FNAME_MAP_WORDINFO
)) return;
2899 vuint8 WorldInfoSaved
= (vuint8
)SavingPlayers
;
2900 *Saver
<< WorldInfoSaved
;
2901 if (WorldInfoSaved
) Saver
->RegisterObject(GGameInfo
->WorldInfo
);
2904 if (!Saver
->CreateFileDirect(NEWFMT_FNAME_MAP_ACTPLYS
)) return;
2907 vassert(MAXPLAYERS
>= 0 && MAXPLAYERS
<= 254);
2908 vuint8 mpl
= MAXPLAYERS
;
2911 for (int i
= 0; i
< MAXPLAYERS
; ++i
) {
2912 vuint8 Active
= (vuint8
)(SavingPlayers
&& GGameInfo
->Players
[i
]);
2914 if (!Active
) continue;
2915 Saver
->RegisterObject(GGameInfo
->Players
[i
]);
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();
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() +
2949 Saver
->Serialise((void *)s
.getCStr(), s
.length());
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
);
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
);
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
);
2978 //==========================================================================
2980 // UnarchiveThinkers
2982 //==========================================================================
2983 static void UnarchiveThinkers (VSaveLoaderStream
*Loader
) {
2984 VObject
*Obj
= nullptr;
2986 if (!Loader
->IsNewFormat()) {
2987 AssertSegment(*Loader
, ASEG_WORLD
);
2991 Loader
->Exports
.Append(GLevel
);
2994 Loader
->OpenFile(NEWFMT_FNAME_MAP_WORDINFO
);
2995 vuint8 WorldInfoSaved
;
2996 *Loader
<< WorldInfoSaved
;
2997 if (WorldInfoSaved
) Loader
->Exports
.Append(GGameInfo
->WorldInfo
);
3000 Loader
->OpenFile(NEWFMT_FNAME_MAP_ACTPLYS
);
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
) {
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
;
3022 bool hasSomethingToRemove
= false;
3024 Loader
->OpenFile(NEWFMT_FNAME_MAP_EXPOBJNS
);
3026 *Loader
<< STRM_INDEX(NumObjects
);
3027 if (NumObjects
< 0) Host_Error("invalid number of VM objects");
3028 for (int i
= 0; i
< NumObjects
; ++i
) {
3032 VClass
*Class
= VClass::FindClass(*CName
);
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);
3045 Sys_Error("I/O ERROR: No such class '%s'", *CName
);
3048 Sys_Error("I/O ERROR: No such class '%s'", *CName
);
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;
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
]);
3081 //GCon->Logf(NAME_Debug, "!!! %d: %s", i, Loader->Exports[i]->GetClass()->GetName());
3082 Loader
->Exports
[i
]->Serialise(*Loader
);
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();
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 //==========================================================================
3128 //==========================================================================
3129 static void ArchiveSounds (VSaveWriterStream
*Saver
) {
3130 if (Saver
->IsNewFormat()) {
3132 if (!Saver
->CreateFileDirect(NEWFMT_FNAME_MAP_SOUNDS
)) return;
3133 GAudio
->SerialiseSounds(*Saver
);
3137 vint32 Seg
= ASEG_SOUNDS
;
3140 GAudio
->SerialiseSounds(*Saver
);
3149 //==========================================================================
3153 //==========================================================================
3154 static void UnarchiveSounds (VSaveLoaderStream
*Loader
) {
3155 if (Loader
->IsNewFormat()) {
3157 Loader
->OpenFile(NEWFMT_FNAME_MAP_SOUNDS
);
3158 GAudio
->SerialiseSounds(*Loader
);
3161 AssertSegment(*Loader
, ASEG_SOUNDS
);
3163 GAudio
->SerialiseSounds(*Loader
);
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
3173 if (xver
!= 0) Sys_Error("invalid sound sequence data");
3177 vint32 CurrentSoundID
;
3179 vuint32 DidDelayOnce
;
3183 *Loader
<< STRM_INDEX(Sequence
)
3184 << STRM_INDEX(OriginId
)
3186 << STRM_INDEX(CurrentSoundID
)
3188 << STRM_INDEX(DidDelayOnce
)
3191 << STRM_INDEX(ModeNum
);
3194 *Loader
<< STRM_INDEX(Offset
);
3197 *Loader
<< STRM_INDEX(Count
);
3198 if (Count
< 0) Sys_Error("invalid sound sequence data");
3199 for (int i
= 0; i
< Count
; ++i
) {
3204 vint32 ParentSeqIdx
;
3206 *Loader
<< STRM_INDEX(ParentSeqIdx
) << STRM_INDEX(ChildSeqIdx
);
3213 //==========================================================================
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()) {
3239 VSavedMap
*Map
= BaseSlot
.FindMap(GLevel
->MapName
);
3241 Map
= new VSavedMap(false);
3242 BaseSlot
.Maps
.Append(Map
);
3243 Map
->Name
= GLevel
->MapName
;
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
;
3258 // write the level timer
3259 *Saver
<< GLevel
->Time
<< GLevel
->TicTime
;
3262 ArchiveThinkers(Saver
, savePlayers
);
3263 ArchiveSounds(Saver
);
3265 // place a termination marker
3269 ArchiveNames(Saver
);
3271 // close the output file
3274 TArrayNC
<vuint8
> &Buf
= InStrm
->GetArray();
3276 // compress map data
3277 Map
->DecompressedSize
= Buf
.length();
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());
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;
3296 if (wasErr
) Host_Error("error compressing savegame data");
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()) {
3309 Host_Error("error creating saved map archive");
3313 VSavedMap
*Map
= BaseSlot
.FindMap(GLevel
->MapName
);
3315 Map
= new VSavedMap(true);
3316 BaseSlot
.Maps
.Append(Map
);
3317 Map
->Name
= GLevel
->MapName
;
3319 Map
->ClearData(true);
3323 Saver
= new VSaveWriterStream(vwad
);
3325 // write the level timer
3326 if (Saver
->CreateFileDirect(NEWFMT_FNAME_MAP_GINFO
)) {
3327 *Saver
<< GLevel
->Time
<< GLevel
->TicTime
;
3332 ArchiveThinkers(Saver
, savePlayers
);
3333 ArchiveSounds(Saver
);
3334 ArchiveNames(Saver
);
3336 // close the output file
3337 const bool ok
= Saver
->Close();
3341 vassert(Map
->IsNewFormat());
3342 TArrayNC
<vuint8
> &Buf
= InStrm
->GetArray();
3344 Map
->Data
.setLength(Buf
.length());
3345 if (Buf
.length()) memcpy(Map
->Data
.ptr(), Buf
.ptr(), Buf
.length());
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
;
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();
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");
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());
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 ===");
3419 //==========================================================================
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!");
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;
3438 // standalone server
3440 Host_Error("Checkpoints aren't supported on dedicated servers!");
3444 // load a base level (spawn thinkers if this is checkpoint save)
3445 if (!hubTeleport
) SV_ResetPlayers();
3447 VBasePlayer::isCheckpointSpawn
= isCheckpoint
;
3449 // setup skill for server here, so checkpoints will start with a right one
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
);
3459 SV_SpawnServer(*MapName
, isCheckpoint
/*spawn thinkers*/);
3461 VBasePlayer::isCheckpointSpawn
= false;
3467 sv_loading
= false; // just in case
3469 VBasePlayer::isCheckpointSpawn
= true;
3470 CL_SetupLocalPlayer();
3472 VBasePlayer::isCheckpointSpawn
= false;
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
;
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
];
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());
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) {
3522 if (dbg_checkpoints
) GCon
->Logf(NAME_Debug
, "QS: #%d:player: %s", f
, *qv
.toString());
3524 qv
.ent
= cp
.EList
[qv
.objidx
-1].ent
;
3526 if (dbg_checkpoints
) GCon
->Logf(NAME_Debug
, "QS: #%d:%s: %s", f
, qv
.ent
->GetClass()->GetName(), *qv
.toString());
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
3537 for (int f
= 0; f
< cp
.EList
.length(); ++f
) {
3538 VSavedCheckpoint::EntityInfo
&ei
= cp
.EList
[f
];
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();
3557 Host_ResetSkipFrames();
3559 VSavedMap
*Map
= BaseSlot
.FindMap(MapName
);
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");
3574 Loader
= new VSaveLoaderStream(vmap
);
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());
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
));
3598 UnarchiveNames(Loader
);
3600 // read the level timer
3601 if (Loader
->IsNewFormat()) {
3602 Loader
->OpenFile(NEWFMT_FNAME_MAP_GINFO
);
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
);
3619 Host_ResetSkipFrames();
3621 // do this here so that clients have loaded info, not initial one
3622 SV_SendServerInfoToClients();
3624 Host_ResetSkipFrames();
3630 //==========================================================================
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
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!)");
3647 GCon
->Logf("AUTOSAVE: checkpoints might be allowed.");
3652 // perform full update, so lightmap cache will be valid
3653 if (!checkpoint
&& GLevel
->Renderer
&& GLevel
->Renderer
->isNeedLightmapCache()) {
3654 GLevel
->Renderer
->FullWorldUpdate(true);
3658 SV_SendBeforeSaveEvent(isAutosave
, 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!)");
3665 SV_SaveMap(true); // true = save player info
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";
3678 bool doPrecalc
= (r_precalc_static_lights_override
>= 0 ? !!r_precalc_static_lights_override
: r_precalc_static_lights
);
3680 enum { doPrecalc
= false };
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
);
3687 GLevel
->cacheFileBase
= saveFileBase
;
3688 GLevel
->cacheFlags
&= ~VLevel::CacheFlag_Ignore
;
3689 VStream
*lmc
= FL_OpenSysFileWrite(ccfname
);
3691 GCon
->Logf("writing lightmap cache to '%s'", *ccfname
);
3692 GLevel
->Renderer
->saveLightmaps(lmc
);
3693 bool err
= lmc
->IsError();
3695 err
= (err
|| lmc
->IsError());
3698 GCon
->Logf(NAME_Warning
, "removed broken lightmap cache '%s'", *ccfname
);
3699 Sys_FileDelete(ccfname
);
3702 GCon
->Logf(NAME_Warning
, "cannot create lightmap cache file '%s'", *ccfname
);
3707 SV_SendAfterSaveEvent(isAutosave
, checkpoint
);
3710 Host_ResetSkipFrames();
3715 //==========================================================================
3719 //==========================================================================
3720 static void SV_LoadGame (int slot
) {
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
;
3735 // load the current map
3736 if (!SV_LoadMap(BaseSlot
.CurrentMap
, true/*allowCheckpoints*/, false/*hubTeleport*/)) {
3738 GLevel
->cacheFileBase
= saveFileBase
;
3739 GLevel
->cacheFlags
&= ~VLevel::CacheFlag_Ignore
;
3740 //GCon->Logf(NAME_Debug, "**********************: <%s>", *GLevel->cacheFileBase);
3742 if (GGameInfo
->NetMode
!= NM_DedicatedServer
) CL_SetupLocalPlayer();
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
];
3751 //GCon->Logf(NAME_Debug, "*** no player #%d", i);
3754 Player
->eventAfterUnarchiveThinkers();
3758 GLevel
->cacheFlags
&= ~VLevel::CacheFlag_Ignore
;
3761 SV_SendLoadedEvent();
3766 //==========================================================================
3770 //==========================================================================
3771 void SV_ClearBaseSlot () {
3772 BaseSlot
.Clear(!dbg_save_in_old_format
.asBool());
3776 //==========================================================================
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
);
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());
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
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
)) {
3856 SV_LoadMap(mapname
, false/*allowCheckpoints*/, true/*hubTeleport*/); // don't allow checkpoints
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();
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
3889 for (auto &&lmp
: fsys_PWadMaps
) {
3890 if (lmp
.mapname
.strEquCI(*mapname
)) {
3895 if (!doSaveGame
) GCon
->Logf(NAME_Warning
, "autosave skipped due to iwad map");
3898 const bool doSaveGame
= false;
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
];
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];
3927 VEntity *sve = plr->ehGetSavedInventory();
3929 GCon->Logf(NAME_Debug, "+++ player #%d saved inventory +++", i);
3930 sve->DebugDumpInventory(true);
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");
3957 //==========================================================================
3959 // CheckIfSaveIsAllowed
3961 //==========================================================================
3962 static bool CheckIfSaveIsAllowed () {
3963 if (svs
.deathmatch
) {
3964 GCon
->Log("Can't save in deathmatch game");
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!");
3973 if (sv
.intermission
) {
3974 GCon
->Log("You can't save while in intermission!");
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
];
3993 if ((plr
->PlayerFlags
&VBasePlayer::PF_Spawned
) == 0) continue;
3994 plr
->eventClientPrint(msg
);
4002 //==========================================================================
4006 //==========================================================================
4007 void SV_AutoSave (bool checkpoint
) {
4008 if (!CheckIfSaveIsAllowed()) return;
4010 int aslot
= SV_FindAutosaveSlot();
4012 BroadcastSaveText("Cannot find autosave slot (this should not happen)!");
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
));
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();
4044 BroadcastSaveText("Cannot find autosave slot (this should not happen)!");
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 //==========================================================================
4065 // Called by the menu task. Description is a 24 byte text string
4067 //==========================================================================
4069 if (Args
.length() != 3) {
4070 GCon
->Log("usage: save slotindex description");
4074 if (!CheckIfSaveIsAllowed()) return;
4076 if (Args
[2].Length() >= 32) {
4077 BroadcastSaveText("Description too long!");
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.");
4113 while (pos
< numstr
.length() && (vuint8
)numstr
[pos
] <= ' ') ++pos
;
4114 if (pos
>= numstr
.length()) return;
4117 if (numstr
[pos
] == '-') {
4120 if (pos
>= numstr
.length()) return;
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
;
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 //==========================================================================
4149 //==========================================================================
4151 if (Args
.length() != 2) return;
4153 if (!CheckIfLoadIsAllowed()) return;
4155 int slot
= VStr::atoi(*Args
[1]);
4157 if (!SV_GetSaveString(slot
, desc
)) {
4158 BroadcastSaveText("Empty slot!");
4161 GCon
->Logf("Loading \"%s\"", *desc
);
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;
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;
4198 if (!SV_GetSaveString(QUICKSAVE_SLOT
, desc
)) {
4199 BroadcastSaveText("Empty quicksave slot");
4202 GCon
->Log("Loading quicksave");
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();
4224 BroadcastSaveText("Cannot find autosave slot (this should not happen)!");
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
);
4263 wadlist
= FL_GetWadPk3List();
4264 GCon
->Log("==== MODS (full) ====");
4265 for (auto &&mname
: wadlist
) GCon
->Logf(" %s", *mname
);
4267 vuint64 hash
= SV_GetModListHash(nullptr);
4268 VStr pfx
= VStr::buf2hex(&hash
, 8);
4269 GCon
->Logf("save prefix: %s", *pfx
);