From 1810d972f457f24af9a800dd275365703fa0d3b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Fri, 23 Jun 2023 17:39:05 +0200 Subject: [PATCH 1/4] Autosave copy of current editor map periodically to `auto` folder A copy of the map currently open in the editor is saved every 10 minutes to the `maps/auto` folder (interval configurable, see below). The automatically saved map uses the filename of the original map with an additional timestamp. Per map name 10 autosaves are kept in the `auto` folder before old autosaves will be deleted (number configurable, see below). Add config variable `ed_autosave_interval` (0 - 240, default 10) to configure the interval in minutes at which a copy of the current editor map is automatically saved to the 'auto' folder. Add config variable `ed_autosave_max` (0 - 1000, default 10) to configure the maximum number of autosaves that are kept per map name (0 = no limit). Autosaving will not take place in the 5 seconds immediately after the map was last modified by the user, to avoid interrupting the user with the autosave. This will only delay autosaving for up to 1 minute though, so autosaves are not prevented entirely, should the user continuously edit the map. When the editor is reopened after being closed for more than 10 seconds, the autosave timer will be adjusted to compensate for the time that was not spent on editing in the editor. When the map is saved manually by the user the autosave file is also updated, if it's outdated by at least half of the configured autosave interval. This ensures that autosaves are always available as a periodic backup of the map. When a copy of the current map is saved, this does not update the autosave and will also no longer reset the modified state. The modified state should reflect whether changes have been made that are not saved to the current map file. As saving a copy does not update the current file, the modified state should not be reset in this case. Closes #6693. --- src/engine/shared/storage.cpp | 1 + src/game/editor/auto_map.cpp | 2 +- src/game/editor/editor.cpp | 134 ++++++++++++++++++++++++++----- src/game/editor/editor.h | 24 ++++-- src/game/editor/io.cpp | 3 + src/game/editor/layer_quads.cpp | 12 +-- src/game/editor/layer_sounds.cpp | 6 +- src/game/editor/layer_tiles.cpp | 2 +- src/game/editor/popups.cpp | 30 +++---- src/game/variables.h | 2 + 10 files changed, 160 insertions(+), 56 deletions(-) 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 d855b71fa..c3cab662e 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -207,7 +208,7 @@ void CLayerGroup::Render() void CLayerGroup::AddLayer(CLayer *pLayer) { - m_pMap->m_Modified = true; + m_pMap->OnModify(); m_vpLayers.push_back(pLayer); } @@ -217,7 +218,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) @@ -228,7 +229,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 @@ -252,7 +253,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; } @@ -828,7 +829,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")) { @@ -836,25 +837,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")) { @@ -864,7 +875,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; } @@ -1524,7 +1534,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; @@ -3817,7 +3827,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) m_SelectedGroup = GroupAfterDraggedLayer - 1; m_vSelectedLayers.clear(); m_vSelectedQuads.clear(); - m_Map.m_Modified = true; + m_Map.OnModify(); } } @@ -3834,7 +3844,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) @@ -5344,7 +5354,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); } @@ -5353,7 +5363,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); } @@ -5362,7 +5372,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); } @@ -5473,7 +5483,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(); } } } @@ -5578,7 +5588,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; @@ -5757,7 +5767,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; @@ -5787,7 +5797,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; @@ -6507,6 +6517,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; @@ -6625,12 +6639,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) @@ -6651,7 +6672,7 @@ void CEditorMap::SwapEnvelopes(int Index0, int Index1) if(Index0 == Index1) return; - m_Modified = true; + OnModify(); VisitEnvelopeReferences([Index0, Index1](int &ElementIndex) { if(ElementIndex == Index0) @@ -6731,6 +6752,7 @@ void CEditorMap::Clean() m_pGameGroup = nullptr; m_Modified = false; + m_ModifiedAuto = false; m_pTeleLayer = nullptr; m_pSpeedupLayer = nullptr; @@ -6856,7 +6878,6 @@ void CEditor::Init() m_Brush.m_pMap = &m_Map; Reset(false); - m_Map.m_Modified = false; ResetMenuBackgroundPositions(); m_vpMenuBackgroundPositionNames.resize(CMenuBackground::NUM_POS); @@ -6966,6 +6987,74 @@ 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::OnUpdate() { CUIElementBase::Init(UI()); // update static pointer because game and editor use separate UI @@ -6977,6 +7066,7 @@ void CEditor::OnUpdate() } HandleCursorMovement(); + HandleAutosave(); } void CEditor::OnRender() diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index 18228ae9b..b8f25e61b 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -315,7 +315,6 @@ class CEditorMap public: CEditor *m_pEditor; - bool m_Modified; CEditorMap() { @@ -327,6 +326,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 +376,7 @@ public: CEnvelope *NewEnvelope(int Channels) { - m_Modified = true; + OnModify(); CEnvelope *pEnv = new CEnvelope(Channels); m_vpEnvelopes.push_back(pEnv); return pEnv; @@ -383,7 +389,7 @@ public: CLayerGroup *NewGroup() { - m_Modified = true; + OnModify(); CLayerGroup *pGroup = new CLayerGroup; pGroup->m_pMap = this; m_vpGroups.push_back(pGroup); @@ -398,7 +404,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 +413,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); } @@ -850,6 +856,8 @@ public: void ResetMentions() override { m_Mentions = 0; } void HandleCursorMovement(); + void HandleAutosave(); + bool PerformAutosave(); CLayerGroup *m_apSavedBrushes[10]; diff --git a/src/game/editor/io.cpp b/src/game/editor/io.cpp index 54d4bff60..b0c58407b 100644 --- a/src/game/editor/io.cpp +++ b/src/game/editor/io.cpp @@ -976,6 +976,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") From 525c33f1b89e1aaf7c2a865847e81acb1dcfb1a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Mon, 26 Jun 2023 21:25:30 +0200 Subject: [PATCH 2/4] Extract `CEditor::RenderPressedKeys` function --- src/game/editor/editor.cpp | 42 +++++++++++++++++++++----------------- src/game/editor/editor.h | 2 ++ 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index c3cab662e..40e6c1ab1 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -6440,25 +6440,7 @@ 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); - - int NKeys = 0; - for(int i = 0; i < KEY_LAST; i++) - { - if(Input()->KeyIsPressed(i)) - { - if(NKeys) - TextRender()->TextEx(&Cursor, " + ", -1); - TextRender()->TextEx(&Cursor, Input()->KeyName(i), -1); - NKeys++; - } - } - } + RenderPressedKeys(View); if(m_ShowMousePointer) { @@ -6477,6 +6459,28 @@ void CEditor::Render() } } +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(NKeys) + TextRender()->TextEx(&Cursor, " + ", -1); + TextRender()->TextEx(&Cursor, Input()->KeyName(i), -1); + NKeys++; + } + } +} + void CEditor::Reset(bool CreateDefault) { m_Map.Clean(); diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index b8f25e61b..e1912b701 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -876,6 +876,8 @@ public: void LoadCurrentMap(); void Render(); + void RenderPressedKeys(CUIRect View); + void ResetMenuBackgroundPositions(); std::vector GetSelectedQuads(); From 5c3e5bf67c79a6679124281b25af64499a741b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Mon, 26 Jun 2023 21:27:29 +0200 Subject: [PATCH 3/4] Extract `CEditor::RenderMousePointer` function --- src/game/editor/editor.cpp | 33 +++++++++++++++++---------------- src/game/editor/editor.h | 1 + 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index 40e6c1ab1..761d78a87 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -6441,22 +6441,7 @@ void CEditor::Render() RenderStatusbar(StatusBar); RenderPressedKeys(View); - - 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(); - } + RenderMousePointer(); } void CEditor::RenderPressedKeys(CUIRect View) @@ -6481,6 +6466,22 @@ void CEditor::RenderPressedKeys(CUIRect View) } } +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) { m_Map.Clean(); diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index e1912b701..d10dbc7f0 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -877,6 +877,7 @@ public: void Render(); void RenderPressedKeys(CUIRect View); + void RenderMousePointer(); void ResetMenuBackgroundPositions(); From 2126d8570fb9e12c2c904d78d51edb35b7b54415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Mon, 26 Jun 2023 20:48:16 +0200 Subject: [PATCH 4/4] Use separate thread to finish saving maps, add saving indicator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compressing the data with zlib takes the majority of the time when saving a datafile. Therefore, compressing is now delayed until the `CDataFileWriter::Finish` function is called. This function is then off-loaded to another thread to make saving maps in the editor not block the rendering. A message "Saving…" is shown in the bottom right of the editor view while a job to save a map is running in the background. While a map is being finished in a background thread another save for the same filename cannot be initiated to prevent multiples accesses to the same file. Closes #6762. --- src/engine/shared/datafile.cpp | 77 ++++++++++++++++++++++------------ src/engine/shared/datafile.h | 23 +++++++++- src/game/editor/editor.cpp | 49 ++++++++++++++++++++++ src/game/editor/editor.h | 52 ++++++++++++++++------- src/game/editor/io.cpp | 28 ++++--------- 5 files changed, 166 insertions(+), 63 deletions(-) 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/game/editor/editor.cpp b/src/game/editor/editor.cpp index 761d78a87..4dc6e0255 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -6441,6 +6441,7 @@ void CEditor::Render() RenderStatusbar(StatusBar); RenderPressedKeys(View); + RenderSavingIndicator(View); RenderMousePointer(); } @@ -6466,6 +6467,17 @@ void CEditor::RenderPressedKeys(CUIRect View) } } +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) @@ -6857,6 +6869,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(); @@ -7060,6 +7073,41 @@ bool CEditor::PerformAutosave() } } +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 @@ -7072,6 +7120,7 @@ void CEditor::OnUpdate() HandleCursorMovement(); HandleAutosave(); + HandleWriterFinishJobs(); } void CEditor::OnRender() diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index d10dbc7f0..c66884d0e 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 @@ -691,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; @@ -728,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; } @@ -738,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(); @@ -858,6 +878,7 @@ public: void HandleCursorMovement(); void HandleAutosave(); bool PerformAutosave(); + void HandleWriterFinishJobs(); CLayerGroup *m_apSavedBrushes[10]; @@ -877,6 +898,7 @@ public: void Render(); void RenderPressedKeys(CUIRect View); + void RenderSavingIndicator(CUIRect View); void RenderMousePointer(); void ResetMenuBackgroundPositions(); @@ -1131,6 +1153,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 b0c58407b..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; }