6763: Autosave copy of current editor map periodically to `auto` folder, use separate thread to finish saving maps, add saving indicator r=def- a=Robyt3

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.

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.

## Checklist

- [X] Tested the change ingame
- [ ] Provided screenshots if it is a visual change
- [X] Tested in combination with possibly related configuration options
- [X] Written a unit test (especially base/) or added coverage to integration test
- [ ] Considered possible null pointers and out of bounds array indexing
- [X] Changed no physics that affect existing maps
- [ ] Tested the change with [ASan+UBSan or valgrind's memcheck](https://github.com/ddnet/ddnet/#using-addresssanitizer--undefinedbehavioursanitizer-or-valgrinds-memcheck) (optional)


Co-authored-by: Robert Müller <robytemueller@gmail.com>
This commit is contained in:
bors[bot] 2023-06-26 21:13:01 +00:00 committed by GitHub
commit 5d89b975be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 364 additions and 149 deletions

View file

@ -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;
}

View file

@ -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

View file

@ -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);

View file

@ -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)
{

View file

@ -17,6 +17,7 @@
#include <engine/input.h>
#include <engine/keys.h>
#include <engine/shared/config.h>
#include <engine/shared/filecollection.h>
#include <engine/storage.h>
#include <engine/textrender.h>
@ -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<CEditor *>(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<CEditor *>(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<IClient>();
m_pConfig = Kernel()->RequestInterface<IConfigManager>()->Values();
m_pConsole = Kernel()->RequestInterface<IConsole>();
m_pEngine = Kernel()->RequestInterface<IEngine>();
m_pGraphics = Kernel()->RequestInterface<IGraphics>();
m_pTextRender = Kernel()->RequestInterface<ITextRender>();
m_pStorage = Kernel()->RequestInterface<IStorage>();
@ -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<CDataFileWriterFinishJob> 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()

View file

@ -12,11 +12,15 @@
#include <game/mapitems_ex.h>
#include <engine/editor.h>
#include <engine/engine.h>
#include <engine/graphics.h>
#include <engine/shared/datafile.h>
#include <engine/shared/jobs.h>
#include "auto_map.h"
#include <chrono>
#include <list>
#include <string>
#include <vector>
@ -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<CLayerGroup *> m_vpGroups;
std::vector<CEditorImage *> m_vpImages;
std::vector<CEnvelope *> 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<CQuad *> GetSelectedQuads();
@ -1121,6 +1154,8 @@ public:
static const void *ms_pUiGotContext;
CEditorMap m_Map;
std::list<std::shared_ptr<CDataFileWriterFinishJob>> m_lpWriterFinishJobs;
int m_ShiftBy;
static void EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Channels, void *pUser);

View file

@ -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<CDataFileWriterFinishJob> &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<CDataFileWriterFinishJob> pWriterFinishJob = std::make_shared<CDataFileWriterFinishJob>(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;
}

View file

@ -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;
}

View file

@ -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)

View file

@ -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);

View file

@ -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)

View file

@ -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")