From 6fea92196dd6d7555eda4d98af1f8d85c8d66623 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 15 Feb 2026 13:18:33 +0100 Subject: [PATCH] Make persistent EvMenus not create extra cmdsets. Resolve #3154 --- CHANGELOG.md | 2 ++ evennia/utils/evmenu.py | 4 ++++ evennia/utils/tests/test_evmenu.py | 35 ++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 056759665a..a1fee9deab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ - [Fix][issue3194]: Make filtering on AttributeProperties consistent across typeclasses (Griatch) - [Fix][issue2774]: Properly support `\n` in `evennia connections` long descriptions (Griatch) - [Fix][issue3312]: Handle all edge cases breaking `monitor/monitored` `input_funcs` (Griatch) +- [Fix][issue3154]: Persistent EvMenu caused multiple cmdsets on reload (Griatch) - [Doc][pull3801]: Move Evennia doc build system to latest Sphinx/myST (PowershellNinja, also honorary mention to electroglyph) - [Doc][pull3800]: Describe support for Telnet SSH in HAProxy documentation (holl0wstar) @@ -77,6 +78,7 @@ [issue3194]: https://github.com/evennia/evennia/issues/3194 [issue2774]: https://github.com/evennia/evennia/issues/2774 [issue3312]: https://github.com/evennia/evennia/issues/3312 +[issue3154]: https://github.com/evennia/evennia/issues/3154 ## Evennia 5.0.1 diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 106e31ecde..f164d5c532 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -691,6 +691,10 @@ class EvMenu: logger.log_trace(_TRACE_PERSISTENT_SAVING) persistent = False + # Make sure to not stack menu cmdsets across reload-restore cycles. + # On reload, ndb is cleared so we can't always close an old menu cleanly first. + self.caller.cmdset.remove(EvMenuCmdSet) + # set up the menu command on the caller menu_cmdset = EvMenuCmdSet() menu_cmdset.mergetype = str(cmdset_mergetype).lower().capitalize() or "Replace" diff --git a/evennia/utils/tests/test_evmenu.py b/evennia/utils/tests/test_evmenu.py index 4f97d684f6..8d49713b5c 100644 --- a/evennia/utils/tests/test_evmenu.py +++ b/evennia/utils/tests/test_evmenu.py @@ -338,3 +338,38 @@ class TestMenuTemplateParse(BaseEvenniaTest): """ with self.assertRaises(RuntimeError): evmenu.parse_menu_template(self.char1, template, self.goto_callables) + + +def _reload_menu_start(caller, raw_string, **kwargs): + return "start text", {"key": "next", "desc": "go next", "goto": "end"} + + +def _reload_menu_end(caller, raw_string, **kwargs): + return "end text", {"key": "back", "desc": "go back", "goto": "start"} + + +class TestEvMenuPersistentReloadRegression(BaseEvenniaTest): + """ + Regression tests for persistent EvMenu reload behavior. + """ + + def test_persistent_restore_does_not_duplicate_cmdset(self): + menutree = {"start": _reload_menu_start, "end": _reload_menu_end} + + evmenu.EvMenu( + self.char1, menutree, persistent=True, cmdset_mergetype="Union", session=self.session + ) + menu_cmdsets = [cmdset for cmdset in self.char1.cmdset.get() if cmdset.key == "menu_cmdset"] + self.assertEqual(len(menu_cmdsets), 1) + + # Simulate reload behavior where ndb is cleared but cmdset persists. + del self.char1.ndb._evmenu + del self.char1.ndb._menutree + + evmenu.EvMenu( + self.char1, menutree, persistent=True, cmdset_mergetype="Union", session=self.session + ) + + menu_cmdsets = [cmdset for cmdset in self.char1.cmdset.get() if cmdset.key == "menu_cmdset"] + self.assertEqual(len(menu_cmdsets), 1) + self.assertEqual(self.char1.cmdset_storage.count("evennia.utils.evmenu.EvMenuCmdSet"), 1)