From 41fc5660e521f0552b73394958e4028f47bc216f Mon Sep 17 00:00:00 2001 From: Christoph Oelckers Date: Sun, 6 Nov 2022 11:46:26 +0100 Subject: [PATCH] - Backend update from GZDoom * IQM enhancements * config storage in Users * moving of savegame filename generation to the backend --- source/common/2d/v_2ddrawer.cpp | 2 +- source/common/console/c_console.cpp | 2 +- source/common/engine/i_specialpaths.h | 1 + source/common/engine/printf.h | 4 +- source/common/menu/savegamemanager.cpp | 58 +++- source/common/menu/savegamemanager.h | 3 + source/common/models/bonecomponents.h | 16 + source/common/models/model.h | 6 +- source/common/models/model_iqm.h | 9 +- source/common/models/models_iqm.cpp | 235 ++++++++------- source/common/objects/dobject.cpp | 1 - .../common/platform/win32/i_specialpaths.cpp | 278 ++++++++++-------- source/common/rendering/v_framebuffer.cpp | 9 +- source/common/rendering/v_video.h | 2 + .../vulkan/system/vk_framebuffer.cpp | 2 +- source/common/scripting/vm/vm.h | 5 +- source/common/scripting/vm/vmframe.cpp | 7 +- source/common/utility/TRS.h | 56 ++++ source/common/utility/engineerrors.cpp | 4 +- source/common/utility/engineerrors.h | 4 +- source/common/utility/vectors.h | 16 + source/core/gamecontrol.cpp | 2 +- source/core/savegamehelp.cpp | 41 --- source/core/savegamehelp.h | 4 - source/core/version.h | 2 + 25 files changed, 454 insertions(+), 315 deletions(-) create mode 100644 source/common/models/bonecomponents.h create mode 100644 source/common/utility/TRS.h diff --git a/source/common/2d/v_2ddrawer.cpp b/source/common/2d/v_2ddrawer.cpp index 1316dfa86..4de396962 100644 --- a/source/common/2d/v_2ddrawer.cpp +++ b/source/common/2d/v_2ddrawer.cpp @@ -138,7 +138,7 @@ IMPLEMENT_CLASS(DShape2D, false, false) static void Shape2D_SetTransform(DShape2D* self, DShape2DTransform *transform) { - self->transform = transform->transform; + self->transform = PARAM_NULLCHECK(transform, transform)->transform; } DEFINE_ACTION_FUNCTION_NATIVE(DShape2D, SetTransform, Shape2D_SetTransform) diff --git a/source/common/console/c_console.cpp b/source/common/console/c_console.cpp index 79eb18505..f6366d7cd 100644 --- a/source/common/console/c_console.cpp +++ b/source/common/console/c_console.cpp @@ -177,7 +177,7 @@ static void setmsgcolor (int index, int color); FILE *Logfile = NULL; -CVARD_NAMED(Int, msglevel, "msg", 0, CVAR_ARCHIVE, "Filters HUD message by importance"); +CVARD_NAMED(Int, msglevel, msg, 0, CVAR_ARCHIVE, "Filters HUD message by importance"); CUSTOM_CVAR (Int, msg0color, CR_UNTRANSLATED, CVAR_ARCHIVE) { diff --git a/source/common/engine/i_specialpaths.h b/source/common/engine/i_specialpaths.h index 4d61bc3d7..01710b672 100644 --- a/source/common/engine/i_specialpaths.h +++ b/source/common/engine/i_specialpaths.h @@ -16,6 +16,7 @@ FString M_GetDemoPath(); FString M_GetNormalizedPath(const char* path); + #ifdef __APPLE__ FString M_GetMacAppSupportPath(const bool create = true); void M_GetMacSearchDirectories(FString& user_docs, FString& user_app_support, FString& local_app_support); diff --git a/source/common/engine/printf.h b/source/common/engine/printf.h index 69d3e7d45..964d7dc85 100644 --- a/source/common/engine/printf.h +++ b/source/common/engine/printf.h @@ -75,8 +75,8 @@ enum }; -void I_Error(const char *fmt, ...) ATTRIBUTE((format(printf,1,2))); -void I_FatalError(const char* fmt, ...) ATTRIBUTE((format(printf, 1, 2))); +[[noreturn]] void I_Error(const char *fmt, ...) ATTRIBUTE((format(printf,1,2))); +[[noreturn]] void I_FatalError(const char* fmt, ...) ATTRIBUTE((format(printf, 1, 2))); // This really could need some cleanup - the main problem is that it'd create // lots of potential for merge conflicts. diff --git a/source/common/menu/savegamemanager.cpp b/source/common/menu/savegamemanager.cpp index f1344efd7..5dc22ec76 100644 --- a/source/common/menu/savegamemanager.cpp +++ b/source/common/menu/savegamemanager.cpp @@ -46,8 +46,11 @@ #include "findfile.h" #include "v_draw.h" #include "savegamemanager.h" +#include "m_argv.h" +#include "i_specialpaths.h" - +CVAR(String, save_dir, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG); +FString SavegameFolder; //============================================================================= // @@ -538,3 +541,56 @@ DEFINE_FIELD_X(SavegameManager, FSavegameManagerBase, WindowSize); DEFINE_FIELD_X(SavegameManager, FSavegameManagerBase, quickSaveSlot); DEFINE_FIELD_X(SavegameManager, FSavegameManagerBase, SaveCommentString); +//============================================================================= +// +// todo: cache this - it never changes once set up. +// +//============================================================================= + +FString G_GetSavegamesFolder() +{ + FString name; + bool usefilter; + + if (const char* const dir = Args->CheckValue("-savedir")) + { + name = dir; + usefilter = false; //-savedir specifies an absolute save directory path. + } + else + { + name = **save_dir ? save_dir : M_GetSavegamesPath(); + usefilter = true; + } + + const size_t len = name.Len(); + if (len > 0) + { + FixPathSeperator(name); + if (name[len - 1] != '/') + name << '/'; + } + + if (usefilter && SavegameFolder.IsNotEmpty()) + name << SavegameFolder << '/'; + + name = NicePath(name); + CreatePath(name); + return name; +} + +//============================================================================= +// +// +// +//============================================================================= + +FString G_BuildSaveName(const char* prefix) +{ + FString name = G_GetSavegamesFolder() + prefix; + DefaultExtension(name, "." SAVEGAME_EXT); // only add an extension if the prefix doesn't have one already. + name = NicePath(name); + name.Substitute("\\", "/"); + return name; +} + diff --git a/source/common/menu/savegamemanager.h b/source/common/menu/savegamemanager.h index 754ddce23..20b466b53 100644 --- a/source/common/menu/savegamemanager.h +++ b/source/common/menu/savegamemanager.h @@ -59,3 +59,6 @@ public: }; +extern FString SavegameFolder; // specifies a subdirectory for the current IWAD. +FString G_GetSavegamesFolder(); +FString G_BuildSaveName(const char* prefix); diff --git a/source/common/models/bonecomponents.h b/source/common/models/bonecomponents.h new file mode 100644 index 000000000..1e65389ea --- /dev/null +++ b/source/common/models/bonecomponents.h @@ -0,0 +1,16 @@ +#pragma once +#include "dobject.h" +#include "tarray.h" +#include "TRS.h" +#include "matrix.h" + + +class DBoneComponents : public DObject +{ + DECLARE_CLASS(DBoneComponents, DObject); +public: + TArray> trscomponents; + TArray> trsmatrix; + + DBoneComponents() = default; +}; diff --git a/source/common/models/model.h b/source/common/models/model.h index 4422207e7..6084e5407 100644 --- a/source/common/models/model.h +++ b/source/common/models/model.h @@ -4,7 +4,9 @@ #include "textureid.h" #include "i_modelvertexbuffer.h" #include "matrix.h" +#include "TRS.h" +class DBoneComponents; class FModelRenderer; class FGameTexture; class IModelVertexBuffer; @@ -74,8 +76,8 @@ public: virtual void BuildVertexBuffer(FModelRenderer *renderer) = 0; virtual void AddSkins(uint8_t *hitlist, const FTextureID* surfaceskinids) = 0; virtual float getAspectFactor(float vscale) { return 1.f; } - virtual const TArray* AttachAnimationData() { return nullptr; }; - virtual const TArray CalculateBones(int frame1, int frame2, double inter, const TArray& animationData) { return {}; }; + virtual const TArray* AttachAnimationData() { return nullptr; }; + virtual const TArray CalculateBones(int frame1, int frame2, double inter, const TArray& animationData, DBoneComponents* bones, int index) { return {}; }; void SetVertexBuffer(int type, IModelVertexBuffer *buffer) { mVBuf[type] = buffer; } IModelVertexBuffer *GetVertexBuffer(int type) const { return mVBuf[type]; } diff --git a/source/common/models/model_iqm.h b/source/common/models/model_iqm.h index d91157c26..cd87a78f3 100644 --- a/source/common/models/model_iqm.h +++ b/source/common/models/model_iqm.h @@ -5,6 +5,9 @@ #include "vectors.h" #include "matrix.h" #include "common/rendering/i_modelvertexbuffer.h" +#include "m_swap.h" + +class DBoneComponents; struct IQMMesh { @@ -112,8 +115,8 @@ public: void RenderFrame(FModelRenderer* renderer, FGameTexture* skin, int frame, int frame2, double inter, int translation, const FTextureID* surfaceskinids, const TArray& boneData, int boneStartPosition) override; void BuildVertexBuffer(FModelRenderer* renderer) override; void AddSkins(uint8_t* hitlist, const FTextureID* surfaceskinids) override; - const TArray* AttachAnimationData() override; - const TArray CalculateBones(int frame1, int frame2, double inter, const TArray& animationData) override; + const TArray* AttachAnimationData() override; + const TArray CalculateBones(int frame1, int frame2, double inter, const TArray& animationData, DBoneComponents* bones, int index) override; private: void LoadGeometry(); @@ -132,7 +135,6 @@ private: TArray Joints; TArray Poses; TArray Anims; - TArray FrameTransforms; TArray Bounds; TArray VertexArrays; uint32_t NumVertices = 0; @@ -141,6 +143,7 @@ private: TArray baseframe; TArray inversebaseframe; + TArray TRSData; }; struct IQMReadErrorException { }; diff --git a/source/common/models/models_iqm.cpp b/source/common/models/models_iqm.cpp index 314a6c600..514fb2833 100644 --- a/source/common/models/models_iqm.cpp +++ b/source/common/models/models_iqm.cpp @@ -5,6 +5,11 @@ #include "texturemanager.h" #include "modelrenderer.h" #include "engineerrors.h" +#include "dobject.h" +#include "bonecomponents.h" + +IMPLEMENT_CLASS(DBoneComponents, false, false); + IQMModel::IQMModel() { @@ -58,8 +63,11 @@ bool IQMModel::Load(const char* path, int lumpnum, const char* buffer, int lengt uint32_t num_extensions = reader.ReadUInt32(); uint32_t ofs_extensions = reader.ReadUInt32(); - if (num_meshes <= 0) - I_FatalError("Invalid model: \"%s%s\", no mesh data is unsupported", path, fileSystem.GetLongName(mLumpNum).GetChars()); + /*if (num_joints <= 0) + { + Printf("Invalid model: \"%s%s\", no joint data is present\n", path, fileSystem.GetLongName(mLumpNum).GetChars()); + return false; + }*/ if (num_text == 0) return false; @@ -168,18 +176,10 @@ bool IQMModel::Load(const char* path, int lumpnum, const char* buffer, int lengt { baseframe[i] = m; inversebaseframe[i] = invm; - } + } } - // Swap YZ axis as we did that with the vertices down in LoadGeometry. - // This is an unfortunate side effect of the coordinate system in the gzdoom model rendering system - float swapYZ[16] = { 0.0f }; - swapYZ[0 + 0 * 4] = 1.0f; - swapYZ[1 + 2 * 4] = 1.0f; - swapYZ[2 + 1 * 4] = 1.0f; - swapYZ[3 + 3 * 4] = 1.0f; - - FrameTransforms.Resize(num_frames * num_poses); + TRSData.Resize(num_frames * num_poses); reader.SeekTo(ofs_frames); for (uint32_t i = 0; i < num_frames; i++) { @@ -204,39 +204,9 @@ bool IQMModel::Load(const char* path, int lumpnum, const char* buffer, int lengt scale.Y = p.ChannelOffset[8]; if (p.ChannelMask & 0x100) scale.Y += reader.ReadUInt16() * p.ChannelScale[8]; scale.Z = p.ChannelOffset[9]; if (p.ChannelMask & 0x200) scale.Z += reader.ReadUInt16() * p.ChannelScale[9]; - VSMatrix m; - m.loadIdentity(); - m.translate(translate.X, translate.Y, translate.Z); - m.multQuaternion(quaternion); - m.scale(scale.X, scale.Y, scale.Z); - - // Concatenate each pose with the inverse base pose to avoid doing this at animation time. - // If the joint has a parent, then it needs to be pre-concatenated with its parent's base pose. - // Thus it all negates at animation time like so: - // (parentPose * parentInverseBasePose) * (parentBasePose * childPose * childInverseBasePose) => - // parentPose * (parentInverseBasePose * parentBasePose) * childPose * childInverseBasePose => - // parentPose * childPose * childInverseBasePose - VSMatrix& result = FrameTransforms[i * num_poses + j]; - if (p.Parent >= 0) - { - result = baseframe[p.Parent]; - result.multMatrix(m); - result.multMatrix(inversebaseframe[j]); - } - else - { - result = m; - result.multMatrix(inversebaseframe[j]); - } - } - - for (uint32_t j = 0; j < num_poses; j++) - { - VSMatrix m; - m.loadMatrix(swapYZ); - m.multMatrix(FrameTransforms[i * num_poses + j]); - m.multMatrix(swapYZ); - FrameTransforms[i * num_poses + j] = m; + TRSData[i * num_poses + j].translation = translate; + TRSData[i * num_poses + j].rotation = quaternion; + TRSData[i * num_poses + j].scaling = scale; } } @@ -244,7 +214,7 @@ bool IQMModel::Load(const char* path, int lumpnum, const char* buffer, int lengt if (num_frames <= 0) { num_frames = 1; - FrameTransforms.Resize(num_joints); + TRSData.Resize(num_joints); for (uint32_t j = 0; j < num_joints; j++) { @@ -265,33 +235,9 @@ bool IQMModel::Load(const char* path, int lumpnum, const char* buffer, int lengt scale.Y = Joints[j].Scale.Y; scale.Z = Joints[j].Scale.Z; - VSMatrix m; - m.loadIdentity(); - m.translate(translate.X, translate.Y, translate.Z); - m.multQuaternion(quaternion); - m.scale(scale.X, scale.Y, scale.Z); - - VSMatrix& result = FrameTransforms[j]; - if (Joints[j].Parent >= 0) - { - result = baseframe[Joints[j].Parent]; - result.multMatrix(m); - result.multMatrix(inversebaseframe[j]); - } - else - { - result = m; - result.multMatrix(inversebaseframe[j]); - } - } - - for (uint32_t j = 0; j < num_joints; j++) - { - VSMatrix m; - m.loadMatrix(swapYZ); - m.multMatrix(FrameTransforms[j]); - m.multMatrix(swapYZ); - FrameTransforms[j] = m; + TRSData[j].translation = translate; + TRSData[j].rotation = quaternion; + TRSData[j].scaling = scale; } } @@ -511,24 +457,25 @@ void IQMModel::RenderFrame(FModelRenderer* renderer, FGameTexture* skin, int fra { meshSkin = TexMan.GetGameTexture(surfaceskinids[i], true); } - else if (!Meshes[i].Skin.isValid()) + else if (Meshes[i].Skin.isValid()) + { + meshSkin = TexMan.GetGameTexture(Meshes[i].Skin, true); + } + else { continue; } - else - { - meshSkin = TexMan.GetGameTexture(Meshes[i].Skin, true); - } - if (!meshSkin) continue; } - if (meshSkin != lastSkin) + if (meshSkin->isValid()) { - renderer->SetMaterial(meshSkin, false, translation); - lastSkin = meshSkin; + if (meshSkin != lastSkin) + { + renderer->SetMaterial(meshSkin, false, translation); + lastSkin = meshSkin; + } + renderer->DrawElements(Meshes[i].NumTriangles * 3, Meshes[i].FirstTriangle * 3 * sizeof(unsigned int)); } - - renderer->DrawElements(Meshes[i].NumTriangles * 3, Meshes[i].FirstTriangle * 3 * sizeof(unsigned int)); } } @@ -562,49 +509,99 @@ void IQMModel::AddSkins(uint8_t* hitlist, const FTextureID* surfaceskinids) } } -const TArray* IQMModel::AttachAnimationData() +const TArray* IQMModel::AttachAnimationData() { - return &FrameTransforms; + return &TRSData; } -const TArray IQMModel::CalculateBones(int frame1, int frame2, double inter, const TArray& animationData) +const TArray IQMModel::CalculateBones(int frame1, int frame2, double inter, const TArray& animationData, DBoneComponents* boneComponentData, int index) { - const TArray& animationFrames = &animationData ? animationData : FrameTransforms; - - int numbones = Joints.Size(); - - frame1 = clamp(frame1, 0, ((int)animationFrames.Size() - 1) / numbones); - frame2 = clamp(frame2, 0, ((int)animationFrames.Size() - 1) / numbones); - - int offset1 = frame1 * numbones; - int offset2 = frame2 * numbones; - float t = (float)inter; - float invt = 1.0f - t; - - TArray bones(numbones, true); - for (int i = 0; i < numbones; i++) + const TArray& animationFrames = &animationData ? animationData : TRSData; + if (Joints.Size() > 0) { - const float* from = animationFrames[offset1 + i].get(); - const float* to = animationFrames[offset2 + i].get(); + int numbones = Joints.Size(); - // Interpolate bone between the two frames - float bone[16]; - for (int j = 0; j < 16; j++) + if (boneComponentData->trscomponents[index].Size() != numbones) + boneComponentData->trscomponents[index].Resize(numbones); + if (boneComponentData->trsmatrix[index].Size() != numbones) + boneComponentData->trsmatrix[index].Resize(numbones); + + frame1 = clamp(frame1, 0, ((int)animationFrames.Size() - 1) / numbones); + frame2 = clamp(frame2, 0, ((int)animationFrames.Size() - 1) / numbones); + + int offset1 = frame1 * numbones; + int offset2 = frame2 * numbones; + float t = (float)inter; + float invt = 1.0f - t; + + float swapYZ[16] = { 0.0f }; + swapYZ[0 + 0 * 4] = 1.0f; + swapYZ[1 + 2 * 4] = 1.0f; + swapYZ[2 + 1 * 4] = 1.0f; + swapYZ[3 + 3 * 4] = 1.0f; + + TArray bones(numbones, true); + TArray modifiedBone(numbones, true); + for (int i = 0; i < numbones; i++) { - bone[j] = from[j] * invt + to[j] * t; + TRS bone; + TRS from = animationFrames[offset1 + i]; + TRS to = animationFrames[offset2 + i]; + + bone.translation = from.translation * invt + to.translation * t; + bone.rotation = from.rotation * invt; + if ((bone.rotation | to.rotation * t) < 0) + { + bone.rotation.X *= -1; bone.rotation.Y *= -1; bone.rotation.Z *= -1; bone.rotation.W *= -1; + } + bone.rotation += to.rotation * t; + bone.rotation.MakeUnit(); + bone.scaling = from.scaling * invt + to.scaling * t; + + if (Joints[i].Parent >= 0 && modifiedBone[Joints[i].Parent]) + { + boneComponentData->trscomponents[index][i] = bone; + modifiedBone[i] = true; + } + else if (boneComponentData->trscomponents[index][i].Equals(bone)) + { + bones[i] = boneComponentData->trsmatrix[index][i]; + modifiedBone[i] = false; + continue; + } + else + { + boneComponentData->trscomponents[index][i] = bone; + modifiedBone[i] = true; + } + + VSMatrix m; + m.loadIdentity(); + m.translate(bone.translation.X, bone.translation.Y, bone.translation.Z); + m.multQuaternion(bone.rotation); + m.scale(bone.scaling.X, bone.scaling.Y, bone.scaling.Z); + + VSMatrix& result = bones[i]; + if (Joints[i].Parent >= 0) + { + result = bones[Joints[i].Parent]; + result.multMatrix(swapYZ); + result.multMatrix(baseframe[Joints[i].Parent]); + result.multMatrix(m); + result.multMatrix(inversebaseframe[i]); + } + else + { + result.loadMatrix(swapYZ); + result.multMatrix(m); + result.multMatrix(inversebaseframe[i]); + } + result.multMatrix(swapYZ); } - // Apply parent bone - if (Joints[i].Parent >= 0) - { - bones[i] = bones[Joints[i].Parent]; - bones[i].multMatrix(bone); - } - else - { - bones[i].loadMatrix(bone); - } + boneComponentData->trsmatrix[index] = bones; + + return bones; } - - return bones; + return {}; } \ No newline at end of file diff --git a/source/common/objects/dobject.cpp b/source/common/objects/dobject.cpp index dfc68fcd5..f82f50580 100644 --- a/source/common/objects/dobject.cpp +++ b/source/common/objects/dobject.cpp @@ -515,5 +515,4 @@ void *DObject::ScriptVar(FName field, PType *type) } // This is only for internal use so I_Error is fine. I_Error("Variable %s not found in %s\n", field.GetChars(), cls->TypeName.GetChars()); - return nullptr; } diff --git a/source/common/platform/win32/i_specialpaths.cpp b/source/common/platform/win32/i_specialpaths.cpp index 777d4fc63..0b29a28c5 100644 --- a/source/common/platform/win32/i_specialpaths.cpp +++ b/source/common/platform/win32/i_specialpaths.cpp @@ -37,21 +37,19 @@ #include #include #include +#include #include "i_specialpaths.h" #include "printf.h" #include "cmdlib.h" #include "findfile.h" #include "version.h" // for GAMENAME +#include "gstrings.h" +#include "i_mainwindow.h" +#include "engineerrors.h" -// Vanilla MinGW does not have folder ids -#if defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR) -static const GUID FOLDERID_LocalAppData = { 0xf1b32785, 0x6fba, 0x4fcf, 0x9d, 0x55, 0x7b, 0x8e, 0x7f, 0x15, 0x70, 0x91 }; -static const GUID FOLDERID_RoamingAppData = { 0x3eb685db, 0x65f9, 0x4cf6, 0xa0, 0x3a, 0xe3, 0xef, 0x65, 0x72, 0x9f, 0x3d }; -static const GUID FOLDERID_SavedGames = { 0x4c5c32ff, 0xbb9d, 0x43b0, 0xb5, 0xb4, 0x2d, 0x72, 0xe5, 0x4e, 0xaa, 0xa4 }; -static const GUID FOLDERID_Documents = { 0xfdd39ad0, 0x238f, 0x46af, 0xad, 0xb4, 0x6c, 0x85, 0x48, 0x03, 0x69, 0xc7 }; -static const GUID FOLDERID_Pictures = { 0x33e28130, 0x4e1e, 0x4676, 0x83, 0x5a, 0x98, 0x39, 0x5c, 0x3b, 0xc3, 0xbb }; -#endif + +static int isportable = -1; //=========================================================================== // @@ -62,18 +60,18 @@ static const GUID FOLDERID_Pictures = { 0x33e28130, 0x4e1e, 0x4676, 0x83, 0x5a, // //=========================================================================== -bool UseKnownFolders() +bool IsPortable() { // Cache this value so the semantics don't change during a single run // of the program. (e.g. Somebody could add write access while the // program is running.) - static int iswritable = -1; HANDLE file; - if (iswritable >= 0) + if (isportable >= 0) { - return !iswritable; + return !!isportable; } + // Consider 'Program Files' read only without actually checking. bool found = false; for (auto p : { L"ProgramFiles", L"ProgramFiles(x86)" }) @@ -85,50 +83,51 @@ bool UseKnownFolders() FixPathSeperator(envpath); if (progdir.MakeLower().IndexOf(envpath.MakeLower()) == 0) { - found = true; - break; + isportable = false; + return false; } } } - if (!found) + // A portable INI means that this storage location should also be portable if the file can be written to. + FStringf path("%s" GAMENAME "_portable.ini", progdir.GetChars()); + if (FileExists(path)) { - std::wstring testpath = progdir.WideString() + L"writest"; - file = CreateFile(testpath.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, - CREATE_ALWAYS, - FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, NULL); + file = CreateFile(path.WideString().c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (file != INVALID_HANDLE_VALUE) { CloseHandle(file); - if (!batchrun) Printf("Using program directory for storage\n"); - iswritable = true; - return false; + if (!batchrun) Printf("Using portable configuration\n"); + isportable = true; + return true; } } - if (!batchrun) Printf("Using known folders for storage\n"); - iswritable = false; - return true; + + isportable = false; + return false; } //=========================================================================== // // GetKnownFolder // -// Returns the known_folder if SHGetKnownFolderPath is available, otherwise -// returns the shell_folder using SHGetFolderPath. +// Returns the known_folder from SHGetKnownFolderPath // //=========================================================================== -bool GetKnownFolder(int shell_folder, REFKNOWNFOLDERID known_folder, bool create, FString &path) +FString GetKnownFolder(int shell_folder, REFKNOWNFOLDERID known_folder, bool create) { PWSTR wpath; if (FAILED(SHGetKnownFolderPath(known_folder, create ? KF_FLAG_CREATE : 0, NULL, &wpath))) { - return false; + // This should never be triggered unless the OS was compromised + I_FatalError("Unable to retrieve known folder."); } - path = wpath; + FString path = FString(wpath); + FixPathSeperator(path); CoTaskMemFree(wpath); - return true; + return path; } //=========================================================================== @@ -141,14 +140,9 @@ bool GetKnownFolder(int shell_folder, REFKNOWNFOLDERID known_folder, bool create FString M_GetAppDataPath(bool create) { - FString path; + FString path = GetKnownFolder(CSIDL_LOCAL_APPDATA, FOLDERID_LocalAppData, create); - if (!GetKnownFolder(CSIDL_LOCAL_APPDATA, FOLDERID_LocalAppData, create, path)) - { // Failed (e.g. On Win9x): use program directory - path = progdir; - } path += "/" GAMENAMELOWERCASE; - path.Substitute("//", "/"); // needed because progdir ends with a slash. if (create) { CreatePath(path); @@ -166,16 +160,11 @@ FString M_GetAppDataPath(bool create) FString M_GetCachePath(bool create) { - FString path; + FString path = GetKnownFolder(CSIDL_LOCAL_APPDATA, FOLDERID_LocalAppData, create); - if (!GetKnownFolder(CSIDL_LOCAL_APPDATA, FOLDERID_LocalAppData, create, path)) - { // Failed (e.g. On Win9x): use program directory - path = progdir; - } // Don't use GAME_DIR and such so that ZDoom and its child ports can // share the node cache. path += "/zdoom/cache"; - path.Substitute("//", "/"); // needed because progdir ends with a slash. if (create) { CreatePath(path); @@ -196,6 +185,84 @@ FString M_GetAutoexecPath() return "$PROGDIR/autoexec.cfg"; } +//=========================================================================== +// +// M_GetOldConfigPath +// +// Check if we have a config in a place that's no longer used. +// +//=========================================================================== + +FString M_GetOldConfigPath(int& type) +{ + FString path; + HRESULT hr; + + // construct "$PROGDIR/-$USER.ini" + WCHAR uname[UNLEN + 1]; + DWORD unamelen = UNLEN; + + path = progdir; + hr = GetUserNameW(uname, &unamelen); + if (SUCCEEDED(hr) && uname[0] != 0) + { + // Is it valid for a user name to have slashes? + // Check for them and substitute just in case. + auto probe = uname; + while (*probe != 0) + { + if (*probe == '\\' || *probe == '/') + *probe = '_'; + ++probe; + } + path << GAMENAMELOWERCASE "-" << FString(uname) << ".ini"; + type = 0; + if (FileExists(path)) + return path; + } + + // Check in app data where this was previously stored. + // We actually prefer to store the config in a more visible place so this is no longer used. + path = GetKnownFolder(CSIDL_APPDATA, FOLDERID_RoamingAppData, true); + path += "/" GAME_DIR "/" GAMENAMELOWERCASE ".ini"; + type = 1; + if (FileExists(path)) + return path; + + return ""; +} + +//=========================================================================== +// +// M_MigrateOldConfig +// +// Ask the user what to do with their old config. +// +//=========================================================================== + +int M_MigrateOldConfig() +{ + int selection = IDCANCEL; + auto globalstr = L"Move to Users/ folder"; + auto portablestr = L"Convert to portable installation"; + auto cancelstr = L"Cancel"; + auto titlestr = L"Migrate existing configuration"; + auto infostr = L"" GAMENAME " found a user specific config in the game folder"; + const TASKDIALOG_BUTTON buttons[] = { {IDYES, globalstr}, {IDNO, portablestr}, {IDCANCEL, cancelstr} }; + TASKDIALOGCONFIG taskDialogConfig = {}; + taskDialogConfig.cbSize = sizeof(TASKDIALOGCONFIG); + taskDialogConfig.pszMainIcon = TD_WARNING_ICON; + taskDialogConfig.pButtons = buttons; + taskDialogConfig.cButtons = countof(buttons); + taskDialogConfig.pszWindowTitle = titlestr; + taskDialogConfig.pszContent = infostr; + taskDialogConfig.hwndParent = mainwindow.GetHandle(); + taskDialogConfig.dwFlags = TDF_USE_COMMAND_LINKS; + TaskDialogIndirect(&taskDialogConfig, &selection, NULL, NULL); + if (selection == IDYES || selection == IDNO) return selection; + throw CExitEvent(3); +} + //=========================================================================== // // M_GetConfigPath Windows @@ -208,51 +275,43 @@ FString M_GetAutoexecPath() FString M_GetConfigPath(bool for_reading) { - FString path; - HRESULT hr; - - path.Format("%s" GAMENAMELOWERCASE "_portable.ini", progdir.GetChars()); - if (FileExists(path)) + if (IsPortable()) { - return path; + return FStringf("%s" GAMENAMELOWERCASE "_portable.ini", progdir.GetChars()); } - path = ""; // Construct a user-specific config name - if (UseKnownFolders() && GetKnownFolder(CSIDL_APPDATA, FOLDERID_RoamingAppData, true, path)) + FString path = GetKnownFolder(CSIDL_APPDATA, FOLDERID_Documents, true); + path += "/My Games/" GAME_DIR; + CreatePath(path); + path += "/" GAMENAMELOWERCASE ".ini"; + if (!for_reading || FileExists(path)) + return path; + + // No config was found in the accepted locations. + // Look in previously valid places to see if we have something we can migrate + + int type = 0; + FString oldpath = M_GetOldConfigPath(type); + if (!oldpath.IsEmpty()) { - path += "/" GAME_DIR; - CreatePath(path); - path += "/" GAMENAMELOWERCASE ".ini"; - } - else - { // construct "$PROGDIR/-$USER.ini" - WCHAR uname[UNLEN+1]; - DWORD unamelen = UNLEN; - - path = progdir; - hr = GetUserNameW(uname, &unamelen); - if (SUCCEEDED(hr) && uname[0] != 0) + if (type == 0) { - // Is it valid for a user name to have slashes? - // Check for them and substitute just in case. - auto probe = uname; - while (*probe != 0) + // If we find a local per-user config, ask the user what to do with it. + int action = M_MigrateOldConfig(); + if (action == IDNO) { - if (*probe == '\\' || *probe == '/') - *probe = '_'; - ++probe; + path.Format("%s" GAMENAMELOWERCASE "_portable.ini", progdir.GetChars()); + isportable = true; } - path << GAMENAMELOWERCASE "-" << FString(uname) << ".ini"; - } - else - { // Couldn't get user name, so just use base version. - path += GAMENAMELOWERCASE ".ini"; } + bool res = MoveFileExW(WideString(oldpath).c_str(), WideString(path).c_str(), MOVEFILE_COPY_ALLOWED); + if (res) return path; + else return oldpath; // if we cannot move, just use the config where it was. It won't be written back, though and never be used again if a new one gets saved. } - // If we are reading the config file, check if it exists. If not, fallback - // to base version. + // Fall back to the global template if nothing was found. + // If we are reading the config file, check if it exists. If not, fallback to base version. if (for_reading) { if (!FileExists(path)) @@ -273,30 +332,25 @@ FString M_GetConfigPath(bool for_reading) // //=========================================================================== -// I'm not sure when FOLDERID_Screenshots was added, but it was probably -// for Windows 8, since it's not in the v7.0 Windows SDK. -static const GUID MyFOLDERID_Screenshots = { 0xb7bede81, 0xdf94, 0x4682, 0xa7, 0xd8, 0x57, 0xa5, 0x26, 0x20, 0xb8, 0x6f }; - FString M_GetScreenshotsPath() { FString path; - if (!UseKnownFolders()) + if (IsPortable()) { path << progdir << "Screenshots/"; } - else if (GetKnownFolder(-1, MyFOLDERID_Screenshots, true, path)) + else if (IsWindows8OrGreater()) { + path = GetKnownFolder(-1, FOLDERID_Screenshots, true); + path << "/" GAMENAME "/"; } - else if (GetKnownFolder(CSIDL_MYPICTURES, FOLDERID_Pictures, true, path)) + else { + path = GetKnownFolder(CSIDL_MYPICTURES, FOLDERID_Pictures, true); path << "/Screenshots/" GAMENAME "/"; } - else - { - path << progdir << "/Screenshots/"; - } CreatePath(path); return path; } @@ -313,27 +367,16 @@ FString M_GetSavegamesPath() { FString path; - if (!UseKnownFolders()) + if (IsPortable()) { path << progdir << "Save/"; } // Try standard Saved Games folder - else if (GetKnownFolder(-1, FOLDERID_SavedGames, true, path)) - { - path << "/" GAMENAME "/"; - } - // Try defacto My Documents/My Games folder - else if (GetKnownFolder(CSIDL_PERSONAL, FOLDERID_Documents, true, path)) - { - // I assume since this isn't a standard folder, it doesn't have - // a localized name either. - path << "/My Games/" GAMENAME "/"; - } else { - path << progdir << "Save/"; + path = GetKnownFolder(-1, FOLDERID_SavedGames, true); + path << "/" GAMENAME "/"; } - return path; } @@ -349,29 +392,18 @@ FString M_GetDocumentsPath() { FString path; - // A portable INI means that this storage location should also be portable. - path.Format("%s" GAMENAME "_portable.ini", progdir.GetChars()); - if (FileExists(path)) - { - return progdir; - } - - if (!UseKnownFolders()) + if (IsPortable()) { return progdir; } // Try defacto My Documents/My Games folder - else if (GetKnownFolder(CSIDL_PERSONAL, FOLDERID_Documents, true, path)) + else { - // I assume since this isn't a standard folder, it doesn't have - // a localized name either. + // I assume since this isn't a standard folder, it doesn't have a localized name either. + path = GetKnownFolder(CSIDL_PERSONAL, FOLDERID_Documents, true); path << "/My Games/" GAMENAME "/"; CreatePath(path); } - else - { - path = progdir; - } return path; } @@ -388,23 +420,17 @@ FString M_GetDemoPath() FString path; // A portable INI means that this storage location should also be portable. - FStringf inipath("%s" GAMENAME "_portable.ini", progdir.GetChars()); - if (FileExists(inipath) || !UseKnownFolders()) + if (IsPortable()) { path << progdir << "Demos/"; } else // Try defacto My Documents/My Games folder - if (GetKnownFolder(CSIDL_PERSONAL, FOLDERID_Documents, true, path)) { - // I assume since this isn't a standard folder, it doesn't have - // a localized name either. + // I assume since this isn't a standard folder, it doesn't have a localized name either. + path = GetKnownFolder(CSIDL_PERSONAL, FOLDERID_Documents, true); path << "/My Games/" GAMENAME "/"; } - else - { - path << progdir << "Demos/"; - } return path; } diff --git a/source/common/rendering/v_framebuffer.cpp b/source/common/rendering/v_framebuffer.cpp index c0ebb4c25..271348bc1 100644 --- a/source/common/rendering/v_framebuffer.cpp +++ b/source/common/rendering/v_framebuffer.cpp @@ -170,6 +170,8 @@ void DFrameBuffer::SetViewportRects(IntRect *bounds) mSceneViewport = *bounds; mScreenViewport = *bounds; mOutputLetterbox = *bounds; + mGameScreenWidth = mScreenViewport.width; + mGameScreenHeight = mScreenViewport.height; return; } @@ -216,6 +218,9 @@ void DFrameBuffer::SetViewportRects(IntRect *bounds) mSceneViewport.width = (int)round(mSceneViewport.width * scaleX); mSceneViewport.height = (int)round(mSceneViewport.height * scaleY); } + + mGameScreenWidth = GetWidth(); + mGameScreenHeight = GetHeight(); } //=========================================================================== @@ -226,12 +231,12 @@ void DFrameBuffer::SetViewportRects(IntRect *bounds) int DFrameBuffer::ScreenToWindowX(int x) { - return mScreenViewport.left + (int)round(x * mScreenViewport.width / (float)GetWidth()); + return mScreenViewport.left + (int)round(x * mScreenViewport.width / (float)mGameScreenWidth); } int DFrameBuffer::ScreenToWindowY(int y) { - return mScreenViewport.top + mScreenViewport.height - (int)round(y * mScreenViewport.height / (float)GetHeight()); + return mScreenViewport.top + mScreenViewport.height - (int)round(y * mScreenViewport.height / (float)mGameScreenHeight); } void DFrameBuffer::ScaleCoordsFromWindow(int16_t &x, int16_t &y) diff --git a/source/common/rendering/v_video.h b/source/common/rendering/v_video.h index 3be892efe..afba5784a 100644 --- a/source/common/rendering/v_video.h +++ b/source/common/rendering/v_video.h @@ -147,6 +147,8 @@ public: BoneBuffer* mBones = nullptr; // Model bones IShadowMap mShadowMap; + int mGameScreenWidth = 0; + int mGameScreenHeight = 0; IntRect mScreenViewport; IntRect mSceneViewport; IntRect mOutputLetterbox; diff --git a/source/common/rendering/vulkan/system/vk_framebuffer.cpp b/source/common/rendering/vulkan/system/vk_framebuffer.cpp index b5925ad53..9ca9d8289 100644 --- a/source/common/rendering/vulkan/system/vk_framebuffer.cpp +++ b/source/common/rendering/vulkan/system/vk_framebuffer.cpp @@ -206,7 +206,7 @@ void VulkanFrameBuffer::RenderTextureView(FCanvasTexture* tex, std::functionEndRenderPass(); VkImageTransition() - .AddImage(image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, true) + .AddImage(image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, false) .Execute(mCommands->GetDrawCommands()); mRenderState->SetRenderTarget(image, depthStencil->View.get(), image->Image->width, image->Image->height, VK_FORMAT_R8G8B8A8_UNORM, VK_SAMPLE_COUNT_1_BIT); diff --git a/source/common/scripting/vm/vm.h b/source/common/scripting/vm/vm.h index 980f5b1d8..1ba1a3c34 100644 --- a/source/common/scripting/vm/vm.h +++ b/source/common/scripting/vm/vm.h @@ -110,8 +110,8 @@ public: }; // This must be a separate function because the VC compiler would otherwise allocate memory on the stack for every separate instance of the exception object that may get thrown. -void ThrowAbortException(EVMAbortException reason, const char *moreinfo, ...); -void ThrowAbortException(VMScriptFunction *sfunc, VMOP *line, EVMAbortException reason, const char *moreinfo, ...); +[[noreturn]] void ThrowAbortException(EVMAbortException reason, const char *moreinfo, ...); +[[noreturn]] void ThrowAbortException(VMScriptFunction *sfunc, VMOP *line, EVMAbortException reason, const char *moreinfo, ...); void ClearGlobalVMStack(); @@ -496,6 +496,7 @@ inline int VMCallAction(VMFunction *func, VMValue *params, int numparams, VMRetu // Use these to collect the parameters in a native function. // variable name at position

+[[noreturn]] void NullParam(const char *varname); #ifndef NDEBUG diff --git a/source/common/scripting/vm/vmframe.cpp b/source/common/scripting/vm/vmframe.cpp index 8a4b87458..69b47dfd5 100644 --- a/source/common/scripting/vm/vmframe.cpp +++ b/source/common/scripting/vm/vmframe.cpp @@ -688,14 +688,14 @@ void CVMAbortException::MaybePrintMessage() } -void ThrowAbortException(EVMAbortException reason, const char *moreinfo, ...) +[[noreturn]] void ThrowAbortException(EVMAbortException reason, const char *moreinfo, ...) { va_list ap; va_start(ap, moreinfo); throw CVMAbortException(reason, moreinfo, ap); } -void ThrowAbortException(VMScriptFunction *sfunc, VMOP *line, EVMAbortException reason, const char *moreinfo, ...) +[[noreturn]] void ThrowAbortException(VMScriptFunction *sfunc, VMOP *line, EVMAbortException reason, const char *moreinfo, ...) { va_list ap; va_start(ap, moreinfo); @@ -711,10 +711,9 @@ DEFINE_ACTION_FUNCTION(DObject, ThrowAbortException) PARAM_PROLOGUE; FString s = FStringFormat(VM_ARGS_NAMES); ThrowAbortException(X_OTHER, s.GetChars()); - return 0; } -void NullParam(const char *varname) +[[noreturn]] void NullParam(const char *varname) { ThrowAbortException(X_READ_NIL, "In function parameter %s", varname); } diff --git a/source/common/utility/TRS.h b/source/common/utility/TRS.h new file mode 100644 index 000000000..c39a870ad --- /dev/null +++ b/source/common/utility/TRS.h @@ -0,0 +1,56 @@ +/* +** TRS +** +**--------------------------------------------------------------------------- +** Copyright 2022 Andrew Clarke +** All rights reserved. +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** 1. Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** 2. Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** 3. The name of the author may not be used to endorse or promote products +** derived from this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**--------------------------------------------------------------------------- +** +*/ + +#pragma once +#include "vectors.h" + +class TRS +{ +public: + FVector3 translation; + FVector4 rotation; + FVector3 scaling; + + TRS() + { + translation = FVector3(0,0,0); + rotation = FVector4(0,0,0,1); + scaling = FVector3(0,0,0); + } + + bool Equals(TRS& compare) + { + return compare.translation == this->translation && compare.rotation == this->rotation && compare.scaling == this->scaling; + } +}; + diff --git a/source/common/utility/engineerrors.cpp b/source/common/utility/engineerrors.cpp index ccc459f72..1e2e2e82e 100644 --- a/source/common/utility/engineerrors.cpp +++ b/source/common/utility/engineerrors.cpp @@ -63,7 +63,7 @@ void I_DebugPrint(const char *cp) // //========================================================================== -void I_Error(const char *error, ...) +[[noreturn]] void I_Error(const char *error, ...) { va_list argptr; char errortext[MAX_ERRORTEXT]; @@ -85,7 +85,7 @@ void I_Error(const char *error, ...) //========================================================================== extern FILE *Logfile; -void I_FatalError(const char *error, ...) +[[noreturn]] void I_FatalError(const char *error, ...) { static bool alreadyThrown = false; gameisdead = true; diff --git a/source/common/utility/engineerrors.h b/source/common/utility/engineerrors.h index 36a755ade..18fe849bc 100644 --- a/source/common/utility/engineerrors.h +++ b/source/common/utility/engineerrors.h @@ -110,7 +110,7 @@ public: }; void I_ShowFatalError(const char *message); -void I_Error (const char *error, ...) GCCPRINTF(1,2); -void I_FatalError (const char *error, ...) GCCPRINTF(1,2); +[[noreturn]] void I_Error (const char *error, ...) GCCPRINTF(1,2); +[[noreturn]] void I_FatalError (const char *error, ...) GCCPRINTF(1,2); #endif //__ERRORS_H__ diff --git a/source/common/utility/vectors.h b/source/common/utility/vectors.h index b4c4152a8..019eb9db1 100644 --- a/source/common/utility/vectors.h +++ b/source/common/utility/vectors.h @@ -846,6 +846,22 @@ struct TVector4 return TVector4(v.X * scalar, v.Y * scalar, v.Z * scalar, v.W * scalar); } + // Multiply as Quaternion + TVector4& operator*= (const TVector4& v) + { + *this = *this * v; + return *this; + } + + friend TVector4 operator* (const TVector4& v1, const TVector4& v2) + { + return TVector4(v2.W * v1.X + v2.X * v1.W + v2.Y * v1.Z - v1.Z * v1.Y, + v2.W * v1.Y + v2.Y * v1.W + v2.Z * v1.X - v2.X * v1.Z, + v2.W * v1.Z + v2.Z * v1.W + v2.X * v1.Y - v2.Y * v1.X, + v2.W * v1.W - v2.X * v1.X - v2.Y * v1.Y - v2.Z * v1.Z + ); + } + // Scalar division TVector4 &operator/= (vec_t scalar) { diff --git a/source/core/gamecontrol.cpp b/source/core/gamecontrol.cpp index 5d0611b2a..bc21b3b9b 100644 --- a/source/core/gamecontrol.cpp +++ b/source/core/gamecontrol.cpp @@ -842,7 +842,7 @@ static TArray SetupGame() LumpFilter = usedgroups.Last().FileInfo.name; LumpFilter.StripChars(".:/\\<>?\"*| \t\r\n"); } - + SavegameFolder = LumpFilter; currentGame = LumpFilter; currentGame.Truncate(currentGame.IndexOf(".")); PClass::StaticInit(); diff --git a/source/core/savegamehelp.cpp b/source/core/savegamehelp.cpp index 5ec3926bf..38ffdebe2 100644 --- a/source/core/savegamehelp.cpp +++ b/source/core/savegamehelp.cpp @@ -393,47 +393,6 @@ int G_ValidateSavegame(FileReader &fr, FString *savetitle, bool formenu) return 0; } -//============================================================================= -// -// -// -//============================================================================= - -FString G_BuildSaveName (const char *prefix) -{ - FString name; - bool usefilter; - - if (const char *const dir = Args->CheckValue("-savedir")) - { - name = dir; - usefilter = false; - } - else - { - name = **cl_savedir ? cl_savedir : M_GetSavegamesPath(); - usefilter = true; - } - - const size_t len = name.Len(); - if (len > 0) - { - name.Substitute("\\", "/"); - if (name[len - 1] != '/') - name << '/'; - } - - if (usefilter) - name << LumpFilter << '/'; - - CreatePath(name); - - name << prefix; - if (!strchr(prefix, '.')) name << SAVEGAME_EXT; // only add an extension if the prefix doesn't have one already. - name = NicePath(name); - name.Substitute("\\", "/"); - return name; -} #include "build.h" diff --git a/source/core/savegamehelp.h b/source/core/savegamehelp.h index e19d10e4f..f6d970083 100644 --- a/source/core/savegamehelp.h +++ b/source/core/savegamehelp.h @@ -9,7 +9,6 @@ class FileReader; extern int SaveVersion; -FString G_BuildSaveName (const char *prefix); int G_ValidateSavegame(FileReader &fr, FString *savetitle, bool formenu); void G_LoadGame(const char* filename, bool hidecon = false); @@ -19,9 +18,6 @@ void G_DoLoadGame(); void M_Autosave(); -#define SAVEGAME_EXT ".dsave" - - template<> inline FSerializer& Serialize(FSerializer& arc, const char* keyname, sectortype*& w, sectortype** def) { assert(arc.isReading() || w == nullptr || (w >= §or[0] && w <= §or.Last())); diff --git a/source/core/version.h b/source/core/version.h index 15baa3547..2560fa71f 100644 --- a/source/core/version.h +++ b/source/core/version.h @@ -69,6 +69,8 @@ const char *GetVersionString(); #define SAVESIG_SW GAMENAME ".ShadowWarrior" #define SAVESIG_PS GAMENAME ".Exhumed" +#define SAVEGAME_EXT "dsave" + #define MINSAVEVER_DN3D 16 #define MINSAVEVER_BLD 16 #define MINSAVEVER_SW 17