using System; using System.Collections.Generic; using System.IO; using System.Threading; using Robust.Client.Graphics; using Robust.Shared; using Robust.Shared.Asynchronous; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Utility; using Timer = Robust.Shared.Timing.Timer; namespace Robust.Client.Utility; internal sealed partial class ReloadManager : IReloadManager { [Dependency] private IClyde _clyde = default!; [Dependency] private IConfigurationManager _cfg = default!; [Dependency] private ILogManager _logMan = default!; [Dependency] private IResourceManagerInternal _res = default!; #if TOOLS [Dependency] private ITaskManager _tasks = default!; #endif private readonly TimeSpan _reloadDelay = TimeSpan.FromMilliseconds(10); private CancellationTokenSource _reloadToken = new(); private readonly HashSet _reloadQueue = new(); private List _watchers = new(); // this list is never used but needed to prevent them from being garbage collected public event Action? OnChanged; private ISawmill _sawmill = default!; public void Initialize() { _sawmill = _logMan.GetSawmill("reload"); _clyde.OnWindowFocused += WindowFocusedChanged; } private void WindowFocusedChanged(WindowFocusedEventArgs args) { #if TOOLS if (args.Focused && _reloadQueue.Count > 0) { Timer.Spawn(_reloadDelay, ReloadFiles, _reloadToken.Token); } else { _reloadToken.Cancel(); _reloadToken = new CancellationTokenSource(); } #endif } private void ReloadFiles() { foreach (var file in _reloadQueue) { if (!_res.ContentFileExists(file)) continue; _sawmill.Info($"Reloading {file}"); OnChanged?.Invoke(file); } _reloadQueue.Clear(); } public void Register(ResPath directory, string filter) { Register(directory.ToRelativeSystemPath(), filter); } public void Register(string directory, string filter) { if (!_cfg.GetCVar(CVars.ResPrototypeReloadWatch)) return; #if TOOLS foreach (var root in _res.GetContentRoots()) { var path = Path.Join(root, directory); if (!Directory.Exists(path)) { continue; } var watcher = new FileSystemWatcher(path, filter) { IncludeSubdirectories = true, NotifyFilter = NotifyFilters.LastWrite }; _watchers.Add(watcher); // prevent garbage collection watcher.Changed += OnWatch; try { watcher.EnableRaisingEvents = true; } catch (IOException ex) { _sawmill.Error($"Watching resources in path {path} threw an exception:\n{ex}"); } } void OnWatch(object sender, FileSystemEventArgs args) { switch (args.ChangeType) { case WatcherChangeTypes.Renamed: case WatcherChangeTypes.Deleted: return; case WatcherChangeTypes.Created: // case WatcherChangeTypes.Deleted: case WatcherChangeTypes.Changed: case WatcherChangeTypes.All: break; default: throw new ArgumentOutOfRangeException(); } _tasks.RunOnMainThread(() => { foreach (var rootIter in _res.GetContentRoots()) { var relPath = Path.GetRelativePath(rootIter, args.FullPath); if (relPath == args.FullPath) { // Different root (i.e., "C:/" and "D:/") continue; } var file = ResPath.FromRelativeSystemPath(relPath).ToRootedPath(); if (!file.CanonPath.Contains("/../")) _reloadQueue.Add(file); } }); } #endif } }