Polyphase Game Engine
Loading...
Searching...
No Matches
NativeAddonManager.h
Go to the documentation of this file.
1#pragma once
2
8#if EDITOR
9
13
14#include <string>
15#include <unordered_map>
16#include <unordered_set>
17#include <vector>
18#include <thread>
19#include <atomic>
20#include <mutex>
21#include <memory>
22
26struct NativeAddonCreateInfo
27{
28 std::string mName; // Display name (e.g., "My Addon")
29 std::string mId; // Internal ID (e.g., "my-addon", auto-generated from name if empty)
30 std::string mAuthor;
31 std::string mDescription;
32 std::string mVersion = "1.0.0";
33 NativeAddonTarget mTarget = NativeAddonTarget::EngineAndEditor;
34 std::string mBinaryName; // Auto-generated from ID if empty
35};
36
40struct NativeAddonPackageOptions
41{
42 std::string mAddonId;
43 bool mIncludeSource = true;
44 bool mIncludeAssets = true;
45 bool mIncludeScripts = true;
46 bool mIncludeThumbnail = true;
47 std::string mOutputPath; // Full path to output zip file
48};
49
53struct NativeAddonState
54{
55 std::string mAddonId;
56 std::string mSourcePath; // Path to addon source (local Packages/ or cache)
57 std::string mLoadedPath; // Path to loaded DLL/SO
58 void* mModuleHandle = nullptr;
59 std::string mFingerprint; // Hash for rebuild detection
60
61 // Build state
62 bool mBuildInProgress = false;
63 bool mBuildSucceeded = false;
64 std::string mBuildLog;
65 std::string mBuildError;
66
67 // Plugin descriptor (after load)
68 PolyphasePluginDesc mDesc = {};
69 bool mDescValid = false;
70
71 // Native metadata from package.json
72 NativeModuleMetadata mNativeMetadata;
73
74 // Runtime resolve/load status
75 NativeAddonResolveMode mActiveResolveMode = NativeAddonResolveMode::Source;
76 bool mLoadedFromBinary = false;
77 std::string mBinaryStatus;
78
79 // UUIDs of assets that PurgeAssetsFromModule unloaded during the most
80 // recent UnloadNativeAddon. LoadNativeAddon drains this on its next
81 // successful load, calling LoadAsset on each so an addon-typed asset
82 // that was loaded before reload comes back loaded after — without this,
83 // the post-reload stub has mAsset=null and SaveAsset becomes a no-op.
84 std::vector<uint64_t> mPurgedAssetUuids;
85};
86
96class NativeAddonManager
97{
98public:
99 static void Create();
100 static void Destroy();
101 static NativeAddonManager* Get();
102
103 // ===== Discovery =====
104
108 void DiscoverNativeAddons();
109
113 std::vector<std::string> GetDiscoveredAddonIds() const;
114
115 // ===== Build Operations =====
116
124 bool BuildNativeAddon(const std::string& addonId, std::string& outError);
125
132 std::string ComputeFingerprint(const std::string& addonId);
133
140 bool NeedsBuild(const std::string& addonId);
141
142private:
144 void WriteAddonBuildMeta(const std::string& outputPath,
145 const std::string& fingerprint);
146
148 NativeAddonResolveMode ResolveModeForAddon(const std::string& addonId) const;
149
151 bool ResolveBinaryModulePath(const std::string& addonId, std::string& outModulePath, std::string& outStatus, std::string& outError);
152
154 bool IsBinaryDescriptorCompatible(const NativeBinaryDescriptor& descriptor, const NativeAddonState& state) const;
155
159 bool MetaIndicatesRebuildNeeded(const std::string& outputPath) const;
160public:
161
162 // ===== Load/Unload Operations =====
163
171 bool LoadNativeAddon(const std::string& addonId, std::string& outError);
172
179 bool UnloadNativeAddon(const std::string& addonId);
180
188 bool ReloadNativeAddon(const std::string& addonId, std::string& outError);
189
195 void ReloadAllNativeAddons();
196
197 // Force Rebuild was removed in favour of the per-addon Reload button +
198 // the per-project chokepoint. To force a fresh compile of all addons,
199 // call ReloadNativeAddonsWithProjectRestart({}, /*forceRebuild*/true, ...).
200
201 // ===== Async build state (drives the progress modal) =====
202
206 void TickAsyncBuilds();
207
209 bool IsBuildingAsync() const;
210
212 int GetAsyncBuildTotal() const;
213
215 int GetAsyncBuildIndex() const;
216
218 std::string GetAsyncBuildAddonId() const;
219
222 std::string GetAsyncBuildOutput() const;
223
224 // ===== Build-blocked state (locked intermediate files) =====
225 //
226 // Before a build runs, BuildNativeAddon / StartNextQueuedBuild sweep the
227 // addon's intermediate fingerprint dir and try to delete every file. If
228 // any file is locked (most commonly the .pdb held open by mspdbsrv.exe
229 // across DLL unload, producing LNK1201 at link time), the sweep records
230 // the offending paths and the build is paused. The editor surfaces a
231 // modal listing the locked files with Retry / Cancel — Retry re-sweeps
232 // and resumes if clean, Cancel abandons the operation.
233 struct BuildBlocked
234 {
235 bool mActive = false;
236 std::string mAddonId;
237 std::vector<std::string> mLockedFiles;
238 // Absolute path to <project>/Intermediate/Plugins/<addonId>/ — the
239 // simplest manual fix is to delete this entire directory. The modal
240 // surfaces this as a copy-paste shell command.
241 std::string mIntermediateDir;
242 };
243 bool IsBuildBlocked() const { return mBlocked.mActive; }
244 const BuildBlocked& GetBuildBlocked() const { return mBlocked; }
248 void RetryBlockedBuild();
250 void CancelBlockedBuild();
251
252 // ===== Project-restart reload chokepoint =====
253 //
254 // Native addon reload is unsafe when scenes are open — live nodes hold
255 // vtable pointers into the addon's mapped DLL pages and any unload
256 // invalidates them, plus Node factories get stripped from the global
257 // registry so a later scene reopen falls back to Node3D and silently
258 // corrupts the in-memory tree on save. The fix is to close the project
259 // entirely (saving dirty scenes first per user choice), unload every
260 // addon, rebuild, then reopen the project from disk so factories,
261 // assets, and scenes all rehydrate cleanly.
262 //
263 // The flow is staged across frames because the rebuild runs async on a
264 // worker thread. Phase advances:
265 // None
266 // -> AwaitingConfirm (one-shot confirm modal)
267 // -> AwaitingDirty (per-scene Save/Discard/Cancel — one popup at
268 // a time, advancing through mDirtyScenes)
269 // -> Building (project closed, builds in flight; advanced by
270 // TickAsyncBuilds when the queue drains)
271 // -> Reopening (synchronous OpenProject + scene restore; only
272 // held briefly for telemetry, then cleared)
273 // -> None
274 enum class ProjectRestartPhase
275 {
276 None,
277 AwaitingConfirm,
278 AwaitingDirty,
279 Building,
280 Reopening,
281 };
282
283 struct ProjectRestart
284 {
285 ProjectRestartPhase mPhase = ProjectRestartPhase::None;
286
287 // Addons being rebuilt. Empty = all installed enabled native addons.
288 std::vector<std::string> mTargetAddons;
289 // forceRebuild=true wipes each target's fingerprint dir before
290 // building so NeedsBuild() returns true even for an unchanged source.
291 bool mForceRebuild = false;
292 std::string mReason; // user-facing modal copy
293
294 // Snapshot — captured at restart entry, restored after OpenProject.
295 std::string mProjectPath;
296 std::vector<std::string> mOpenSceneNames; // names of edit scenes to reopen
297 std::string mActiveSceneName; // active edit scene at snapshot
298
299 // Per-scene dirty queue. Walked one-at-a-time during AwaitingDirty.
300 std::vector<std::string> mDirtyScenes;
301 int32_t mDirtyCursor = 0;
302 };
303
304 bool IsProjectRestartActive() const { return mRestart.mPhase != ProjectRestartPhase::None; }
305 const ProjectRestart& GetProjectRestart() const { return mRestart; }
306
311 void ReloadNativeAddonsWithProjectRestart(const std::vector<std::string>& addonIds,
312 bool forceRebuild,
313 const char* reason);
314
315 // Modal callbacks. Called from the EditorImgui modal renderers when the
316 // user clicks the corresponding button. Public because the modal lives
317 // outside this class.
318 void ProjectRestartConfirm(); // [Continue] on confirm modal
319 void ProjectRestartCancel(); // [Cancel] on confirm modal — abort whole flow
320 void ProjectRestartDirtySave(); // [Save] for the current dirty scene
321 void ProjectRestartDirtyDiscard(); // [Discard] for the current dirty scene
322 void ProjectRestartDirtyCancel(); // [Cancel] in dirty prompt — abort whole flow
323
332 void TickAllPlugins(float deltaTime);
333
342 void TickEditorAllPlugins(float deltaTime);
343
347 void CallOnEditorPreInit();
348
352 void CallOnEditorReady();
353
354 // ===== State Queries =====
355
362 const NativeAddonState* GetState(const std::string& addonId) const;
363
367 bool IsLoaded(const std::string& addonId) const;
368
375 std::string GetAddonSourcePath(const std::string& addonId) const;
376
380 std::vector<NativeAddonState> GetEngineAddons() const;
381
385 PolyphaseEngineAPI* GetEngineAPI() { return &mEngineAPI; }
386
387 // ===== Creation and Packaging =====
388
403 bool CreateNativeAddon(const NativeAddonCreateInfo& info, std::string& outError, std::string* outPath = nullptr);
404
417 bool CreateNativeAddonAtPath(const NativeAddonCreateInfo& info, const std::string& targetDir,
418 std::string& outError, std::string* outPath = nullptr);
419
429 bool PackageNativeAddon(const NativeAddonPackageOptions& options, std::string& outError);
430
439 bool GenerateIDEConfig(const std::string& addonPath);
440
444 std::vector<std::string> GetLocalPackageIds() const;
445
454 static bool GenerateAddonIncludesManifest();
455
463 static bool LoadAddonIncludesManifest(std::vector<std::string>& outIncludePaths,
464 std::vector<std::string>& outDefines);
465
466private:
467 static NativeAddonManager* sInstance;
468 NativeAddonManager();
469 ~NativeAddonManager();
470
471 // Discovery helpers
472 void ScanLocalPackages();
473 void ScanInstalledAddons();
474 bool ParsePackageJson(const std::string& path, NativeModuleMetadata& outMetadata);
475
476 // Build helpers
477 std::string GetIntermediateDir(const std::string& addonId);
478 std::string GetOutputPath(const std::string& addonId, const std::string& fingerprint);
479 bool GenerateBuildScript(const std::string& addonId, const std::string& outputDir,
480 const std::string& outputPath, std::string& outScriptPath);
481 std::vector<std::string> GatherSourceFiles(const std::string& sourceDir);
482
489 std::vector<std::string> TryClearAddonIntermediates(const std::string& addonId);
490
491 // Engine API setup
492 void InitializeEngineAPI();
493
494 // Creation helpers
495 std::string GenerateIdFromName(const std::string& name);
496 bool WriteTemplateSourceFile(const std::string& path, const std::string& addonName,
497 const std::string& binaryName);
498 bool WritePackageJson(const std::string& path, const NativeAddonCreateInfo& info);
499 bool WriteVSCodeConfig(const std::string& addonPath);
500 bool WriteCMakeLists(const std::string& addonPath, const std::string& binaryName);
501 bool WriteVSProject(const std::string& addonPath, const std::string& addonName,
502 const std::string& binaryName);
503
504 std::unordered_map<std::string, NativeAddonState> mStates;
505 PolyphaseEngineAPI mEngineAPI;
506
507 // ----- Async build queue -----
508 //
509 // One worker thread shells out to build.bat / build.sh per addon. The
510 // main thread polls completion in TickAsyncBuilds(), runs the post-
511 // build steps (write meta, MOD_Load, register types), and starts the
512 // next queued item. This keeps the editor interactive while addons
513 // compile, especially during multi-addon Force Rebuild.
514 struct AsyncAddonBuild
515 {
516 std::string addonId;
517 std::string scriptPath;
518 std::string outputPath;
519 std::string fingerprint;
520
521 std::thread thread;
522 std::atomic<bool> complete{false};
523 std::atomic<int> exitCode{0};
524
525 mutable std::mutex outputMutex;
526 std::string output; // guarded by outputMutex
527 };
528
529 std::unique_ptr<AsyncAddonBuild> mActiveBuild;
530 std::vector<std::string> mBuildQueue;
531 int mBuildQueueTotal = 0;
532 int mBuildQueueIndex = 0; // 1-based, advanced when a build starts
533
534 // Set when a pre-build sweep finds locked files in the intermediate dir.
535 BuildBlocked mBlocked;
536
537 // Project-restart state machine. See ProjectRestartPhase for the flow.
538 ProjectRestart mRestart;
539
540 // One-shot per-addon override: addon IDs in this set get their next
541 // LoadNativeAddon() invocation treated as resolveMode=source even when
542 // package.json says "binary". Set when the user clicks Reload Native
543 // Addons on a binary-mode addon — Reload means "recompile my local
544 // source", not "redownload the published binary". Consumed (erased)
545 // on first read so the override doesn't persist beyond a single load.
546 std::unordered_set<std::string> mForceSourceForNextLoad;
547
548 // Internal helpers
549 void StartNextQueuedBuild();
550 void FinalizeAsyncBuild(AsyncAddonBuild& job, bool success);
551 bool LoadNativeAddonAfterBuild(const std::string& addonId, std::string& outError);
552
553 // Project-restart helpers
554 void ProjectRestartBeginClose(); // dirty queue exhausted → close project + enqueue rebuilds
555 void ProjectRestartOnBuildsDone(); // called from TickAsyncBuilds when in Building phase
556 void ProjectRestartReset(); // clear state back to Phase::None
557};
558
559#endif // EDITOR
Engine API exposed to native addon plugins.
Stable C ABI header for native addon plugins.
Engine API provided to plugins during OnLoad.
Definition PolyphaseEngineAPI.h:32
Plugin descriptor returned by PolyphasePlugin_GetDesc.
Definition PolyphasePluginAPI.h:46