3232#include < stdlib.h>
3333
3434#include < memory>
35+ #include < algorithm>
36+ #include < string>
37+ #include < unordered_map>
38+ #include < unordered_set>
39+ #include < vector>
3540
3641#include " ExtensionSys.h"
3742#include < ILibrarySys.h>
@@ -250,15 +255,166 @@ bool CRemoteExtension::Reload(char *error, size_t maxlength)
250255 return false ;
251256}
252257
258+ namespace {
259+ struct ReloadPluginInfo {
260+ std::string filename;
261+ PluginType type;
262+ size_t order;
263+ bool was_paused;
264+ };
265+
266+ struct PendingExtensionReload {
267+ CLocalExtension *ext;
268+ std::vector<ReloadPluginInfo> to_reload;
269+ };
270+ } // anonymous namespace
271+
272+ void CExtensionManager::ProcessReloadFrame (void *data)
273+ {
274+ PendingExtensionReload *pending = static_cast <PendingExtensionReload *>(data);
275+
276+ // Bail if it was unloaded in the gap between frames.
277+ if (g_Extensions.m_Libs .find (pending->ext ) == g_Extensions.m_Libs .end ())
278+ {
279+ delete pending;
280+ return ;
281+ }
282+
283+ // Reopen now that the deferred dlclose has run, so static initializers re-run.
284+ char error[256 ];
285+ if (!pending->ext ->Load (error, sizeof (error)))
286+ {
287+ rootmenu->ConsolePrint (" [SM] Failed to reload extension \" %s\" : %s" ,
288+ pending->ext ->GetFilename (), error);
289+ delete pending;
290+ return ;
291+ }
292+
293+ // Reload dependent plugins in original load order.
294+ std::vector<std::pair<std::string, PluginType>> batch;
295+ for (auto &info : pending->to_reload )
296+ batch.push_back ({info.filename , info.type });
297+
298+ std::vector<CPlugin *> results = g_PluginSys.LoadPluginBatch (batch);
299+
300+ for (size_t i = 0 ; i < pending->to_reload .size (); i++) {
301+ if (!results[i]) {
302+ rootmenu->ConsolePrint (" [SM] Failed to reload plugin \" %s\" " ,
303+ pending->to_reload [i].filename .c_str ());
304+ } else {
305+ rootmenu->ConsolePrint (" [SM] Reloaded plugin \" %s\" " ,
306+ pending->to_reload [i].filename .c_str ());
307+ if (pending->to_reload [i].was_paused )
308+ results[i]->SetPauseState (true );
309+ }
310+ }
311+
312+ rootmenu->ConsolePrint (" [SM] Extension \" %s\" reloaded." , pending->ext ->GetFilename ());
313+ delete pending;
314+ }
315+
253316bool CLocalExtension::Reload (char *error, size_t maxlength)
254317{
255318 if (m_pLib == NULL ) // FIXME: just load it instead?
256319 return false ;
257-
320+
321+ // Step 1: Build a load-order map and identify direct dependents.
322+ // We must save this before any cleanup since unloading plugins removes them from
323+ // m_Dependents via DropRefsTo.
324+ std::unordered_map<std::string, size_t > load_order;
325+ std::unordered_set<std::string> was_running;
326+ std::vector<ReloadPluginInfo> to_reload;
327+
328+ {
329+ AutoPluginList list (scripts);
330+ for (size_t i = 0 ; i < list->size (); i++) {
331+ SMPlugin *plugin = list->at (i);
332+ std::string filename (plugin->GetFilename ());
333+ load_order[filename] = i;
334+
335+ PluginStatus status = plugin->GetStatus ();
336+ if (status == Plugin_Running || status == Plugin_Paused)
337+ was_running.insert (filename);
338+
339+ CPlugin *cp = static_cast <CPlugin *>(plugin);
340+ if (m_Dependents.find (cp) != m_Dependents.end () &&
341+ (status == Plugin_Running || status == Plugin_Paused))
342+ {
343+ to_reload.push_back ({filename, plugin->GetType (), i, status == Plugin_Paused});
344+ }
345+ }
346+ }
347+
348+ // Step 2: Clear native cache entries and unbind weak refs.
349+ DropEverything ();
350+
351+ // Step 3: Unload direct dependent plugins. They hold JIT-baked stale function
352+ // pointers that will crash if called after dlclose.
353+ // Copy and clear m_Dependents first — UnloadPlugin triggers OnPluginDestroyed
354+ // which calls DropRefsTo, removing entries from m_Dependents during iteration.
355+ // (UnloadExtension avoids this by removing itself from m_Libs first, but we
356+ // can't do that since we need to stay in m_Libs for reload.)
357+ List<CPlugin *> dependents_copy = m_Dependents;
358+ m_Dependents.clear ();
359+ for (List<CPlugin *>::iterator p_iter = dependents_copy.begin ();
360+ p_iter != dependents_copy.end ();
361+ p_iter++)
362+ {
363+ scripts->UnloadPlugin ((*p_iter));
364+ }
365+
366+ // Step 3b: Collect and unload cascaded victims — plugins that entered Plugin_Error
367+ // because they depended on a plugin we just unloaded (not on this extension directly).
368+ bool found;
369+ do {
370+ found = false ;
371+ AutoPluginList list (scripts);
372+ for (size_t i = 0 ; i < list->size (); i++) {
373+ SMPlugin *plugin = list->at (i);
374+ std::string filename (plugin->GetFilename ());
375+ if (plugin->GetStatus () == Plugin_Error && was_running.count (filename)) {
376+ auto it = load_order.find (filename);
377+ size_t order = (it != load_order.end ()) ? it->second : SIZE_MAX ;
378+ to_reload.push_back ({filename, plugin->GetType (), order, false });
379+ was_running.erase (filename);
380+ scripts->UnloadPlugin (plugin);
381+ found = true ;
382+ break ; // List was modified, restart scan.
383+ }
384+ }
385+ } while (found);
386+
387+ // Sort by original load order so inter-plugin dependencies resolve correctly.
388+ std::sort (to_reload.begin (), to_reload.end (),
389+ [](const ReloadPluginInfo &a, const ReloadPluginInfo &b) {
390+ return a.order < b.order ;
391+ });
392+
393+ // Step 4: Clean up extension state to prevent duplicates on reload.
394+ g_ShareSys.RemoveInterfaces (this );
395+ for (List<String>::iterator s_iter = m_Libraries.begin ();
396+ s_iter != m_Libraries.end ();
397+ s_iter++)
398+ {
399+ scripts->OnLibraryAction ((*s_iter).c_str (), LibraryAction_Removed);
400+ }
401+ m_Libraries.clear ();
402+ m_Interfaces.clear ();
403+
404+ // Step 5: Unload the extension (request dlclose).
258405 m_pAPI->OnExtensionUnload ();
259406 Unload ();
260-
261- return Load (error, maxlength);
407+
408+ // Step 6: Defer the reopen. Metamod doesn't actually dlclose until the current
409+ // command's hook stack unwinds, so reopening here would re-dlopen the still
410+ // mapped image and skip static re-init. Reopen on the next frame instead.
411+ PendingExtensionReload *pending = new PendingExtensionReload ();
412+ pending->ext = this ;
413+ pending->to_reload = std::move (to_reload);
414+
415+ g_pSM->AddFrameAction (&CExtensionManager::ProcessReloadFrame, pending);
416+
417+ return true ;
262418}
263419
264420bool CRemoteExtension::IsExternal ()
@@ -1188,18 +1344,39 @@ void CExtensionManager::OnRootConsoleCommand(const char *cmdname, const ICommand
11881344 {
11891345 if (argcount < 4 )
11901346 {
1191- rootmenu->ConsolePrint (" [SM] Usage: sm exts reload <#>" );
1347+ rootmenu->ConsolePrint (" [SM] Usage: sm exts reload <# or file >" );
11921348 return ;
11931349 }
1194-
1350+
11951351 const char *arg = command->Arg (3 );
11961352 unsigned int num = atoi (arg);
1197- CExtension *pExt = FindByOrder (num) ;
1353+ CExtension *pExt;
11981354
1199- if (!pExt )
1355+ if (num != 0 )
12001356 {
1201- rootmenu->ConsolePrint (" [SM] Extension number %d was not found." , num);
1202- return ;
1357+ pExt = FindByOrder (num);
1358+ if (!pExt)
1359+ {
1360+ rootmenu->ConsolePrint (" [SM] Extension number %d was not found." , num);
1361+ return ;
1362+ }
1363+ }
1364+ else
1365+ {
1366+ char path[PLATFORM_MAX_PATH ];
1367+ ke::SafeSprintf (path, sizeof (path), " %s%s" , arg, !strstr (arg, " .ext" ) ? " .ext" : " " );
1368+
1369+ /* Strip platform extension if present, m_File doesn't include it. */
1370+ const char *ext = libsys->GetFileExtension (path);
1371+ if (ext && strcmp (ext, PLATFORM_LIB_EXT ) == 0 )
1372+ path[strlen (path) - strlen (PLATFORM_LIB_EXT ) - 1 ] = ' \0 ' ;
1373+
1374+ pExt = (CExtension *)FindExtensionByFile (path);
1375+ if (!pExt)
1376+ {
1377+ rootmenu->ConsolePrint (" [SM] Extension %s is not loaded." , path);
1378+ return ;
1379+ }
12031380 }
12041381
12051382 if (pExt->IsLoaded ())
@@ -1211,7 +1388,8 @@ void CExtensionManager::OnRootConsoleCommand(const char *cmdname, const ICommand
12111388
12121389 if (pExt->Reload (error, sizeof (error)))
12131390 {
1214- rootmenu->ConsolePrint (" [SM] Extension %s is now reloaded." , filename);
1391+ // Reopen is deferred a frame; ProcessReloadFrame prints completion.
1392+ rootmenu->ConsolePrint (" [SM] Reloading extension %s..." , filename);
12151393 }
12161394 else
12171395 {
@@ -1234,7 +1412,7 @@ void CExtensionManager::OnRootConsoleCommand(const char *cmdname, const ICommand
12341412 rootmenu->DrawGenericOption (" info" , " Extra extension information" );
12351413 rootmenu->DrawGenericOption (" list" , " List extensions" );
12361414 rootmenu->DrawGenericOption (" load" , " Load an extension" );
1237- rootmenu->DrawGenericOption (" reload" , " Reload an extension" );
1415+ rootmenu->DrawGenericOption (" reload" , " Reload an extension by # or file " );
12381416 rootmenu->DrawGenericOption (" unload" , " Unload an extension" );
12391417}
12401418
0 commit comments