Polyphase Game Engine
Loading...
Searching...
No Matches
FileWatcher.h
Go to the documentation of this file.
1#pragma once
2
3#include "EngineTypes.h"
4
5#if !defined(POLYPHASE_PLATFORM_ADDON)
6
7#include <string>
8#include <vector>
9#include <functional>
10#include <thread>
11#include <atomic>
12#include <unordered_map>
13#include <mutex>
14
15enum class FileAction
16{
17 Added,
19 Removed,
21};
22
24{
25 std::string filePath;
27 std::string oldPath; // For rename events
28};
29
30using FileChangeCallback = std::function<void(const FileChangeEvent&)>;
31
32// ---------------------------------------------------------------------------
33// Portable polling file watcher.
34//
35// A worker thread walks the watched directories every kPollIntervalMs and
36// diffs each file's mtime against a snapshot; differences become events that
37// the main thread drains in Update(). This deliberately uses only stat() and
38// the SYS_* directory API, so it behaves identically on every editor-capable
39// platform (Windows, Linux, and anything added later) with no per-platform
40// code. The previous implementation was Windows-only — ReadDirectoryChangesW
41// over an IOCP — which meant script hot-reload silently did nothing on the
42// Linux editor build, and which stored the in-flight OVERLAPPED/buffer inside
43// a std::vector whose reallocation handed the kernel dangling pointers.
44//
45// The cost is that a change is picked up within a poll interval rather than
46// instantly. For a Scripts/ tree of a few hundred .lua files that walk is
47// negligible, and sub-second latency is imperceptible in an edit/save loop.
48// ---------------------------------------------------------------------------
50{
51public:
54
55 // Initialize the file watcher
56 bool Initialize();
57
58 // Shutdown the file watcher
59 void Shutdown();
60
61 // Add a directory to watch. `extensions` filters which files emit events
62 // (case-sensitive, with leading dot); the default preserves the historical
63 // .lua-only behavior for every existing call site. Empty list = all files.
64 bool WatchDirectory(const std::string& directory, bool recursive = true,
65 const std::vector<std::string>& extensions = { ".lua" });
66
67 // Remove a directory from watching
68 void UnwatchDirectory(const std::string& directory);
69
70 // Drop every watch at once. Called on project close so the next project's
71 // watches don't stack on top of the previous one's.
72 void UnwatchAll();
73
74 // Set callback for file change events
76
77 // Update function to process events (called from main thread)
78 void Update();
79
80 // Enable/disable the file watcher
81 void SetEnabled(bool enabled);
82 bool IsEnabled() const { return mEnabled; }
83
84private:
85 struct WatchDir
86 {
87 std::string path; // always ends in '/'
88 bool recursive = true;
89 std::vector<std::string> extensions; // empty = every file
90 };
91
92 struct FileSnapshot
93 {
94 int64_t lastSeenTime = 0; // mtime observed by the most recent scan
95 int64_t lastEmittedTime = 0; // mtime of the last change we dispatched
96 bool seenThisScan = false; // scan mark used to detect deletions
97 };
98
99 void WatcherThread();
100 void ProcessEvents();
101
102 // Worker-thread only. Walks a watched root, marking and diffing snapshots.
103 void ScanDirRecursive(const std::string& dir, bool recursive, const std::vector<std::string>& extensions,
104 std::vector<FileChangeEvent>& outEvents, bool rebaseline, uint32_t depth = 0);
105 void QueueEvents(const std::vector<FileChangeEvent>& events);
106
107 std::thread mWatcherThread;
108 std::atomic<bool> mRunning;
109 std::atomic<bool> mEnabled;
110
111 // Set when the watch set changes or the watcher is re-enabled: the next
112 // scan records mtimes without emitting, so re-enabling hot-reload doesn't
113 // dispatch a reload for every file edited while it was switched off.
114 std::atomic<bool> mNeedsRebaseline;
115
116 FileChangeCallback mCallback;
117
118 std::vector<WatchDir> mWatchDirs;
119 std::mutex mWatchMutex;
120
121 std::vector<FileChangeEvent> mPendingEvents;
122 std::mutex mEventsMutex;
123
124 // Worker-thread private: absolute path -> observed mtimes.
125 std::unordered_map<std::string, FileSnapshot> mSnapshots;
126};
127
128// Global file watcher instance
130void CreateFileWatcher();
131void DestroyFileWatcher();
132
133#else // POLYPHASE_PLATFORM_ADDON
134
135// ---------------------------------------------------------------------------
136// Addon-platform stub. The full FileWatcher uses std::mutex / std::thread /
137// std::atomic — none of which are available in embedded libstdc++ builds
138// like MARSDEV's m68k-elf. Hot-reload makes no sense on a ROM cart anyway,
139// so we provide a header-only no-op stub. Engine.cpp's `if (GetFileWatcher())`
140// checks already gate every real call site, and they all become inert when
141// GetFileWatcher() returns null.
142// ---------------------------------------------------------------------------
143
144#include <string>
145#include <vector>
146
147enum class FileAction { Added, Modified, Removed, Renamed };
148
149struct FileChangeEvent
150{
151 std::string filePath;
153 std::string oldPath;
154};
155
156// Trivial callback type — no <functional>/std::function so we don't drag
157// any extra STL into the addon build. Engine code passes a free function
158// pointer at the only call site (OnScriptFileChanged in Engine.cpp).
159using FileChangeCallback = void(*)(const FileChangeEvent&);
160
161class FileWatcher
162{
163public:
164 bool Initialize() { return false; }
165 void Shutdown() {}
166 bool WatchDirectory(const std::string&, bool = true,
167 const std::vector<std::string>& = {}) { return false; }
168 void UnwatchDirectory(const std::string&) {}
169 void UnwatchAll() {}
171 void Update() {}
172 void SetEnabled(bool) {}
173 bool IsEnabled() const { return false; }
174};
175
176// Declared here, defined in the matching #else block in FileWatcher.cpp so
177// header inline + .cpp body don't clash at link time.
179void CreateFileWatcher();
180void DestroyFileWatcher();
181
182#endif // POLYPHASE_PLATFORM_ADDON
void CreateFileWatcher()
Definition FileWatcher.cpp:66
std::function< void(const FileChangeEvent &)> FileChangeCallback
Definition FileWatcher.h:30
FileWatcher * GetFileWatcher()
Definition FileWatcher.cpp:61
void DestroyFileWatcher()
Definition FileWatcher.cpp:74
FileAction
Definition FileWatcher.h:16
Definition FileWatcher.h:50
FileWatcher()
Definition FileWatcher.cpp:83
~FileWatcher()
Definition FileWatcher.cpp:90
void SetFileChangeCallback(FileChangeCallback callback)
Definition FileWatcher.cpp:234
void UnwatchDirectory(const std::string &directory)
Definition FileWatcher.cpp:195
void UnwatchAll()
Definition FileWatcher.cpp:217
void SetEnabled(bool enabled)
Definition FileWatcher.cpp:247
bool WatchDirectory(const std::string &directory, bool recursive=true, const std::vector< std::string > &extensions={ ".lua" })
Definition FileWatcher.cpp:153
void Shutdown()
Definition FileWatcher.cpp:109
bool Initialize()
Definition FileWatcher.cpp:95
void Update()
Definition FileWatcher.cpp:239
bool IsEnabled() const
Definition FileWatcher.h:82
Definition FileWatcher.h:24
std::string filePath
Definition FileWatcher.h:25
std::string oldPath
Definition FileWatcher.h:27
FileAction action
Definition FileWatcher.h:26