diff --git a/src/engine/shared/datafile.cpp b/src/engine/shared/datafile.cpp index 5b75771af..c0f668487 100644 --- a/src/engine/shared/datafile.cpp +++ b/src/engine/shared/datafile.cpp @@ -525,16 +525,35 @@ CDataFileWriter::CDataFileWriter() CDataFileWriter::~CDataFileWriter() { + if(m_File) + { + io_close(m_File); + m_File = 0; + } + free(m_pItemTypes); m_pItemTypes = nullptr; - for(int i = 0; i < m_NumItems; i++) - free(m_pItems[i].m_pData); - for(int i = 0; i < m_NumDatas; ++i) - free(m_pDatas[i].m_pCompressedData); - free(m_pItems); - m_pItems = nullptr; - free(m_pDatas); - m_pDatas = nullptr; + + if(m_pItems) + { + for(int i = 0; i < m_NumItems; i++) + { + free(m_pItems[i].m_pData); + } + free(m_pItems); + m_pItems = nullptr; + } + + if(m_pDatas) + { + for(int i = 0; i < m_NumDatas; ++i) + { + free(m_pDatas[i].m_pUncompressedData); + free(m_pDatas[i].m_pCompressedData); + } + free(m_pDatas); + m_pDatas = nullptr; + } } bool CDataFileWriter::OpenFile(class IStorage *pStorage, const char *pFilename, int StorageType) @@ -636,21 +655,12 @@ int CDataFileWriter::AddData(int Size, void *pData, int CompressionLevel) dbg_assert(m_NumDatas < 1024, "too much data"); CDataInfo *pInfo = &m_pDatas[m_NumDatas]; - unsigned long s = compressBound(Size); - void *pCompData = malloc(s); // temporary buffer that we use during compression - - int Result = compress2((Bytef *)pCompData, &s, (Bytef *)pData, Size, CompressionLevel); - if(Result != Z_OK) - { - dbg_msg("datafile", "compression error %d", Result); - dbg_assert(0, "zlib error"); - } - + pInfo->m_pUncompressedData = malloc(Size); + mem_copy(pInfo->m_pUncompressedData, pData, Size); pInfo->m_UncompressedSize = Size; - pInfo->m_CompressedSize = (int)s; - pInfo->m_pCompressedData = malloc(pInfo->m_CompressedSize); - mem_copy(pInfo->m_pCompressedData, pCompData, pInfo->m_CompressedSize); - free(pCompData); + pInfo->m_pCompressedData = nullptr; + pInfo->m_CompressedSize = 0; + pInfo->m_CompressionLevel = CompressionLevel; m_NumDatas++; return m_NumDatas - 1; @@ -672,15 +682,31 @@ int CDataFileWriter::AddDataSwapped(int Size, void *pData) #endif } -int CDataFileWriter::Finish() +void CDataFileWriter::Finish() { - if(!m_File) - return 1; + dbg_assert((bool)m_File, "file not open"); // we should now write this file! if(DEBUG) dbg_msg("datafile", "writing"); + // Compress data. This takes the majority of the time when saving a datafile, + // so it's delayed until the end so it can be off-loaded to another thread. + for(int i = 0; i < m_NumDatas; i++) + { + unsigned long CompressedSize = compressBound(m_pDatas[i].m_UncompressedSize); + m_pDatas[i].m_pCompressedData = malloc(CompressedSize); + const int Result = compress2((Bytef *)m_pDatas[i].m_pCompressedData, &CompressedSize, (Bytef *)m_pDatas[i].m_pUncompressedData, m_pDatas[i].m_UncompressedSize, m_pDatas[i].m_CompressionLevel); + m_pDatas[i].m_CompressedSize = CompressedSize; + free(m_pDatas[i].m_pUncompressedData); + m_pDatas[i].m_pUncompressedData = nullptr; + if(Result != Z_OK) + { + dbg_msg("datafile", "compression error %d", Result); + dbg_assert(false, "zlib error"); + } + } + // calculate sizes int ItemSize = 0; for(int i = 0; i < m_NumItems; i++) @@ -851,5 +877,4 @@ int CDataFileWriter::Finish() if(DEBUG) dbg_msg("datafile", "done"); - return 0; } diff --git a/src/engine/shared/datafile.h b/src/engine/shared/datafile.h index c124812b4..abc5af217 100644 --- a/src/engine/shared/datafile.h +++ b/src/engine/shared/datafile.h @@ -58,9 +58,11 @@ class CDataFileWriter { struct CDataInfo { + void *m_pUncompressedData; int m_UncompressedSize; - int m_CompressedSize; void *m_pCompressedData; + int m_CompressedSize; + int m_CompressionLevel; }; struct CItemInfo @@ -103,14 +105,31 @@ class CDataFileWriter public: CDataFileWriter(); + CDataFileWriter(CDataFileWriter &&Other) : + m_NumItems(Other.m_NumItems), + m_NumDatas(Other.m_NumDatas), + m_NumItemTypes(Other.m_NumItemTypes), + m_NumExtendedItemTypes(Other.m_NumExtendedItemTypes) + { + m_File = Other.m_File; + Other.m_File = 0; + m_pItemTypes = Other.m_pItemTypes; + Other.m_pItemTypes = nullptr; + m_pItems = Other.m_pItems; + Other.m_pItems = nullptr; + m_pDatas = Other.m_pDatas; + Other.m_pDatas = nullptr; + mem_copy(m_aExtendedItemTypes, Other.m_aExtendedItemTypes, sizeof(m_aExtendedItemTypes)); + } ~CDataFileWriter(); + void Init(); bool OpenFile(class IStorage *pStorage, const char *pFilename, int StorageType = IStorage::TYPE_SAVE); bool Open(class IStorage *pStorage, const char *pFilename, int StorageType = IStorage::TYPE_SAVE); int AddData(int Size, void *pData, int CompressionLevel = Z_DEFAULT_COMPRESSION); int AddDataSwapped(int Size, void *pData); int AddItem(int Type, int ID, int Size, void *pData); - int Finish(); + void Finish(); }; #endif diff --git a/src/engine/shared/storage.cpp b/src/engine/shared/storage.cpp index 929191733..4b01a3d1d 100644 --- a/src/engine/shared/storage.cpp +++ b/src/engine/shared/storage.cpp @@ -68,6 +68,7 @@ public: CreateFolder("screenshots/auto", TYPE_SAVE); CreateFolder("screenshots/auto/stats", TYPE_SAVE); CreateFolder("maps", TYPE_SAVE); + CreateFolder("maps/auto", TYPE_SAVE); CreateFolder("mapres", TYPE_SAVE); CreateFolder("downloadedmaps", TYPE_SAVE); CreateFolder("skins", TYPE_SAVE); diff --git a/src/game/editor/auto_map.cpp b/src/game/editor/auto_map.cpp index 4b8b481d5..d339a2601 100644 --- a/src/game/editor/auto_map.cpp +++ b/src/game/editor/auto_map.cpp @@ -470,7 +470,7 @@ void CAutoMapper::Proceed(CLayerTiles *pLayer, int ConfigID, int Seed, int SeedO for(int x = 0; x < pLayer->m_Width; x++) { CTile *pTile = &(pLayer->m_pTiles[y * pLayer->m_Width + x]); - m_pEditor->m_Map.m_Modified = true; + m_pEditor->m_Map.OnModify(); for(size_t i = 0; i < pRun->m_vIndexRules.size(); ++i) { diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index b6dacce78..d51bbb0b7 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -212,7 +213,7 @@ void CLayerGroup::Render() void CLayerGroup::AddLayer(CLayer *pLayer) { - m_pMap->m_Modified = true; + m_pMap->OnModify(); m_vpLayers.push_back(pLayer); } @@ -222,7 +223,7 @@ void CLayerGroup::DeleteLayer(int Index) return; delete m_vpLayers[Index]; m_vpLayers.erase(m_vpLayers.begin() + Index); - m_pMap->m_Modified = true; + m_pMap->OnModify(); } void CLayerGroup::DuplicateLayer(int Index) @@ -233,7 +234,7 @@ void CLayerGroup::DuplicateLayer(int Index) auto *pDup = m_vpLayers[Index]->Duplicate(); m_vpLayers.insert(m_vpLayers.begin() + Index + 1, pDup); - m_pMap->m_Modified = true; + m_pMap->OnModify(); } void CLayerGroup::GetSize(float *pWidth, float *pHeight) const @@ -257,7 +258,7 @@ int CLayerGroup::SwapLayers(int Index0, int Index1) return Index0; if(Index0 == Index1) return Index0; - m_pMap->m_Modified = true; + m_pMap->OnModify(); std::swap(m_vpLayers[Index0], m_vpLayers[Index1]); return Index1; } @@ -833,7 +834,7 @@ bool CEditor::CallbackAppendMap(const char *pFileName, int StorageType, void *pU bool CEditor::CallbackSaveMap(const char *pFileName, int StorageType, void *pUser) { CEditor *pEditor = static_cast(pUser); - char aBuf[1024]; + char aBuf[IO_MAX_PATH_LENGTH]; // add map extension if(!str_endswith(pFileName, ".map")) { @@ -841,25 +842,35 @@ bool CEditor::CallbackSaveMap(const char *pFileName, int StorageType, void *pUse pFileName = aBuf; } + // Save map to specified file if(pEditor->Save(pFileName)) { str_copy(pEditor->m_aFileName, pFileName); pEditor->m_ValidSaveFilename = StorageType == IStorage::TYPE_SAVE && pEditor->m_pFileDialogPath == pEditor->m_aFileDialogCurrentFolder; pEditor->m_Map.m_Modified = false; - pEditor->m_Dialog = DIALOG_NONE; - return true; } else { pEditor->ShowFileDialogError("Failed to save map to file '%s'.", pFileName); return false; } + + // Also update autosave if it's older than half the configured autosave interval, so we also have periodic backups. + const float Time = pEditor->Client()->GlobalTime(); + if(g_Config.m_EdAutosaveInterval > 0 && pEditor->m_Map.m_LastSaveTime < Time && Time - pEditor->m_Map.m_LastSaveTime > 30 * g_Config.m_EdAutosaveInterval) + { + if(!pEditor->PerformAutosave()) + return false; + } + + pEditor->m_Dialog = DIALOG_NONE; + return true; } bool CEditor::CallbackSaveCopyMap(const char *pFileName, int StorageType, void *pUser) { CEditor *pEditor = static_cast(pUser); - char aBuf[1024]; + char aBuf[IO_MAX_PATH_LENGTH]; // add map extension if(!str_endswith(pFileName, ".map")) { @@ -869,7 +880,6 @@ bool CEditor::CallbackSaveCopyMap(const char *pFileName, int StorageType, void * if(pEditor->Save(pFileName)) { - pEditor->m_Map.m_Modified = false; pEditor->m_Dialog = DIALOG_NONE; return true; } @@ -1529,7 +1539,7 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) if(m_vSelectedLayers.size() == 1) { UI()->DisableMouseLock(); - m_Map.m_Modified = true; + m_Map.OnModify(); DeleteSelectedQuads(); } s_Operation = OP_NONE; @@ -3822,7 +3832,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) m_SelectedGroup = GroupAfterDraggedLayer - 1; m_vSelectedLayers.clear(); m_vSelectedQuads.clear(); - m_Map.m_Modified = true; + m_Map.OnModify(); } } @@ -3839,7 +3849,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) m_Map.m_vpGroups.insert(InsertPosition, pSelectedGroup); m_SelectedGroup = InsertPosition - m_Map.m_vpGroups.begin(); - m_Map.m_Modified = true; + m_Map.OnModify(); } if(MoveLayers || MoveGroup) @@ -5349,7 +5359,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) static int s_NewSoundButton = 0; if(DoButton_Editor(&s_NewSoundButton, "Sound+", 0, &Button, 0, "Creates a new sound envelope")) { - m_Map.m_Modified = true; + m_Map.OnModify(); pNewEnv = m_Map.NewEnvelope(1); } @@ -5358,7 +5368,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) static int s_New4dButton = 0; if(DoButton_Editor(&s_New4dButton, "Color+", 0, &Button, 0, "Creates a new color envelope")) { - m_Map.m_Modified = true; + m_Map.OnModify(); pNewEnv = m_Map.NewEnvelope(4); } @@ -5367,7 +5377,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) static int s_New2dButton = 0; if(DoButton_Editor(&s_New2dButton, "Pos.+", 0, &Button, 0, "Creates a new position envelope")) { - m_Map.m_Modified = true; + m_Map.OnModify(); pNewEnv = m_Map.NewEnvelope(3); } @@ -5478,7 +5488,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) s_NameInput.SetBuffer(pEnvelope->m_aName, sizeof(pEnvelope->m_aName)); if(DoEditBox(&s_NameInput, &Button, 10.0f, IGraphics::CORNER_ALL, "The name of the selected envelope")) { - m_Map.m_Modified = true; + m_Map.OnModify(); } } } @@ -5583,7 +5593,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) pEnvelope->AddPoint(Time, f2fx(Channels.r), f2fx(Channels.g), f2fx(Channels.b), f2fx(Channels.a)); - m_Map.m_Modified = true; + m_Map.OnModify(); } m_ShowEnvelopePreview = SHOWENV_SELECTED; @@ -5762,7 +5772,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) m_SelectedQuadEnvelope = m_SelectedEnvelope; m_ShowEnvelopePreview = SHOWENV_SELECTED; m_SelectedEnvelopePoint = i; - m_Map.m_Modified = true; + m_Map.OnModify(); } ColorMod = 100.0f; @@ -5792,7 +5802,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } pEnvelope->m_vPoints.erase(pEnvelope->m_vPoints.begin() + i); - m_Map.m_Modified = true; + m_Map.OnModify(); } m_ShowEnvelopePreview = SHOWENV_SELECTED; @@ -6435,41 +6445,58 @@ void CEditor::Render() if(m_GuiActive) RenderStatusbar(StatusBar); - // - if(g_Config.m_EdShowkeys) - { - UI()->MapScreen(); - CTextCursor Cursor; - TextRender()->SetCursor(&Cursor, View.x + 10, View.y + View.h - 24 - 10, 24.0f, TEXTFLAG_RENDER); + RenderPressedKeys(View); + RenderSavingIndicator(View); + RenderMousePointer(); +} - int NKeys = 0; - for(int i = 0; i < KEY_LAST; i++) +void CEditor::RenderPressedKeys(CUIRect View) +{ + if(!g_Config.m_EdShowkeys) + return; + + UI()->MapScreen(); + CTextCursor Cursor; + TextRender()->SetCursor(&Cursor, View.x + 10, View.y + View.h - 24 - 10, 24.0f, TEXTFLAG_RENDER); + + int NKeys = 0; + for(int i = 0; i < KEY_LAST; i++) + { + if(Input()->KeyIsPressed(i)) { - if(Input()->KeyIsPressed(i)) - { - if(NKeys) - TextRender()->TextEx(&Cursor, " + ", -1); - TextRender()->TextEx(&Cursor, Input()->KeyName(i), -1); - NKeys++; - } + if(NKeys) + TextRender()->TextEx(&Cursor, " + ", -1); + TextRender()->TextEx(&Cursor, Input()->KeyName(i), -1); + NKeys++; } } +} - if(m_ShowMousePointer) - { - // render butt ugly mouse cursor - float mx = UI()->MouseX(); - float my = UI()->MouseY(); - Graphics()->WrapClamp(); - Graphics()->TextureSet(m_CursorTexture); - Graphics()->QuadsBegin(); - if(ms_pUiGotContext == UI()->HotItem()) - Graphics()->SetColor(1, 0, 0, 1); - IGraphics::CQuadItem QuadItem(mx, my, 16.0f, 16.0f); - Graphics()->QuadsDrawTL(&QuadItem, 1); - Graphics()->QuadsEnd(); - Graphics()->WrapNormal(); - } +void CEditor::RenderSavingIndicator(CUIRect View) +{ + if(m_lpWriterFinishJobs.empty()) + return; + + UI()->MapScreen(); + CUIRect Label; + View.Margin(20.0f, &Label); + UI()->DoLabel(&Label, "Saving…", 24.0f, TEXTALIGN_BR); +} + +void CEditor::RenderMousePointer() +{ + if(!m_ShowMousePointer) + return; + + Graphics()->WrapClamp(); + Graphics()->TextureSet(m_CursorTexture); + Graphics()->QuadsBegin(); + if(ms_pUiGotContext == UI()->HotItem()) + Graphics()->SetColor(1, 0, 0, 1); + IGraphics::CQuadItem QuadItem(UI()->MouseX(), UI()->MouseY(), 16.0f, 16.0f); + Graphics()->QuadsDrawTL(&QuadItem, 1); + Graphics()->QuadsEnd(); + Graphics()->WrapNormal(); } void CEditor::Reset(bool CreateDefault) @@ -6512,6 +6539,10 @@ void CEditor::Reset(bool CreateDefault) m_MouseDeltaWy = 0; m_Map.m_Modified = false; + m_Map.m_ModifiedAuto = false; + m_Map.m_LastModifiedTime = -1.0f; + m_Map.m_LastSaveTime = Client()->GlobalTime(); + m_Map.m_LastAutosaveUpdateTime = -1.0f; m_ShowEnvelopePreview = SHOWENV_NONE; m_ShiftBy = 1; @@ -6630,12 +6661,19 @@ void CEditor::Goto(float X, float Y) m_WorldOffsetY = Y * 32; } +void CEditorMap::OnModify() +{ + m_Modified = true; + m_ModifiedAuto = true; + m_LastModifiedTime = m_pEditor->Client()->GlobalTime(); +} + void CEditorMap::DeleteEnvelope(int Index) { if(Index < 0 || Index >= (int)m_vpEnvelopes.size()) return; - m_Modified = true; + OnModify(); VisitEnvelopeReferences([Index](int &ElementIndex) { if(ElementIndex == Index) @@ -6656,7 +6694,7 @@ void CEditorMap::SwapEnvelopes(int Index0, int Index1) if(Index0 == Index1) return; - m_Modified = true; + OnModify(); VisitEnvelopeReferences([Index0, Index1](int &ElementIndex) { if(ElementIndex == Index0) @@ -6736,6 +6774,7 @@ void CEditorMap::Clean() m_pGameGroup = nullptr; m_Modified = false; + m_ModifiedAuto = false; m_pTeleLayer = nullptr; m_pSpeedupLayer = nullptr; @@ -6835,6 +6874,7 @@ void CEditor::Init() m_pClient = Kernel()->RequestInterface(); m_pConfig = Kernel()->RequestInterface()->Values(); m_pConsole = Kernel()->RequestInterface(); + m_pEngine = Kernel()->RequestInterface(); m_pGraphics = Kernel()->RequestInterface(); m_pTextRender = Kernel()->RequestInterface(); m_pStorage = Kernel()->RequestInterface(); @@ -6861,7 +6901,6 @@ void CEditor::Init() m_Brush.m_pMap = &m_Map; Reset(false); - m_Map.m_Modified = false; ResetMenuBackgroundPositions(); m_vpMenuBackgroundPositionNames.resize(CMenuBackground::NUM_POS); @@ -6971,6 +7010,109 @@ void CEditor::HandleCursorMovement() } } +void CEditor::HandleAutosave() +{ + const float Time = Client()->GlobalTime(); + const float LastAutosaveUpdateTime = m_Map.m_LastAutosaveUpdateTime; + m_Map.m_LastAutosaveUpdateTime = Time; + + if(g_Config.m_EdAutosaveInterval == 0) + return; // autosave disabled + if(!m_Map.m_ModifiedAuto || m_Map.m_LastModifiedTime < 0.0f) + return; // no unsaved changes + + // Add time to autosave timer if the editor was disabled for more than 10 seconds, + // to prevent autosave from immediately activating when the editor is activated + // after being deactivated for some time. + if(LastAutosaveUpdateTime >= 0.0f && Time - LastAutosaveUpdateTime > 10.0f) + { + m_Map.m_LastSaveTime += Time - LastAutosaveUpdateTime; + } + + // Check if autosave timer has expired. + if(m_Map.m_LastSaveTime >= Time || Time - m_Map.m_LastSaveTime < 60 * g_Config.m_EdAutosaveInterval) + return; + + // Wait for 5 seconds of no modification before saving, to prevent autosave + // from immediately activating when a map is first modified or while user is + // modifying the map, but don't delay the autosave for more than 1 minute. + if(Time - m_Map.m_LastModifiedTime < 5.0f && Time - m_Map.m_LastSaveTime < 60 * (g_Config.m_EdAutosaveInterval + 1)) + return; + + PerformAutosave(); +} + +bool CEditor::PerformAutosave() +{ + char aDate[20]; + char aAutosavePath[IO_MAX_PATH_LENGTH]; + str_timestamp(aDate, sizeof(aDate)); + char aFileNameNoExt[IO_MAX_PATH_LENGTH]; + if(m_aFileName[0] == '\0') + { + str_copy(aFileNameNoExt, "unnamed"); + } + else + { + const char *pFileName = fs_filename(m_aFileName); + str_truncate(aFileNameNoExt, sizeof(aFileNameNoExt), pFileName, str_length(pFileName) - str_length(".map")); + } + str_format(aAutosavePath, sizeof(aAutosavePath), "maps/auto/%s_%s.map", aFileNameNoExt, aDate); + + m_Map.m_LastSaveTime = Client()->GlobalTime(); + if(Save(aAutosavePath)) + { + m_Map.m_ModifiedAuto = false; + // Clean up autosaves + if(g_Config.m_EdAutosaveMax) + { + CFileCollection AutosavedMaps; + AutosavedMaps.Init(Storage(), "maps/auto", aFileNameNoExt, ".map", g_Config.m_EdAutosaveMax); + } + return true; + } + else + { + ShowFileDialogError("Failed to automatically save map to file '%s'.", aAutosavePath); + return false; + } +} + +void CEditor::HandleWriterFinishJobs() +{ + if(m_lpWriterFinishJobs.empty()) + return; + + std::shared_ptr pJob = m_lpWriterFinishJobs.front(); + if(pJob->Status() != IJob::STATE_DONE) + return; + + char aBuf[IO_MAX_PATH_LENGTH + 32]; + str_format(aBuf, sizeof(aBuf), "saving '%s' done", pJob->GetFileName()); + Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "editor", aBuf); + + // send rcon.. if we can + if(Client()->RconAuthed()) + { + CServerInfo CurrentServerInfo; + Client()->GetServerInfo(&CurrentServerInfo); + NETADDR ServerAddr = Client()->ServerAddress(); + const unsigned char aIpv4Localhost[16] = {127, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + const unsigned char aIpv6Localhost[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; + + // and if we're on localhost + if(!mem_comp(ServerAddr.ip, aIpv4Localhost, sizeof(aIpv4Localhost)) || !mem_comp(ServerAddr.ip, aIpv6Localhost, sizeof(aIpv6Localhost))) + { + char aMapName[128]; + IStorage::StripPathAndExtension(pJob->GetFileName(), aMapName, sizeof(aMapName)); + if(!str_comp(aMapName, CurrentServerInfo.m_aMap)) + Client()->Rcon("reload"); + } + } + + m_lpWriterFinishJobs.pop_front(); +} + void CEditor::OnUpdate() { CUIElementBase::Init(UI()); // update static pointer because game and editor use separate UI @@ -6982,6 +7124,8 @@ void CEditor::OnUpdate() } HandleCursorMovement(); + HandleAutosave(); + HandleWriterFinishJobs(); } void CEditor::OnRender() diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index 8423b5e1a..7f3ef5ddf 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -12,11 +12,15 @@ #include #include +#include #include +#include +#include #include "auto_map.h" #include +#include #include #include @@ -315,7 +319,6 @@ class CEditorMap public: CEditor *m_pEditor; - bool m_Modified; CEditorMap() { @@ -327,6 +330,13 @@ public: Clean(); } + bool m_Modified; // unsaved changes in manual save + bool m_ModifiedAuto; // unsaved changes in autosave + float m_LastModifiedTime; + float m_LastSaveTime; + float m_LastAutosaveUpdateTime; + void OnModify(); + std::vector m_vpGroups; std::vector m_vpImages; std::vector m_vpEnvelopes; @@ -370,7 +380,7 @@ public: CEnvelope *NewEnvelope(int Channels) { - m_Modified = true; + OnModify(); CEnvelope *pEnv = new CEnvelope(Channels); m_vpEnvelopes.push_back(pEnv); return pEnv; @@ -383,7 +393,7 @@ public: CLayerGroup *NewGroup() { - m_Modified = true; + OnModify(); CLayerGroup *pGroup = new CLayerGroup; pGroup->m_pMap = this; m_vpGroups.push_back(pGroup); @@ -398,7 +408,7 @@ public: return Index0; if(Index0 == Index1) return Index0; - m_Modified = true; + OnModify(); std::swap(m_vpGroups[Index0], m_vpGroups[Index1]); return Index1; } @@ -407,28 +417,28 @@ public: { if(Index < 0 || Index >= (int)m_vpGroups.size()) return; - m_Modified = true; + OnModify(); delete m_vpGroups[Index]; m_vpGroups.erase(m_vpGroups.begin() + Index); } void ModifyImageIndex(INDEX_MODIFY_FUNC pfnFunc) { - m_Modified = true; + OnModify(); for(auto &pGroup : m_vpGroups) pGroup->ModifyImageIndex(pfnFunc); } void ModifyEnvelopeIndex(INDEX_MODIFY_FUNC pfnFunc) { - m_Modified = true; + OnModify(); for(auto &pGroup : m_vpGroups) pGroup->ModifyEnvelopeIndex(pfnFunc); } void ModifySoundIndex(INDEX_MODIFY_FUNC pfnFunc) { - m_Modified = true; + OnModify(); for(auto &pGroup : m_vpGroups) pGroup->ModifySoundIndex(pfnFunc); } @@ -685,16 +695,37 @@ public: CUI::EPopupMenuFunctionResult RenderProperties(CUIRect *pToolbox) override; }; +class CDataFileWriterFinishJob : public IJob +{ + char m_aFileName[IO_MAX_PATH_LENGTH]; + CDataFileWriter m_Writer; + + void Run() override + { + m_Writer.Finish(); + } + +public: + CDataFileWriterFinishJob(const char *pFileName, CDataFileWriter &&Writer) : + m_Writer(std::move(Writer)) + { + str_copy(m_aFileName, pFileName); + } + + const char *GetFileName() const { return m_aFileName; } +}; + class CEditor : public IEditor { - class IInput *m_pInput; - class IClient *m_pClient; - class CConfig *m_pConfig; - class IConsole *m_pConsole; - class IGraphics *m_pGraphics; - class ITextRender *m_pTextRender; - class ISound *m_pSound; - class IStorage *m_pStorage; + class IInput *m_pInput = nullptr; + class IClient *m_pClient = nullptr; + class CConfig *m_pConfig = nullptr; + class IConsole *m_pConsole = nullptr; + class IEngine *m_pEngine = nullptr; + class IGraphics *m_pGraphics = nullptr; + class ITextRender *m_pTextRender = nullptr; + class ISound *m_pSound = nullptr; + class IStorage *m_pStorage = nullptr; CRenderTools m_RenderTools; CUI m_UI; @@ -722,6 +753,7 @@ public: class IClient *Client() { return m_pClient; } class CConfig *Config() { return m_pConfig; } class IConsole *Console() { return m_pConsole; } + class IEngine *Engine() { return m_pEngine; } class IGraphics *Graphics() { return m_pGraphics; } class ISound *Sound() { return m_pSound; } class ITextRender *TextRender() { return m_pTextRender; } @@ -732,12 +764,6 @@ public: CEditor() : m_TilesetPicker(16, 16) { - m_pInput = nullptr; - m_pClient = nullptr; - m_pGraphics = nullptr; - m_pTextRender = nullptr; - m_pSound = nullptr; - m_EntitiesTexture.Invalidate(); m_FrontTexture.Invalidate(); m_TeleTexture.Invalidate(); @@ -850,6 +876,9 @@ public: void ResetMentions() override { m_Mentions = 0; } void HandleCursorMovement(); + void HandleAutosave(); + bool PerformAutosave(); + void HandleWriterFinishJobs(); CLayerGroup *m_apSavedBrushes[10]; @@ -868,6 +897,10 @@ public: void LoadCurrentMap(); void Render(); + void RenderPressedKeys(CUIRect View); + void RenderSavingIndicator(CUIRect View); + void RenderMousePointer(); + void ResetMenuBackgroundPositions(); std::vector GetSelectedQuads(); @@ -1121,6 +1154,8 @@ public: static const void *ms_pUiGotContext; CEditorMap m_Map; + std::list> m_lpWriterFinishJobs; + int m_ShiftBy; static void EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Channels, void *pUser); diff --git a/src/game/editor/io.cpp b/src/game/editor/io.cpp index 54d4bff60..fd1dc8374 100644 --- a/src/game/editor/io.cpp +++ b/src/game/editor/io.cpp @@ -33,6 +33,10 @@ struct CSoundSource_DEPRECATED bool CEditor::Save(const char *pFilename) { + // Check if file with this name is already being saved at the moment + if(std::any_of(std::begin(m_lpWriterFinishJobs), std::end(m_lpWriterFinishJobs), [pFilename](const std::shared_ptr &Job) { return str_comp(pFilename, Job->GetFileName()) == 0; })) + return false; + return m_Map.Save(pFilename); } @@ -372,27 +376,9 @@ bool CEditorMap::Save(const char *pFileName) free(pPoints); // finish the data file - df.Finish(); - m_pEditor->Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "editor", "saving done"); - - // send rcon.. if we can - if(m_pEditor->Client()->RconAuthed()) - { - CServerInfo CurrentServerInfo; - m_pEditor->Client()->GetServerInfo(&CurrentServerInfo); - NETADDR ServerAddr = m_pEditor->Client()->ServerAddress(); - const unsigned char aIpv4Localhost[16] = {127, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - const unsigned char aIpv6Localhost[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; - - // and if we're on localhost - if(!mem_comp(ServerAddr.ip, aIpv4Localhost, sizeof(aIpv4Localhost)) || !mem_comp(ServerAddr.ip, aIpv6Localhost, sizeof(aIpv6Localhost))) - { - char aMapName[128]; - IStorage::StripPathAndExtension(pFileName, aMapName, sizeof(aMapName)); - if(!str_comp(aMapName, CurrentServerInfo.m_aMap)) - m_pEditor->Client()->Rcon("reload"); - } - } + std::shared_ptr pWriterFinishJob = std::make_shared(pFileName, std::move(df)); + m_pEditor->Engine()->AddJob(pWriterFinishJob); + m_pEditor->m_lpWriterFinishJobs.push_back(pWriterFinishJob); return true; } @@ -976,6 +962,9 @@ bool CEditorMap::Load(const char *pFileName, int StorageType) return false; m_Modified = false; + m_ModifiedAuto = false; + m_LastModifiedTime = -1.0f; + m_LastSaveTime = m_pEditor->Client()->GlobalTime(); return true; } diff --git a/src/game/editor/layer_quads.cpp b/src/game/editor/layer_quads.cpp index 585ca776a..27dc484f4 100644 --- a/src/game/editor/layer_quads.cpp +++ b/src/game/editor/layer_quads.cpp @@ -37,7 +37,7 @@ void CLayerQuads::Render(bool QuadPicker) CQuad *CLayerQuads::NewQuad(int x, int y, int Width, int Height) { - m_pEditor->m_Map.m_Modified = true; + m_pEditor->m_Map.OnModify(); m_vQuads.emplace_back(); CQuad *pQuad = &m_vQuads[m_vQuads.size() - 1]; @@ -153,7 +153,7 @@ void CLayerQuads::BrushPlace(CLayer *pBrush, float wx, float wy) m_vQuads.push_back(n); } - m_pEditor->m_Map.m_Modified = true; + m_pEditor->m_Map.OnModify(); } void CLayerQuads::BrushFlipX() @@ -163,7 +163,7 @@ void CLayerQuads::BrushFlipX() std::swap(Quad.m_aPoints[0], Quad.m_aPoints[1]); std::swap(Quad.m_aPoints[2], Quad.m_aPoints[3]); } - m_pEditor->m_Map.m_Modified = true; + m_pEditor->m_Map.OnModify(); } void CLayerQuads::BrushFlipY() @@ -173,7 +173,7 @@ void CLayerQuads::BrushFlipY() std::swap(Quad.m_aPoints[0], Quad.m_aPoints[2]); std::swap(Quad.m_aPoints[1], Quad.m_aPoints[3]); } - m_pEditor->m_Map.m_Modified = true; + m_pEditor->m_Map.OnModify(); } void Rotate(vec2 *pCenter, vec2 *pPoint, float Rotation) @@ -236,7 +236,7 @@ CUI::EPopupMenuFunctionResult CLayerQuads::RenderProperties(CUIRect *pToolBox) int Prop = m_pEditor->DoProperties(pToolBox, aProps, s_aIds, &NewVal); if(Prop != -1) { - m_pEditor->m_Map.m_Modified = true; + m_pEditor->m_Map.OnModify(); } if(Prop == PROP_IMAGE) @@ -277,7 +277,7 @@ int CLayerQuads::SwapQuads(int Index0, int Index1) return Index0; if(Index0 == Index1) return Index0; - m_pEditor->m_Map.m_Modified = true; + m_pEditor->m_Map.OnModify(); std::swap(m_vQuads[Index0], m_vQuads[Index1]); return Index1; } diff --git a/src/game/editor/layer_sounds.cpp b/src/game/editor/layer_sounds.cpp index 1c236aa54..1f20f1def 100644 --- a/src/game/editor/layer_sounds.cpp +++ b/src/game/editor/layer_sounds.cpp @@ -101,7 +101,7 @@ void CLayerSounds::Render(bool Tileset) CSoundSource *CLayerSounds::NewSource(int x, int y) { - m_pEditor->m_Map.m_Modified = true; + m_pEditor->m_Map.OnModify(); m_vSources.emplace_back(); CSoundSource *pSource = &m_vSources[m_vSources.size() - 1]; @@ -179,7 +179,7 @@ void CLayerSounds::BrushPlace(CLayer *pBrush, float wx, float wy) m_vSources.push_back(n); } - m_pEditor->m_Map.m_Modified = true; + m_pEditor->m_Map.OnModify(); } CUI::EPopupMenuFunctionResult CLayerSounds::RenderProperties(CUIRect *pToolBox) @@ -200,7 +200,7 @@ CUI::EPopupMenuFunctionResult CLayerSounds::RenderProperties(CUIRect *pToolBox) int Prop = m_pEditor->DoProperties(pToolBox, aProps, s_aIds, &NewVal); if(Prop != -1) { - m_pEditor->m_Map.m_Modified = true; + m_pEditor->m_Map.OnModify(); } if(Prop == PROP_SOUND) diff --git a/src/game/editor/layer_tiles.cpp b/src/game/editor/layer_tiles.cpp index cbbbeed95..d093827ff 100644 --- a/src/game/editor/layer_tiles.cpp +++ b/src/game/editor/layer_tiles.cpp @@ -1079,7 +1079,7 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropSta void CLayerTiles::FlagModified(int x, int y, int w, int h) { - m_pEditor->m_Map.m_Modified = true; + m_pEditor->m_Map.OnModify(); if(m_Seed != 0 && m_AutoMapperConfig != -1 && m_AutoAutoMap && m_Image >= 0) { m_pEditor->m_Map.m_vpImages[m_Image]->m_AutoMapper.ProceedLocalized(this, m_AutoMapperConfig, m_Seed, x, y, w, h); diff --git a/src/game/editor/popups.cpp b/src/game/editor/popups.cpp index 2e7e68bcf..a9bdd0443 100644 --- a/src/game/editor/popups.cpp +++ b/src/game/editor/popups.cpp @@ -366,7 +366,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, if(!Found) { pGameLayer->m_pTiles[y * pGameLayer->m_Width + x].m_Index = TILE_AIR; - pEditor->m_Map.m_Modified = true; + pEditor->m_Map.OnModify(); } } } @@ -512,7 +512,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, static CLineInput s_NameInput; s_NameInput.SetBuffer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_aName, sizeof(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_aName)); if(pEditor->DoEditBox(&s_NameInput, &Button, 10.0f)) - pEditor->m_Map.m_Modified = true; + pEditor->m_Map.OnModify(); } enum @@ -557,7 +557,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, int Prop = pEditor->DoProperties(&View, aProps, s_aIds, &NewVal); if(Prop != -1) { - pEditor->m_Map.m_Modified = true; + pEditor->m_Map.OnModify(); } if(Prop == PROP_ORDER) @@ -683,7 +683,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, static CLineInput s_NameInput; s_NameInput.SetBuffer(pCurrentLayer->m_aName, sizeof(pCurrentLayer->m_aName)); if(pEditor->DoEditBox(&s_NameInput, &EditBox, 10.0f)) - pEditor->m_Map.m_Modified = true; + pEditor->m_Map.OnModify(); } // spacing if any button was rendered @@ -717,7 +717,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, int Prop = pEditor->DoProperties(&View, aProps, s_aIds, &NewVal); if(Prop != -1) { - pEditor->m_Map.m_Modified = true; + pEditor->m_Map.OnModify(); } if(Prop == PROP_ORDER) @@ -762,7 +762,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b { if(pLayer) { - pEditor->m_Map.m_Modified = true; + pEditor->m_Map.OnModify(); pEditor->DeleteSelectedQuads(); } return CUI::POPUP_CLOSE_CURRENT; @@ -802,7 +802,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b pQuad->m_aPoints[2].y = Top + Height; pQuad->m_aPoints[3].x = Right; pQuad->m_aPoints[3].y = Top + Height; - pEditor->m_Map.m_Modified = true; + pEditor->m_Map.OnModify(); } return CUI::POPUP_CLOSE_CURRENT; } @@ -821,7 +821,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b pQuad->m_aPoints[k].x = 1000.0f * (pQuad->m_aPoints[k].x / 1000); pQuad->m_aPoints[k].y = 1000.0f * (pQuad->m_aPoints[k].y / 1000); } - pEditor->m_Map.m_Modified = true; + pEditor->m_Map.OnModify(); } return CUI::POPUP_CLOSE_CURRENT; } @@ -859,7 +859,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b pQuad->m_aPoints[2].y = Bottom; pQuad->m_aPoints[3].x = Right; pQuad->m_aPoints[3].y = Bottom; - pEditor->m_Map.m_Modified = true; + pEditor->m_Map.OnModify(); } return CUI::POPUP_CLOSE_CURRENT; } @@ -904,7 +904,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b int Prop = pEditor->DoProperties(&View, aProps, s_aIds, &NewVal); if(Prop != -1) { - pEditor->m_Map.m_Modified = true; + pEditor->m_Map.OnModify(); } const float OffsetX = i2fx(NewVal) - pCurrentQuad->m_aPoints[4].x; @@ -988,7 +988,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, CLayerSounds *pLayer = (CLayerSounds *)pEditor->GetSelectedLayerType(0, LAYERTYPE_SOUNDS); if(pLayer) { - pEditor->m_Map.m_Modified = true; + pEditor->m_Map.OnModify(); pLayer->m_vSources.erase(pLayer->m_vSources.begin() + pEditor->m_SelectedSource); pEditor->m_SelectedSource--; } @@ -1062,7 +1062,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, int Prop = pEditor->DoProperties(&View, aProps, s_aIds, &NewVal); if(Prop != -1) { - pEditor->m_Map.m_Modified = true; + pEditor->m_Map.OnModify(); } if(Prop == PROP_POS_X) @@ -1145,7 +1145,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, Prop = pEditor->DoProperties(&View, aCircleProps, s_aCircleIds, &NewVal); if(Prop != -1) { - pEditor->m_Map.m_Modified = true; + pEditor->m_Map.OnModify(); } if(Prop == PROP_CIRCLE_RADIUS) @@ -1176,7 +1176,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, Prop = pEditor->DoProperties(&View, aRectangleProps, s_aRectangleIds, &NewVal); if(Prop != -1) { - pEditor->m_Map.m_Modified = true; + pEditor->m_Map.OnModify(); } if(Prop == PROP_RECTANGLE_WIDTH) @@ -1236,7 +1236,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupPoint(void *pContext, CUIRect View, int Prop = pEditor->DoProperties(&View, aProps, s_aIds, &NewVal); if(Prop != -1) { - pEditor->m_Map.m_Modified = true; + pEditor->m_Map.OnModify(); } for(auto &pQuad : vpQuads) diff --git a/src/game/variables.h b/src/game/variables.h index 42a17857e..f67d3ff6d 100644 --- a/src/game/variables.h +++ b/src/game/variables.h @@ -90,6 +90,8 @@ MACRO_CONFIG_INT(ClDyncamFollowFactor, cl_dyncam_follow_factor, 60, 0, 200, CFGF MACRO_CONFIG_INT(ClDyncamSmoothness, cl_dyncam_smoothness, 0, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Transition amount of the camera movement, 0=instant, 100=slow and smooth") MACRO_CONFIG_INT(ClDyncamStabilizing, cl_dyncam_stabilizing, 0, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Amount of camera slowdown during fast cursor movement. High value can cause delay in camera movement") +MACRO_CONFIG_INT(EdAutosaveInterval, ed_autosave_interval, 10, 0, 240, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Interval in minutes at which a copy of the current editor map is automatically saved to the 'auto' folder (0 for off)") +MACRO_CONFIG_INT(EdAutosaveMax, ed_autosave_max, 10, 0, 1000, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Maximum number of autosaves that are kept per map name (0 = no limit)") MACRO_CONFIG_INT(EdSmoothZoomTime, ed_smooth_zoom_time, 250, 0, 5000, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Time of smooth zoom animation in the editor in ms (0 for off)") MACRO_CONFIG_INT(EdLimitMaxZoomLevel, ed_limit_max_zoom_level, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Specifies, if zooming in the editor should be limited or not (0 = no limit)") MACRO_CONFIG_INT(EdZoomTarget, ed_zoom_target, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Zoom to the current mouse target")