1 // Copyright 2001-2018 Crytek GmbH / Crytek Group. All rights reserved.
4 #include "EditorFileMonitor.h"
5 #include <CryCore/ToolsHelpers/ResourceCompilerHelper.h>
6 #include "CrySystem/IProjectManager.h"
7 #include "GameEngine.h"
8 #include "Include/IAnimationCompressionManager.h"
9 #include <CryString/StringUtils.h>
10 #include <CrySystem/IProjectManager.h>
12 #include "FilePathUtil.h"
14 //////////////////////////////////////////////////////////////////////////
15 CEditorFileMonitor::CEditorFileMonitor()
17 GetIEditorImpl()->RegisterNotifyListener(this);
20 //////////////////////////////////////////////////////////////////////////
21 CEditorFileMonitor::~CEditorFileMonitor()
23 CFileChangeMonitor::DeleteInstance();
26 //////////////////////////////////////////////////////////////////////////
27 void CEditorFileMonitor::OnEditorNotifyEvent(EEditorNotifyEvent ev
)
29 if (ev
== eNotify_OnInit
)
31 // Setup file change monitoring
32 gEnv
->pSystem
->SetIFileChangeMonitor(this);
34 // We don't want the file monitor to be enabled while
36 if (!GetIEditorImpl()->IsInConsolewMode())
39 CFileChangeMonitor::Instance()->Subscribe(this);
41 else if (ev
== eNotify_OnQuit
)
43 gEnv
->pSystem
->SetIFileChangeMonitor(NULL
);
44 CFileChangeMonitor::Instance()->StopMonitor();
45 GetIEditorImpl()->UnregisterNotifyListener(this);
49 //////////////////////////////////////////////////////////////////////////
50 bool CEditorFileMonitor::RegisterListener(IFileChangeListener
* pListener
, const char* sMonitorItem
)
52 return RegisterListener(pListener
, sMonitorItem
, "*");
55 //////////////////////////////////////////////////////////////////////////
56 static string
CanonicalizePath(const char* szPath
)
58 std::vector
<char> canonicalizedPath(strlen(szPath
) + 1, '\0');
59 if (PathCanonicalize(&canonicalizedPath
[0], szPath
))
60 return string(&canonicalizedPath
[0]);
62 return string(szPath
);
65 //////////////////////////////////////////////////////////////////////////
66 static string
GetAbsolutePathOfProjectFolder(const char* szPath
)
68 const char* szProjectRoot
= GetISystem()->GetIProjectManager()->GetCurrentProjectDirectoryAbsolute();
69 CryPathString path
= PathUtil::Make(szProjectRoot
, szPath
);
70 path
= PathUtil::AddBackslash(path
);
71 path
= PathUtil::ToDosPath(path
);
72 return CanonicalizePath(path
.c_str());
75 //////////////////////////////////////////////////////////////////////////
76 // TODO: Change the initialization order to call MonitorDirectories() before any of CEditorFileMonitor::RegisterListener
77 void CEditorFileMonitor::MonitorDirectories()
80 // NOTE: Instead of monitoring each sub-directory we monitor the whole root
81 // folder. This is needed since if the sub-directory does not exist when
82 // we register it it will never get monitored properly.
83 CFileChangeMonitor::Instance()->MonitorItem(GetAbsolutePathOfProjectFolder(PathUtil::GetGameFolder()));
86 for (int index
= 0;; index
++)
88 const char* sModPath
= gEnv
->pCryPak
->GetMod(index
);
91 CFileChangeMonitor::Instance()->MonitorItem(GetAbsolutePathOfProjectFolder(sModPath
));
94 // Add editor directory for scripts
95 CFileChangeMonitor::Instance()->MonitorItem(GetAbsolutePathOfProjectFolder("Editor"));
97 // Add shader source folder
98 CFileChangeMonitor::Instance()->MonitorItem(GetAbsolutePathOfProjectFolder("Engine/Shaders"));
101 //////////////////////////////////////////////////////////////////////////
102 bool CEditorFileMonitor::RegisterListener(IFileChangeListener
* pListener
, const char* szFolderRelativeToGame
, const char* sExtension
)
104 m_vecFileChangeCallbacks
.push_back(SFileChangeCallback(pListener
, PathUtil::ToUnixPath(szFolderRelativeToGame
), sExtension
));
106 // NOTE: Here we do not register any CFileChangeMonitor::Instance()->MonitorItem(path).
107 // See CEditorFileMonitor::MonitorDirectories().
111 bool CEditorFileMonitor::UnregisterListener(IFileChangeListener
* pListener
)
115 // Note that we remove the listener, but we don't currently remove the monitored item
116 // from the file monitor. This is fine, but inefficient
118 std::vector
<SFileChangeCallback
>::iterator iter
= m_vecFileChangeCallbacks
.begin();
119 while (iter
!= m_vecFileChangeCallbacks
.end())
121 if (iter
->pListener
== pListener
)
123 iter
= m_vecFileChangeCallbacks
.erase(iter
);
135 //////////////////////////////////////////////////////////////////////////
136 static bool IsFilenameEndsWithDotDaeDotZip(const char* fln
)
138 size_t len
= strlen(fln
);
142 if (stricmp(fln
+ len
- 8, ".dae.zip") == 0)
148 //////////////////////////////////////////////////////////////////////////
149 static bool RecompileColladaFile(const char* path
)
151 string pathWithGameFolder
= PathUtil::ToUnixPath(PathUtil::AddSlash(PathUtil::GetGameFolder())) + string(path
);
152 if (CResourceCompilerHelper::CallResourceCompiler(
153 pathWithGameFolder
.c_str(), "/refresh", NULL
, false, CResourceCompilerHelper::eRcExePath_editor
, true, true, L
".")
154 != CResourceCompilerHelper::eRcCallResult_success
)
160 //////////////////////////////////////////////////////////////////////////
161 static const char* AbsoluteToProjectPath(const char* szAbsolutePath
)
163 if (!GetISystem()->GetIPak()->IsAbsPath(szAbsolutePath
))
165 return szAbsolutePath
;
168 const string projectPath
= PathUtil::AddSlash(gEnv
->pSystem
->GetIProjectManager()->GetCurrentProjectDirectoryAbsolute());
169 const string fixedPath
= PathUtil::ToUnixPath(szAbsolutePath
);
171 if (cry_strnicmp(fixedPath
.c_str(), projectPath
.c_str(), projectPath
.length()) == 0) // path starts with rootPathStr
173 return szAbsolutePath
+ projectPath
.length();
178 //////////////////////////////////////////////////////////////////////////
179 const char* GetPathRelativeToModFolder(const char* szAbsolutePath
)
181 if (szAbsolutePath
[0] == '\0')
182 return szAbsolutePath
;
184 if (_strnicmp("engine", szAbsolutePath
, 6) == 0 && (szAbsolutePath
[6] == '\\' || szAbsolutePath
[6] == '/'))
185 return szAbsolutePath
;
187 szAbsolutePath
= AbsoluteToProjectPath(szAbsolutePath
);
189 string gameFolder
= PathUtil::GetGameFolder();
191 const char* modFolder
= gameFolder
.c_str();
195 if (_strnicmp(modFolder
, szAbsolutePath
, strlen(modFolder
)) == 0)
197 const char* result
= szAbsolutePath
+ strlen(modFolder
);
198 if (*result
== '\\' || *result
== '/')
203 modFolder
= gEnv
->pCryPak
->GetMod(modIndex
);
206 while (modFolder
!= 0);
211 ///////////////////////////////////////////////////////////////////////////
213 // Called when file monitor message is received
214 void CEditorFileMonitor::OnFileMonitorChange(const SFileChangeInfo
& rChange
)
216 CCryEditApp
* app
= CCryEditApp::GetInstance();
217 if (app
== NULL
|| app
->IsExiting())
221 if (QDir(QString::fromLocal8Bit(rChange
.filename
)).exists())
226 // Process updated file.
227 // Make file relative to MasterCD folder.
228 string filename
= rChange
.filename
;
230 // Make sure there is no leading slash
231 if (!filename
.IsEmpty() && filename
[0] == '\\' || filename
[0] == '/')
232 filename
= filename
.Mid(1);
234 if (!filename
.IsEmpty())
236 // Make it relative to the game folder
237 const string filenameRelGame
= GetPathRelativeToModFolder(filename
.GetString());
239 const string ext
= filename
.Right(filename
.GetLength() - filename
.ReverseFind('.') - 1);
241 // Check for File Monitor callback
242 std::vector
<SFileChangeCallback
>::iterator iter
;
243 for (iter
= m_vecFileChangeCallbacks
.begin(); iter
!= m_vecFileChangeCallbacks
.end(); ++iter
)
245 SFileChangeCallback
& sCallback
= *iter
;
247 // We compare against length of callback string, so we get directory matches as well as full filenames
248 if (sCallback
.pListener
)
250 if (sCallback
.extension
== "*" || stricmp(ext
, sCallback
.extension
) == 0)
252 if (!filenameRelGame
.empty() && _strnicmp(filenameRelGame
, sCallback
.item
, sCallback
.item
.GetLength()) == 0)
254 sCallback
.pListener
->OnFileChange(filenameRelGame
, IFileChangeListener::EChangeType(rChange
.changeType
));
256 else if (_strnicmp(filename
, sCallback
.item
, sCallback
.item
.GetLength()) == 0)
258 sCallback
.pListener
->OnFileChange(filename
, IFileChangeListener::EChangeType(rChange
.changeType
));
264 //TODO: have all these file types encapsulated in some IFileChangeFileTypeHandler to deal with each of them in a more generic way
265 bool isCAF
= stricmp(ext
, "caf") == 0;
266 bool isLMG
= (stricmp(ext
, "lmg") == 0) || (stricmp(ext
, "bspace") == 0) || (stricmp(ext
, "comb") == 0);
268 bool isExportLog
= stricmp(ext
, "exportlog") == 0;
269 bool isRCDone
= stricmp(ext
, "rcdone") == 0;
270 bool isCOLLADA
= (stricmp(ext
, "dae") == 0 || IsFilenameEndsWithDotDaeDotZip(filename
.GetString()));
272 if (!isCAF
&& !isLMG
&& !isCOLLADA
&& !isExportLog
&& !isRCDone
)
274 // Updating an existing CGF file is often implemented as a DeleteFile + MoveFile pair.
275 // If the file listener triggers a refresh of a CStatObj after deletion and before moving,
276 // it will not find the CGF file, overwrite its path with a default file, and lose the
277 // reference to the original path. This causes objects in the level to disappear.
278 // Therefore, we ignore deletions of CGF objects.
279 if (rChange
.changeType
!= IFileChangeListener::eChangeType_Deleted
)
281 // Apart from reloading resources it also may trigger importing of tif files.
282 if (rChange
.changeType
!= IFileChangeListener::eChangeType_RenamedOldName
)
284 GetIEditorImpl()->GetGameEngine()->ReloadResourceFile(filenameRelGame
);
287 if (stricmp(ext
, "cgf") == 0)
289 CBaseObjectsArray objects
;
290 GetIEditorImpl()->GetObjectManager()->GetObjects(objects
);
291 for (CBaseObject
* pObj
: objects
)
293 pObj
->InvalidateGeometryFile(filenameRelGame
);
300 // Make a corresponding .cgf path.
302 int nameLength
= filename
.GetLength();
304 cgfFileName
= filename
;
305 if (stricmp(ext
, "dae") == 0)
307 cgfFileName
.SetAt(nameLength
- 3, 'c');
308 cgfFileName
.SetAt(nameLength
- 2, 'g');
309 cgfFileName
.SetAt(nameLength
- 1, 'f');
313 cgfFileName
.SetAt(nameLength
- 7, 'c');
314 cgfFileName
.SetAt(nameLength
- 6, 'g');
315 cgfFileName
.SetAt(nameLength
- 5, 'f');
316 cgfFileName
.SetAt(nameLength
- 4, 0);
318 IStatObj
* pStatObjectToReload
= GetIEditorImpl()->Get3DEngine()->FindStatObjectByFilename(cgfFileName
.GetBuffer());
320 // If the corresponding .cgf file exists, recompile the changed COLLADA file.
321 if (pStatObjectToReload
)
323 CryLog("Recompile DAE file: %s", (LPCTSTR
)filename
);
324 RecompileColladaFile(filename
.GetString());
329 ICharacterManager
* pICharacterManager
= GetISystem()->GetIAnimationSystem();
330 stack_string strPath
= filenameRelGame
;
331 PathUtil::UnifyFilePath(strPath
);
335 CryLog("Reload blendspace file: %s", (LPCTSTR
)strPath
);
336 pICharacterManager
->ReloadLMG(strPath
.c_str());
340 // Set this flag to make sure that the viewport will update at least once,
341 // so that the changes will be shown, even if the app does not have focus.
342 CCryEditApp::GetInstance()->ForceNextIdleProcessing();