From 0a7c6bf3ef4d605aba60d88ba690522631edb2a7 Mon Sep 17 00:00:00 2001 From: patrik kinnunen Date: Wed, 1 Mar 2023 17:04:16 +0100 Subject: [PATCH 01/32] feat: #1 coverage for get all scripts on None obj --- evennia/scripts/tests.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index 7e3cb46ddb..8634c67e14 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -7,7 +7,7 @@ from evennia.scripts.models import ObjectDoesNotExist, ScriptDB from evennia.scripts.scripts import DoNothing, ExtendedLoopingCall from evennia.utils.create import create_script from evennia.utils.test_resources import BaseEvenniaTest - +from evennia.scripts.manager import ScriptDBManager class TestScript(BaseEvenniaTest): def test_create(self): @@ -18,6 +18,11 @@ class TestScript(BaseEvenniaTest): self.assertFalse(errors, errors) mockinit.assert_called() +class Test_improve_coverage(TestCase): + def test_not_obj_return_empty_list(self): + manager_obj = ScriptDBManager() + returned_list = manager_obj.get_all_scripts_on_obj(False) + self.assertEqual(returned_list, []) class TestScriptDB(TestCase): "Check the singleton/static ScriptDB object works correctly" @@ -51,11 +56,9 @@ class TestScriptDB(TestCase): # Check the script is not recreated as a side-effect self.assertFalse(self.scr in ScriptDB.objects.get_all_scripts()) - class TestExtendedLoopingCall(TestCase): """ Test the ExtendedLoopingCall class. - """ @mock.patch("evennia.scripts.scripts.LoopingCall") @@ -87,4 +90,4 @@ class TestExtendedLoopingCall(TestCase): loopcall.__call__.assert_not_called() self.assertEqual(loopcall.interval, 20) - loopcall._scheduleFrom.assert_called_with(121) + loopcall._scheduleFrom.assert_called_with(121) \ No newline at end of file From 4d9211d69f5455f22449f3002c1f1a6836ae6fe9 Mon Sep 17 00:00:00 2001 From: Daniel Ericsson Date: Wed, 1 Mar 2023 19:49:35 +0100 Subject: [PATCH 02/32] feat #3 Implemented ScriptHandler unit tests Implemented tests for starting scripts and listing script intervals through ScriptHandler. Code coverage improved from 72% -> 84% --- evennia/scripts/tests.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index 7e3cb46ddb..77786d125d 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -3,6 +3,7 @@ from unittest import TestCase, mock from parameterized import parameterized from evennia import DefaultScript +from evennia.objects.objects import DefaultObject from evennia.scripts.models import ObjectDoesNotExist, ScriptDB from evennia.scripts.scripts import DoNothing, ExtendedLoopingCall from evennia.utils.create import create_script @@ -18,6 +19,42 @@ class TestScript(BaseEvenniaTest): self.assertFalse(errors, errors) mockinit.assert_called() +class ListIntervalsScript(DefaultScript): + """ + A script that does nothing. Used to test listing of script with nonzero intervals. + """ + def at_script_creation(self): + """ + Setup the script + """ + self.key = "interval_test" + self.desc = "This is an empty placeholder script." + self.interval = 1 + self.repeats = 1 + +class TestScriptHandler(BaseEvenniaTest): + """ + Test the ScriptHandler class. + + """ + def setUp(self): + self.obj, self.errors = DefaultObject.create("test_object") + + def tearDown(self): + self.obj.delete() + + def test_start(self): + "Check that ScriptHandler start function works correctly" + self.obj.scripts.add(ListIntervalsScript) + self.num = self.obj.scripts.start(self.obj.scripts.all()[0].key) + self.assertTrue(self.num == 1) + + def test_list_script_intervals(self): + "Checks that Scripthandler __str__ function lists script intervals correctly" + self.obj.scripts.add(ListIntervalsScript) + self.str = str(self.obj.scripts) + self.assertTrue("None/1" in self.str) + self.assertTrue("1 repeats" in self.str) class TestScriptDB(TestCase): "Check the singleton/static ScriptDB object works correctly" From 633fc23f5eab475d444d7c6739d52fb5529eca10 Mon Sep 17 00:00:00 2001 From: hapeters Date: Wed, 1 Mar 2023 22:26:10 +0100 Subject: [PATCH 03/32] feat: #4 add tests to increase code coverage add test_store_key_raises_RunTimeError and test_remove_raises_RunTimeError to scripts/tests.py to increase code coverage for scripts/tickethandler.py --- evennia/scripts/tests.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index 7e3cb46ddb..ef43abccbd 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -7,6 +7,7 @@ from evennia.scripts.models import ObjectDoesNotExist, ScriptDB from evennia.scripts.scripts import DoNothing, ExtendedLoopingCall from evennia.utils.create import create_script from evennia.utils.test_resources import BaseEvenniaTest +from evennia.scripts.tickerhandler import TickerHandler class TestScript(BaseEvenniaTest): @@ -18,6 +19,17 @@ class TestScript(BaseEvenniaTest): self.assertFalse(errors, errors) mockinit.assert_called() +class Test_improve_coverage(TestCase): + def test_store_key_raises_RunTimeError(self): + with self.assertRaises(RuntimeError): + th=TickerHandler() + th._store_key(None, None, 0, None) + + def test_remove_raises_RunTimeError(self): + with self.assertRaises(RuntimeError): + th=TickerHandler() + th.remove(callback=1) + class TestScriptDB(TestCase): "Check the singleton/static ScriptDB object works correctly" From 1cc47d77e5d7b8bddf1847e6d724e00a4831fa92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pontus=20Pr=C3=BCzelius?= Date: Thu, 2 Mar 2023 01:18:37 +0100 Subject: [PATCH 04/32] feat: tests for add,remove in monitorhandler --- evennia/scripts/tests.py | 57 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index 7e3cb46ddb..d767cdf830 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -7,7 +7,8 @@ from evennia.scripts.models import ObjectDoesNotExist, ScriptDB from evennia.scripts.scripts import DoNothing, ExtendedLoopingCall from evennia.utils.create import create_script from evennia.utils.test_resources import BaseEvenniaTest - +from evennia.scripts.monitorhandler import MonitorHandler +import inspect class TestScript(BaseEvenniaTest): def test_create(self): @@ -88,3 +89,57 @@ class TestExtendedLoopingCall(TestCase): loopcall.__call__.assert_not_called() self.assertEqual(loopcall.interval, 20) loopcall._scheduleFrom.assert_called_with(121) +def dummy_func(): + return 0 +class TestMonitorHandler(TestCase): + def setUp(self): + self.handler = MonitorHandler() + + def test_add(self): + obj = mock.Mock() + fieldname = "db_add" + callback = dummy_func + idstring = "test" + self.assertEquals(inspect.isfunction(callback),True) + + self.handler.add(obj, fieldname, callback, idstring=idstring) + + self.assertIn(fieldname, self.handler.monitors[obj]) + self.assertIn(idstring, self.handler.monitors[obj][fieldname]) + self.assertEqual(self.handler.monitors[obj][fieldname][idstring], (callback, False, {})) + + def test_remove(self): + obj = mock.Mock() + fieldname = 'db_remove' + callback = dummy_func + idstring = 'test_remove' + + self.handler.add(obj,fieldname,callback,idstring=idstring) + self.assertIn(fieldname,self.handler.monitors[obj]) + self.assertEqual(self.handler.monitors[obj][fieldname][idstring], (callback, False, {})) + + self.handler.remove(obj,fieldname,idstring=idstring) + self.assertEquals(self.handler.monitors[obj][fieldname], {}) + + def test_add_with_invalid_callback_does_not_work(self): + obj = mock.Mock() + fieldname = "db_key" + callback = "not_a_function" + + self.handler.add(obj, fieldname, callback) + self.assertNotIn(fieldname, self.handler.monitors[obj]) + + """ def test_add_raise_exception(self): + obj = mock.Mock() + fieldname = "db_add" + callback = 1 + idstring = "test" + # self.assertEquals(inspect.isfunction(callback),True) + self.assertRaises(Exception,self.handler.add,obj, fieldname, callback, idstring=idstring) + """ + + + + + + From e70da12872b9c9f64baf66f3cb509fe241d6d918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pontus=20Pr=C3=BCzelius?= Date: Fri, 3 Mar 2023 16:53:49 +0100 Subject: [PATCH 05/32] fix:Removed unused commented out function --- evennia/scripts/tests.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index d767cdf830..776ab722aa 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -129,15 +129,6 @@ class TestMonitorHandler(TestCase): self.handler.add(obj, fieldname, callback) self.assertNotIn(fieldname, self.handler.monitors[obj]) - """ def test_add_raise_exception(self): - obj = mock.Mock() - fieldname = "db_add" - callback = 1 - idstring = "test" - # self.assertEquals(inspect.isfunction(callback),True) - self.assertRaises(Exception,self.handler.add,obj, fieldname, callback, idstring=idstring) - """ - From 3de7ce7d416b367262474877d5aa33a03ebd9820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pontus=20Pr=C3=BCzelius?= Date: Fri, 3 Mar 2023 18:21:39 +0100 Subject: [PATCH 06/32] feat: added tests for all,clear #2 --- evennia/scripts/tests.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index 41affece5f..8694a82057 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -12,6 +12,7 @@ from evennia.scripts.tickerhandler import TickerHandler from evennia.scripts.monitorhandler import MonitorHandler import inspect from evennia.scripts.manager import ScriptDBManager +from collections import defaultdict class TestScript(BaseEvenniaTest): def test_create(self): @@ -145,16 +146,16 @@ class TestExtendedLoopingCall(TestCase): def dummy_func(): return 0 + class TestMonitorHandler(TestCase): def setUp(self): self.handler = MonitorHandler() - + def test_add(self): obj = mock.Mock() fieldname = "db_add" callback = dummy_func idstring = "test" - self.assertEquals(inspect.isfunction(callback),True) self.handler.add(obj, fieldname, callback, idstring=idstring) @@ -183,6 +184,36 @@ class TestMonitorHandler(TestCase): self.handler.add(obj, fieldname, callback) self.assertNotIn(fieldname, self.handler.monitors[obj]) + def test_all(self): + obj = [mock.Mock(),mock.Mock()] + fieldname = ["db_all1","db_all2"] + callback = dummy_func + idstring = ["test_all1","test_all2"] + + self.handler.add(obj[0], fieldname[0], callback, idstring=idstring[0]) + self.handler.add(obj[1], fieldname[1], callback, idstring=idstring[1],persistent=True) + + output = self.handler.all() + self.assertEquals(output, + [(obj[0], fieldname[0], idstring[0], False, {}), + (obj[1], fieldname[1], idstring[1], True, {})]) + + def test_clear(self): + obj = mock.Mock() + fieldname = "db_add" + callback = dummy_func + idstring = "test" + + self.handler.add(obj, fieldname, callback, idstring=idstring) + self.assertIn(obj, self.handler.monitors) + + self.handler.clear() + self.assertNotIn(obj, self.handler.monitors) + self.assertEquals(defaultdict(lambda: defaultdict(dict)), self.handler.monitors) + + + + From d39a1d974ef91ad3c243c748ff7020cb681c4aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pontus=20Pr=C3=BCzelius?= Date: Fri, 3 Mar 2023 18:31:12 +0100 Subject: [PATCH 07/32] doc: added documentation to added tests #2 --- evennia/scripts/tests.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index 8694a82057..5120422f08 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -148,10 +148,15 @@ def dummy_func(): return 0 class TestMonitorHandler(TestCase): + """ + Test the MonitorHandler class. + """ + def setUp(self): self.handler = MonitorHandler() def test_add(self): + """Tests that adding an object to the monitor handler works correctly""" obj = mock.Mock() fieldname = "db_add" callback = dummy_func @@ -164,20 +169,20 @@ class TestMonitorHandler(TestCase): self.assertEqual(self.handler.monitors[obj][fieldname][idstring], (callback, False, {})) def test_remove(self): + """Tests that removing an object from the monitor handler works correctly""" obj = mock.Mock() fieldname = 'db_remove' callback = dummy_func idstring = 'test_remove' + """Add an object to the monitor handler and then remove it""" self.handler.add(obj,fieldname,callback,idstring=idstring) - self.assertIn(fieldname,self.handler.monitors[obj]) - self.assertEqual(self.handler.monitors[obj][fieldname][idstring], (callback, False, {})) - self.handler.remove(obj,fieldname,idstring=idstring) self.assertEquals(self.handler.monitors[obj][fieldname], {}) - def test_add_with_invalid_callback_does_not_work(self): + def test_add_with_invalid_function(self): obj = mock.Mock() + """Tests that add method rejects objects where callback is not a function""" fieldname = "db_key" callback = "not_a_function" @@ -185,6 +190,7 @@ class TestMonitorHandler(TestCase): self.assertNotIn(fieldname, self.handler.monitors[obj]) def test_all(self): + """Tests that all method correctly returns information about added objects""" obj = [mock.Mock(),mock.Mock()] fieldname = ["db_all1","db_all2"] callback = dummy_func @@ -199,6 +205,7 @@ class TestMonitorHandler(TestCase): (obj[1], fieldname[1], idstring[1], True, {})]) def test_clear(self): + """Tests that the clear function correctly clears the monitor handler""" obj = mock.Mock() fieldname = "db_add" callback = dummy_func From cff9f7e2c43e93a8eb128cc27c37f5c4b6cafc52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pontus=20Pr=C3=BCzelius?= Date: Fri, 3 Mar 2023 19:49:14 +0100 Subject: [PATCH 08/32] feat: tests for add/remove attributes from monitorhandler --- evennia/scripts/tests.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index 5120422f08..c4aec37a41 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -13,6 +13,7 @@ from evennia.scripts.monitorhandler import MonitorHandler import inspect from evennia.scripts.manager import ScriptDBManager from collections import defaultdict +from evennia.utils.dbserialize import dbserialize class TestScript(BaseEvenniaTest): def test_create(self): @@ -218,11 +219,26 @@ class TestMonitorHandler(TestCase): self.assertNotIn(obj, self.handler.monitors) self.assertEquals(defaultdict(lambda: defaultdict(dict)), self.handler.monitors) + def test_add_remove_attribute(self): + """Tests that adding and removing an object attribute to the monitor handler works correctly""" + obj = mock.Mock() + obj.name = "testaddattribute" + fieldname = "name" + callback = dummy_func + idstring = "test" + category = "testattribute" - - - - + """Add attribute to handler and assert that it has been added""" + self.handler.add(obj, fieldname, callback, idstring=idstring,category=category) + index = obj.attributes.get(fieldname, return_obj=True) + name = "db_value[testattribute]" + self.assertIn(name, self.handler.monitors[index]) + self.assertIn(idstring, self.handler.monitors[index][name]) + self.assertEqual(self.handler.monitors[index][name][idstring], (callback, False, {})) + """Remove attribute from the handler and assert that it is gone""" + self.handler.remove(obj,fieldname,idstring=idstring,category=category) + self.assertEquals(self.handler.monitors[index][name], {}) + \ No newline at end of file From f5b4a0fc4712b69575336af43b9e11f3c60ee0db Mon Sep 17 00:00:00 2001 From: Storsorken Date: Sun, 5 Mar 2023 20:20:26 +0100 Subject: [PATCH 09/32] tests: #11 add test for invalid argument Tests invalid argument to start method in ExtendedLoopingCall class throws ValueError --- evennia/scripts/tests.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index 51a244a1c9..aa0b97d1ec 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -143,6 +143,13 @@ class TestExtendedLoopingCall(TestCase): self.assertEqual(loopcall.interval, 20) loopcall._scheduleFrom.assert_called_with(121) + def test_start_invalid_interval(self): + """ Test the .start method with interval less than zero """ + with self.assertRaises(ValueError): + callback = mock.MagicMock() + loopcall = ExtendedLoopingCall(callback) + loopcall.start(-1, now=True, start_delay=None, count_start=1) + def dummy_func(): return 0 class TestMonitorHandler(TestCase): From 437bbd24971bdc4d98dcfc86f38faedc5d6adf69 Mon Sep 17 00:00:00 2001 From: Storsorken Date: Sun, 5 Mar 2023 20:43:14 +0100 Subject: [PATCH 10/32] tests: #11 add test for __call__ Add test for __call__ in ExtendedLoopingCall class Test __call__ modifies start_delay and starttime if start_delay was previously set --- evennia/scripts/tests.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index aa0b97d1ec..e8e4868a3f 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -150,6 +150,19 @@ class TestExtendedLoopingCall(TestCase): loopcall = ExtendedLoopingCall(callback) loopcall.start(-1, now=True, start_delay=None, count_start=1) + def test__call__when_delay(self): + """ Test __call__ modifies start_delay and starttime if start_delay was previously set """ + callback = mock.MagicMock() + loopcall = ExtendedLoopingCall(callback) + loopcall.clock.seconds = mock.MagicMock(return_value=1) + loopcall.start_delay = 2 + loopcall.starttime = 0 + + loopcall() + + self.assertEqual(loopcall.start_delay, None) + self.assertEqual(loopcall.starttime, 1) + def dummy_func(): return 0 class TestMonitorHandler(TestCase): From 371e8bbefb90b47190535525cb9ca7959dcc8039 Mon Sep 17 00:00:00 2001 From: Storsorken Date: Mon, 6 Mar 2023 01:24:22 +0100 Subject: [PATCH 11/32] Tests: #11 test force_repeat Test forcing script to run that is scheduled to run in the future --- evennia/scripts/tests.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index e8e4868a3f..00d3b3de67 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -163,6 +163,17 @@ class TestExtendedLoopingCall(TestCase): self.assertEqual(loopcall.start_delay, None) self.assertEqual(loopcall.starttime, 1) + def test_force_repeat(self): + """ Test forcing script to run that is scheduled to run in the future """ + callback = mock.MagicMock() + loopcall = ExtendedLoopingCall(callback) + loopcall.clock.seconds = mock.MagicMock(return_value=0) + + loopcall.start(20, now=False, start_delay=5, count_start=0) + loopcall.force_repeat() + + callback.assert_called_once() + def dummy_func(): return 0 class TestMonitorHandler(TestCase): From f3f9b712c5372748a840eac04e2fc3b3b1ad026e Mon Sep 17 00:00:00 2001 From: Wendy Wang Date: Mon, 6 Mar 2023 10:45:36 +0100 Subject: [PATCH 12/32] Fix f-string in clothing contrib Fixes #3132 --- evennia/contrib/game_systems/clothing/clothing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/game_systems/clothing/clothing.py b/evennia/contrib/game_systems/clothing/clothing.py index c368916517..5143b9d9af 100644 --- a/evennia/contrib/game_systems/clothing/clothing.py +++ b/evennia/contrib/game_systems/clothing/clothing.py @@ -277,7 +277,7 @@ class ContribClothing(DefaultObject): else: message = f"$You() $conj(put) on {self.name}" if to_cover: - message += ", covering {iter_to_str(to_cover)}" + message += f", covering {iter_to_str(to_cover)}" wearer.location.msg_contents(message + ".", from_obj=wearer) def remove(self, wearer, quiet=False): From 5dac82db7ee4810004022952f84092e5c27458db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pontus=20Pr=C3=BCzelius?= Date: Mon, 6 Mar 2023 21:54:18 +0100 Subject: [PATCH 13/32] doc: dummy_func explained --- evennia/scripts/tests.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index c4aec37a41..a97cfb1c47 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -144,7 +144,10 @@ class TestExtendedLoopingCall(TestCase): loopcall.__call__.assert_not_called() self.assertEqual(loopcall.interval, 20) loopcall._scheduleFrom.assert_called_with(121) - + +""" +Dummy function used as callback parameter +""" def dummy_func(): return 0 From 18c1ddd469d11084f2ce629d0d4990c5145ff667 Mon Sep 17 00:00:00 2001 From: Storsorken Date: Tue, 7 Mar 2023 19:36:47 +0100 Subject: [PATCH 14/32] fix: #18 clean up patch Fix white space changes. End of file newline has been re-added and and some unnecessary whitespace changes have been reverted --- evennia/scripts/tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index 2530e0006e..471af8746f 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -109,9 +109,11 @@ class TestScriptDB(TestCase): # Check the script is not recreated as a side-effect self.assertFalse(self.scr in ScriptDB.objects.get_all_scripts()) + class TestExtendedLoopingCall(TestCase): """ Test the ExtendedLoopingCall class. + """ @mock.patch("evennia.scripts.scripts.LoopingCall") @@ -275,4 +277,3 @@ class TestMonitorHandler(TestCase): """Remove attribute from the handler and assert that it is gone""" self.handler.remove(obj,fieldname,idstring=idstring,category=category) self.assertEquals(self.handler.monitors[index][name], {}) - \ No newline at end of file From a35631eba947ed6ff9cccd6e7e97422e0b2284f0 Mon Sep 17 00:00:00 2001 From: Storsorken Date: Tue, 7 Mar 2023 19:50:22 +0100 Subject: [PATCH 15/32] fix: #18 revert unnecessary newline change --- evennia/scripts/tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index 471af8746f..14e8c46a96 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -15,6 +15,7 @@ from evennia.scripts.manager import ScriptDBManager from collections import defaultdict from evennia.utils.dbserialize import dbserialize + class TestScript(BaseEvenniaTest): def test_create(self): "Check the script can be created via the convenience method." From d5dac050387402424a4ff47ab061e10c90e83068 Mon Sep 17 00:00:00 2001 From: Storsorken Date: Wed, 8 Mar 2023 12:25:33 +0100 Subject: [PATCH 16/32] fix: #20 separate tests in Test_improve_coverage --- evennia/scripts/tests.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index 14e8c46a96..1c21885c71 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -25,7 +25,7 @@ class TestScript(BaseEvenniaTest): self.assertFalse(errors, errors) mockinit.assert_called() -class Test_improve_coverage(TestCase): +class TestTickerHandler(TestCase): def test_store_key_raises_RunTimeError(self): with self.assertRaises(RuntimeError): th=TickerHandler() @@ -35,7 +35,8 @@ class Test_improve_coverage(TestCase): with self.assertRaises(RuntimeError): th=TickerHandler() th.remove(callback=1) - + +class TestScriptDBManager(TestCase): def test_not_obj_return_empty_list(self): manager_obj = ScriptDBManager() returned_list = manager_obj.get_all_scripts_on_obj(False) From db625370db8c7e2d348b273271f0c6c1ac775398 Mon Sep 17 00:00:00 2001 From: Storsorken Date: Wed, 8 Mar 2023 13:20:26 +0100 Subject: [PATCH 17/32] fix: #21 fix imports --- evennia/scripts/tests.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index 1c21885c71..6b7f05907d 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -1,4 +1,5 @@ from unittest import TestCase, mock +from collections import defaultdict from parameterized import parameterized @@ -10,9 +11,7 @@ from evennia.utils.create import create_script from evennia.utils.test_resources import BaseEvenniaTest from evennia.scripts.tickerhandler import TickerHandler from evennia.scripts.monitorhandler import MonitorHandler -import inspect from evennia.scripts.manager import ScriptDBManager -from collections import defaultdict from evennia.utils.dbserialize import dbserialize From cc308c98d8c6cbf28161592b564325c637094490 Mon Sep 17 00:00:00 2001 From: Storsorken Date: Wed, 8 Mar 2023 13:22:39 +0100 Subject: [PATCH 18/32] docs: #21 move comment --- evennia/scripts/tests.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index 6b7f05907d..f46db121cb 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -179,10 +179,8 @@ class TestExtendedLoopingCall(TestCase): callback.assert_called_once() -""" -Dummy function used as callback parameter -""" def dummy_func(): + """ Dummy function used as callback parameter """ return 0 class TestMonitorHandler(TestCase): From a9e2309c3e6887627a8f3f96f732e47cb275d7b8 Mon Sep 17 00:00:00 2001 From: Storsorken Date: Wed, 8 Mar 2023 13:45:32 +0100 Subject: [PATCH 19/32] docs: #21 add missing docstrings Add missing docstrings for scripts/tests.py --- evennia/scripts/tests.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index f46db121cb..700e82e429 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -1,3 +1,8 @@ +""" +Unit tests for the scripts module + +""" + from unittest import TestCase, mock from collections import defaultdict @@ -25,18 +30,25 @@ class TestScript(BaseEvenniaTest): mockinit.assert_called() class TestTickerHandler(TestCase): + """ Test the TickerHandler class """ + def test_store_key_raises_RunTimeError(self): + """ Test _store_key method raises RuntimeError for interval < 1 """ with self.assertRaises(RuntimeError): th=TickerHandler() th._store_key(None, None, 0, None) def test_remove_raises_RunTimeError(self): + """ Test remove method raises RuntimeError for catching old ordering of arguments """ with self.assertRaises(RuntimeError): th=TickerHandler() th.remove(callback=1) class TestScriptDBManager(TestCase): + """ Test the ScriptDBManger class """ + def test_not_obj_return_empty_list(self): + """ Test get_all_scripts_on_obj returns empty list for falsy object """ manager_obj = ScriptDBManager() returned_list = manager_obj.get_all_scripts_on_obj(False) self.assertEqual(returned_list, []) From f95b9c4fab4e3d7ed1ba7fed1aaf56711c9107b9 Mon Sep 17 00:00:00 2001 From: Storsorken Date: Wed, 8 Mar 2023 13:55:19 +0100 Subject: [PATCH 20/32] docs: #21 fix comment --- evennia/scripts/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index 700e82e429..7e4aa5f13f 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -1,5 +1,5 @@ """ -Unit tests for the scripts module +Unit tests for the scripts package """ From 9e65842e720a19e6886e2bbb65d56e8058d2f7e7 Mon Sep 17 00:00:00 2001 From: Antrare Date: Thu, 9 Mar 2023 12:20:38 +1100 Subject: [PATCH 21/32] Update tb_basic.py Fixing issue with at_pre_move not having the updated arguments for 1.0 --- evennia/contrib/game_systems/turnbattle/tb_basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/contrib/game_systems/turnbattle/tb_basic.py b/evennia/contrib/game_systems/turnbattle/tb_basic.py index 3b6d0a5075..6757f6b852 100644 --- a/evennia/contrib/game_systems/turnbattle/tb_basic.py +++ b/evennia/contrib/game_systems/turnbattle/tb_basic.py @@ -323,7 +323,7 @@ class TBBasicCharacter(DefaultCharacter): can be changed at creation and factor into combat calculations. """ - def at_pre_move(self, destination): + def at_pre_move(self, destination, move_type='move', **kwargs): """ Called just before starting to move this object to destination. From 176e0f5a08851f6918e943aede0df9bfe4a3bbf3 Mon Sep 17 00:00:00 2001 From: Storsorken Date: Mon, 13 Mar 2023 20:33:18 +0100 Subject: [PATCH 22/32] fix: rename ListIntervalsScript due to global name Rename ListIntervalsScript to TestingListIntervalScript to avoid potential name clashes --- evennia/scripts/tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index 7e4aa5f13f..e9b40a9626 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -53,7 +53,7 @@ class TestScriptDBManager(TestCase): returned_list = manager_obj.get_all_scripts_on_obj(False) self.assertEqual(returned_list, []) -class ListIntervalsScript(DefaultScript): +class TestingListIntervalScript(DefaultScript): """ A script that does nothing. Used to test listing of script with nonzero intervals. """ @@ -79,13 +79,13 @@ class TestScriptHandler(BaseEvenniaTest): def test_start(self): "Check that ScriptHandler start function works correctly" - self.obj.scripts.add(ListIntervalsScript) + self.obj.scripts.add(TestingListIntervalScript) self.num = self.obj.scripts.start(self.obj.scripts.all()[0].key) self.assertTrue(self.num == 1) def test_list_script_intervals(self): "Checks that Scripthandler __str__ function lists script intervals correctly" - self.obj.scripts.add(ListIntervalsScript) + self.obj.scripts.add(TestingListIntervalScript) self.str = str(self.obj.scripts) self.assertTrue("None/1" in self.str) self.assertTrue("1 repeats" in self.str) From fb66a71639ec5d569080075d9a32c330af81f17c Mon Sep 17 00:00:00 2001 From: Storsorken Date: Mon, 13 Mar 2023 20:50:01 +0100 Subject: [PATCH 23/32] bug: stop repeating script before test ends Test might otherwise causing unexpected side effects --- evennia/scripts/tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/evennia/scripts/tests.py b/evennia/scripts/tests.py index e9b40a9626..b54f465044 100644 --- a/evennia/scripts/tests.py +++ b/evennia/scripts/tests.py @@ -188,6 +188,7 @@ class TestExtendedLoopingCall(TestCase): loopcall.start(20, now=False, start_delay=5, count_start=0) loopcall.force_repeat() + loopcall.stop() callback.assert_called_once() From af2bbd12c9ed5f74e8cfa30a9a2ec4a35a0c1c5f Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 6 Mar 2023 20:12:49 +0100 Subject: [PATCH 24/32] Add support for saving maxlen of deque in Attributes --- evennia/utils/dbserialize.py | 17 +++++++++++------ evennia/utils/tests/test_dbserialize.py | 15 +++++++++++++-- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/evennia/utils/dbserialize.py b/evennia/utils/dbserialize.py index d49143f970..3ae239d263 100644 --- a/evennia/utils/dbserialize.py +++ b/evennia/utils/dbserialize.py @@ -30,7 +30,6 @@ except ImportError: from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.utils.safestring import SafeString - from evennia.utils import logger from evennia.utils.utils import is_iter, to_bytes, uses_database @@ -209,6 +208,8 @@ class _SaverMutable: dat = _SaverDefaultDict(item.default_factory, _parent=parent) dat._data.update((key, process_tree(val, dat)) for key, val in item.items()) return dat + elif dtype == deque: + dat = _SaverDeque(_parent=parent, maxlen=item.maxlen) elif dtype == set: dat = _SaverSet(_parent=parent) dat._data.update(process_tree(val, dat) for val in item) @@ -431,9 +432,9 @@ class _SaverDeque(_SaverMutable): A deque that can be saved and operated on. """ - def __init__(self, *args, **kwargs): + def __init__(self, *args, maxlen=None, **kwargs): super().__init__(*args, **kwargs) - self._data = deque() + self._data = deque((), maxlen=maxlen) @_save def append(self, *args, **kwargs): @@ -513,6 +514,8 @@ def deserialize(obj): return defaultdict( obj.default_factory, {_iter(key): _iter(val) for key, val in obj.items()} ) + elif tname in ("_SaverDeque", deque): + return deque((_iter(val) for val in obj), maxlen=obj.maxlen) elif tname in _DESERIALIZE_MAPPING: return _DESERIALIZE_MAPPING[tname](_iter(val) for val in obj) elif is_iter(obj): @@ -680,6 +683,8 @@ def to_pickle(data): item.default_factory, ((process_item(key), process_item(val)) for key, val in item.items()), ) + elif dtype in (deque, _SaverDeque): + return deque((process_item(val) for val in item), maxlen=item.maxlen) elif dtype in (set, _SaverSet): return set(process_item(val) for val in item) elif dtype in (OrderedDict, _SaverOrderedDict): @@ -776,7 +781,7 @@ def from_pickle(data, db_obj=None): elif dtype == OrderedDict: return OrderedDict((process_item(key), process_item(val)) for key, val in item.items()) elif dtype == deque: - return deque(process_item(val) for val in item) + return deque((process_item(val) for val in item), maxlen=item.maxlen) elif hasattr(item, "__iter__"): try: # we try to conserve the iterable class, if not convert to dict @@ -849,7 +854,7 @@ def from_pickle(data, db_obj=None): ) return dat elif dtype == deque: - dat = _SaverDeque(_parent=parent) + dat = _SaverDeque(_parent=parent, maxlen=item.maxlen) dat._data.extend(process_item(val) for val in item) return dat elif hasattr(item, "__iter__"): @@ -920,7 +925,7 @@ def from_pickle(data, db_obj=None): ) return dat elif dtype == deque: - dat = _SaverDeque(_db_obj=db_obj) + dat = _SaverDeque(_db_obj=db_obj, maxlen=data.maxlen) dat._data.extend(process_item(val) for val in data) return dat elif hasattr(data, "__iter__"): diff --git a/evennia/utils/tests/test_dbserialize.py b/evennia/utils/tests/test_dbserialize.py index 893e4949cf..2c0ef1ab2b 100644 --- a/evennia/utils/tests/test_dbserialize.py +++ b/evennia/utils/tests/test_dbserialize.py @@ -5,10 +5,9 @@ Tests for dbserialize module from collections import defaultdict, deque from django.test import TestCase -from parameterized import parameterized - from evennia.objects.objects import DefaultObject from evennia.utils import dbserialize +from parameterized import parameterized class TestDbSerialize(TestCase): @@ -116,6 +115,18 @@ class TestDbSerialize(TestCase): self.obj.db.test |= {"b": [5, 6]} self.assertEqual(self.obj.db.test, {"a": [1, 2, 3], "b": [5, 6]}) + def test_deque_with_maxlen(self): + _dd = deque((), maxlen=1) + _dd.append(1) + _dd.append(2) + self.assertEqual(list(_dd), [2]) + + dd = deque((), maxlen=1) + self.obj.db.test = dd + self.obj.db.test.append(1) + self.obj.db.test.append(2) + self.assertEqual(list(self.obj.db.test), [2]) + class _InvalidContainer: """Container not saveable in Attribute (if obj is dbobj, it 'hides' it)""" From 4655fcfab06da3991c5a5730b97562c8fb97d9db Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 6 Mar 2023 20:15:08 +0100 Subject: [PATCH 25/32] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c27159731..fdb2573585 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Main branch + +- Feature: Add support for saving `deque` with `maxlen` to Attributes (before + `maxlen` was ignored). + ## Evennia 1.2.1 Feb 26, 2023 From 6c5e9193bba1cb7af19d8a2c9f1d13fa54f3e977 Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 17 Mar 2023 22:19:00 +0100 Subject: [PATCH 26/32] Fix evmenu doc example --- docs/source/Components/EvMenu.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/source/Components/EvMenu.md b/docs/source/Components/EvMenu.md index 44c2f5e6d1..ba3f8f9eaa 100644 --- a/docs/source/Components/EvMenu.md +++ b/docs/source/Components/EvMenu.md @@ -645,9 +645,6 @@ def _gamble(caller, raw_string, **kwargs): else: return "win" -def _try_again(caller, raw_string, **kwargs): - return None # reruns the same node - template_string = """ ## node start @@ -660,8 +657,8 @@ he says. ## options -1. Roll the dice -> gamble() -2. Try to talk yourself out of rolling -> ask_again() +1: Roll the dice -> gamble() +2: Try to talk yourself out of rolling -> start ## node win @@ -687,7 +684,9 @@ says Death. """ -goto_callables = {"gamble": _gamble, "ask_again": _ask_again} +# map the in-template callable-name to real python code +goto_callables = {"gamble": _gamble} +# this starts the evmenu for the caller evmenu.template2menu(caller, template_string, goto_callables) ``` From e0fea71794027937cad5e559e8c23e7ad164c660 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 18 Mar 2023 10:24:06 +0100 Subject: [PATCH 27/32] Have is_ooc lockfunc return True if no session found. Resolve #3129 --- CHANGELOG.md | 1 + evennia/locks/lockfuncs.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdb2573585..b4fc887ef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Feature: Add support for saving `deque` with `maxlen` to Attributes (before `maxlen` was ignored). +- Fix: More unit tests for scripts (Storsorken) ## Evennia 1.2.1 diff --git a/evennia/locks/lockfuncs.py b/evennia/locks/lockfuncs.py index d9d2302561..0f5a7733b6 100644 --- a/evennia/locks/lockfuncs.py +++ b/evennia/locks/lockfuncs.py @@ -511,7 +511,8 @@ def is_ooc(accessing_obj, accessed_obj, *args, **kwargs): is_ooc() This is normally used to lock a Command, so it can be used - only when out of character. + only when out of character. When not logged in at all, this + function will still return True. """ obj = accessed_obj.obj if hasattr(accessed_obj, "obj") else accessed_obj account = obj.account if hasattr(obj, "account") else obj @@ -520,9 +521,14 @@ def is_ooc(accessing_obj, accessed_obj, *args, **kwargs): try: session = accessed_obj.session except AttributeError: - session = account.sessions.get()[0] # note-this doesn't work well + # note-this doesn't work well # for high multisession mode. We may need # to change to sessiondb to resolve this + sessions = session = account.sessions.get() + session = sessions[0] if sessions else None + if not session: + # this suggests we are not even logged in; treat as ooc. + return True try: return not account.get_puppet(session) except TypeError: From d10fad89ad2e90c9458b262b73fe6a77db4be97c Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 18 Mar 2023 12:09:42 +0100 Subject: [PATCH 28/32] Split Exit/Rooms/Characters into seaprate doc pages. Describe desc templating. Resolve #3134 --- CHANGELOG.md | 2 + docs/source/Coding/Changelog.md | 8 + docs/source/Components/Characters.md | 29 +++ docs/source/Components/Components-Overview.md | 3 + docs/source/Components/Exits.md | 55 +++++ docs/source/Components/Objects.md | 195 +++++++----------- docs/source/Components/Rooms.md | 31 +++ docs/source/Contribs/Contrib-Ingame-Python.md | 2 +- docs/source/Evennia-API.md | 6 +- .../base_systems/ingame_python/README.md | 2 +- 10 files changed, 210 insertions(+), 123 deletions(-) create mode 100644 docs/source/Components/Characters.md create mode 100644 docs/source/Components/Exits.md create mode 100644 docs/source/Components/Rooms.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b4fc887ef8..8a0fa4856d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Feature: Add support for saving `deque` with `maxlen` to Attributes (before `maxlen` was ignored). - Fix: More unit tests for scripts (Storsorken) +- Docs: Made separate doc pages for Exits, Characters and Rooms. Expanded on how + to change the description of an in-game object with templating. ## Evennia 1.2.1 diff --git a/docs/source/Coding/Changelog.md b/docs/source/Coding/Changelog.md index 8c27159731..8a0fa4856d 100644 --- a/docs/source/Coding/Changelog.md +++ b/docs/source/Coding/Changelog.md @@ -1,5 +1,13 @@ # Changelog +## Main branch + +- Feature: Add support for saving `deque` with `maxlen` to Attributes (before + `maxlen` was ignored). +- Fix: More unit tests for scripts (Storsorken) +- Docs: Made separate doc pages for Exits, Characters and Rooms. Expanded on how + to change the description of an in-game object with templating. + ## Evennia 1.2.1 Feb 26, 2023 diff --git a/docs/source/Components/Characters.md b/docs/source/Components/Characters.md new file mode 100644 index 0000000000..45ceeb1bde --- /dev/null +++ b/docs/source/Components/Characters.md @@ -0,0 +1,29 @@ +# Characters +**Inheritance Tree: +``` +┌─────────────┐ +│DefaultObject│ +└─────▲───────┘ + │ +┌─────┴──────────┐ +│DefaultCharacter│ +└─────▲──────────┘ + │ ┌────────────┐ + │ ┌─────────►ObjectParent│ + │ │ └────────────┘ + ┌───┴─┴───┐ + │Character│ + └─────────┘ +``` + +_Characters_ is an in-game [Object](./Objects.md) commonly used to represent the player's in-game avatar. The empty `Character` class is found in `mygame/typeclasses/characters.py`. It inherits from [DefaultCharacter](evennia.objects.objects.DefaultCharacter) and the (by default empty) `ObjectParent` class (used if wanting to add share properties between all in-game Objects). + +When a new [Account](./Accounts.md) logs in to Evennia for the first time, a new `Character` object is created and the [Account](./Accounts.md) will be set to _puppet_ it. By default this first Character will get the same name as the Account (but Evennia supports [alternative connection-styles](../Concepts/Connection-Styles.md) if so desired). + +A `Character` object will usually have a [Default Commandset](./Command-Sets.md) set on itself at creation, or the account will not be able to issue any in-game commands! + +If you want to change the default character created by the default commands, you can change it in settings: + + BASE_CHARACTER_TYPECLASS = "typeclasses.characters.Character" + +This deafult points at the empty class in `mygame/typeclasses/characters.py` , ready for you to modify as you please. \ No newline at end of file diff --git a/docs/source/Components/Components-Overview.md b/docs/source/Components/Components-Overview.md index 5218cddcc6..194a6cf2c1 100644 --- a/docs/source/Components/Components-Overview.md +++ b/docs/source/Components/Components-Overview.md @@ -13,6 +13,9 @@ Sessions.md Typeclasses.md Accounts.md Objects.md +Characters.md +Rooms.md +Exits.md Scripts.md Channels.md Msg.md diff --git a/docs/source/Components/Exits.md b/docs/source/Components/Exits.md new file mode 100644 index 0000000000..17770a2b76 --- /dev/null +++ b/docs/source/Components/Exits.md @@ -0,0 +1,55 @@ +# Exits + +**Inheritance Tree:** +``` +┌─────────────┐ +│DefaultObject│ +└─────▲───────┘ + │ +┌─────┴─────┐ +│DefaultExit│ +└─────▲─────┘ + │ ┌────────────┐ + │ ┌─────►ObjectParent│ + │ │ └────────────┘ + ┌─┴─┴┐ + │Exit│ + └────┘ +``` + +*Exits* are in-game [Objects](./Objects.md) connecting other objects (usually [Rooms](./Rooms.md)) together. + +An object named `north` or `in` might be exits, as well as `door`, `portal` or `jump out the window`. + +An exit has two things that separate them from other objects. +1. Their `.destination` property is set and points to a valid target location. This fact makes it easy and fast to locate exits in the database. +2. Exits define a special [Transit Command](./Commands.md) on themselves when they are created. This command is named the same as the exit object and will, when called, handle the practicalities of moving the character to the Exits's `.destination` - this allows you to just enter the name of the exit on its own to move around, just as you would expect. + +The default exit functionality is all defined on the [DefaultExit](DefaultExit) typeclass. You could in principle completely change how exits work in your game by overriding this - it's not recommended though, unless you really know what you are doing). + +Exits are [locked](./Locks.md) using an `access_type` called *traverse* and also make use of a few hook methods for giving feedback if the traversal fails. See `evennia.DefaultExit` for more info. + +Exits are normally overridden on a case-by-case basis, but if you want to change the default exit createad by rooms like `dig` , `tunnel` or `open` you can change it in settings: + + BASE_EXIT_TYPECLASS = "typeclasses.exits.Exit" + +In `mygame/typeclasses/exits.py` there is an empty `Exit` class for you to modify. + +### Exit details + +The process of traversing an exit is as follows: + +1. The traversing `obj` sends a command that matches the Exit-command name on the Exit object. The [cmdhandler](./Commands.md) detects this and triggers the command defined on the Exit. Traversal always involves the "source" (the current location) and the `destination` (this is stored on the Exit object). +1. The Exit command checks the `traverse` lock on the Exit object +1. The Exit command triggers `at_traverse(obj, destination)` on the Exit object. +1. In `at_traverse`, `object.move_to(destination)` is triggered. This triggers the following hooks, in order: + 1. `obj.at_pre_move(destination)` - if this returns False, move is aborted. + 1. `origin.at_pre_leave(obj, destination)` + 1. `obj.announce_move_from(destination)` + 1. Move is performed by changing `obj.location` from source location to `destination`. + 1. `obj.announce_move_to(source)` + 1. `destination.at_object_receive(obj, source)` + 1. `obj.at_post_move(source)` +1. On the Exit object, `at_post_traverse(obj, source)` is triggered. + +If the move fails for whatever reason, the Exit will look for an Attribute `err_traverse` on itself and display this as an error message. If this is not found, the Exit will instead call `at_failed_traverse(obj)` on itself. \ No newline at end of file diff --git a/docs/source/Components/Objects.md b/docs/source/Components/Objects.md index 789dcc444a..b547bf976b 100644 --- a/docs/source/Components/Objects.md +++ b/docs/source/Components/Objects.md @@ -1,5 +1,6 @@ # Objects +**Message-path:** ``` ┌──────┐ │ ┌───────┐ ┌───────┐ ┌──────┐ │Client├─┼──►│Session├───►│Account├──►│Object│ @@ -7,51 +8,63 @@ ^ ``` -All in-game objects in Evennia, be it characters, chairs, monsters, rooms or hand grenades are -represented by an Evennia *Object*. Objects form the core of Evennia and is probably what you'll -spend most time working with. Objects are [Typeclassed](./Typeclasses.md) entities. +All in-game objects in Evennia, be it characters, chairs, monsters, rooms or hand grenades are jointly referred to as an Evennia *Object*. An Object is generally something you can look and interact with in the game world. When a message travels from the client, the Object-level is the last stop. -An Evennia Object is, by definition, a Python class that includes [evennia.objects.objects.DefaultObject](evennia.objects.objects.DefaultObject) among its parents. Evennia defines several subclasses of `DefaultObject` in the following inheritance tree: +Objects form the core of Evennia and is probably what you'll spend most time working with. Objects are [Typeclassed](./Typeclasses.md) entities. +An Evennia Object is, by definition, a Python class that includes [evennia.objects.objects.DefaultObject](evennia.objects.objects.DefaultObject) among its parents. Evennia defines several subclasses of `DefaultObject`: + +- `Object` - the base in-game entity. Found in `mygame/typeclasses/objects.py`. Inherits directly from `DefaultObject`. +- [Characters](./Characters.md) - the normal in-game Character, controlled by a player. Found in `mygame/typeclasses/characters.py`. Inherits from `DefaultCharacter`, which is turn a child of `DefaultObject`. +- [Rooms](./Rooms.md) - a location in the game world. Found in `mygame/typeclasses/rooms.py`. Inherits from `DefaultRoom`, which is in turn a child of `DefaultObject`). +- [Exits](./Exits.md) - represents a one-way connection to another location. Found in `mygame/typeclasses/exits.py` (inherits from `DefaultExit`, which is in turn a child of `DefaultObject`). + +## Object + +**Inheritance Tree:** ``` - ┌────────────┐ - Evennia│ │ObjectParent│ - library│ └──────▲─────┘ -┌─────────────┐ │ ┌──────┐ │ -│DefaultObject◄────────────────────┼────┤Object├──────┤ -└──────▲──────┘ │ └──────┘ │ - │ ┌────────────────┐ │ ┌─────────┐ │ - ├────────┤DefaultCharacter◄─┼────┤Character├───┤ - │ └────────────────┘ │ └─────────┘ │ - │ ┌────────────────┐ │ ┌────┐ │ - ├────────┤DefaultRoom ◄─┼────┤Room├────────┤ - │ └────────────────┘ │ └────┘ │ - │ ┌────────────────┐ │ ┌────┐ │ - └────────┤DefaultExit ◄─┼────┤Exit├────────┘ - └────────────────┘ │ └────┘ - │Game-dir +┌─────────────┐ +│DefaultObject│ +└──────▲──────┘ + │ ┌────────────┐ + │ ┌─────►ObjectParent│ + │ │ └────────────┘ + ┌─┴─┴──┐ + │Object│ + └──────┘ ``` -Here, arrows indicate inheritance and point from-child-to-parent. +> For an explanation of `ObjectParent`, see next section. -So, for example `DefaultObject` is a child of `DefaultCharacter` (in the Evennia library), which is a parent of `Character` (in the game dir). The class in the game-dir is the one you should modify for your game. +The `Object` class is meant to be used as the basis for creating things that are neither characters, rooms or exits - anything from weapons and armour, equipment and houses can be represented by extending the Object class. Depending on your game, this also goes for NPCs and monsters (in some games you may want to treat NPCs as just an un-puppeted [Character](./Characters.md) instead). -> Note the `ObjectParent` class. This is an empty mix-in that all classes in the game-dir inherits from. It's where you put things you want _all_ these classes to have. +You should not use Objects for game _systems_. Don't use an 'invisible' Object for tracking weather, combat, economy or guild memberships - that's what [Scripts](./Scripts.md) are for. -- [evennia.objects.objects.DefaultCharacter](evennia.objects.objects.DefaultCharacter) - the normal in-game Character, controlled by a player. -- [evennia.objects.objects.DefaultRoom](evennia.objects.objects.DefaultRoom) - a location in the game world. -- [evennia.objects.objects.DefaultExit](evennia.objects.objects.DefaultExit) - an entity that in a location (usually a Room). It represents a one-way connection to another location. +## ObjectParent - Adding common functionality -Here are the import paths for the relevant child classes in the game dir +`Object`, as well as `Character`, `Room` and `Exit` classes all additionally inherit from `mygame.typeclasses.objects.ObjectParent`. -- `mygame.typeclasses.objects.Object` (inherits from `DefaultObject`) -- `mygame.typeclasses.characters.Character` (inherits from `DefaultCharacter`) -- `mygame.typeclasses.rooms.Room` (inherits from `DefaultRoom`) -- `mygame.typeclasses.exits.Exit` (inherits from `DefaultExit`) +`ObjectParent` is an empty 'mixin' class. You can add stuff to this class that you want _all_ in-game entities to have. -## Working with Objects +Here is an example: -You can easily add your own in-game behavior by either modifying one of the typeclasses in your game dir or by inheriting from them. +```python +# in mygame/typeclasses/objects.py +# ... + +from evennia.objects.objects import DefaultObject + +class ObjectParent: + def at_pre_get(self, getter, **kwargs): + # make all entities by default un-pickable + return False +``` + +Now all of `Object`, `Exit`. `Room` and `Character` default to not being able to be picked up using the `get` command. + +## Working with children of DefaultObject + +This functionality is shared by all sub-classes of `DefaultObject`. You can easily add your own in-game behavior by either modifying one of the typeclasses in your game dir or by inheriting further from them. You can put your new typeclass directly in the relevant module, or you could organize your code in some other way. Here we assume we make a new module `mygame/typeclasses/flowers.py`: @@ -82,15 +95,10 @@ What the `create` command actually *does* is to use the [evennia.create_object]( new_rose = create_object("typeclasses.flowers.Rose", key="MyRose") ``` -(The `create` command will auto-append the most likely path to your typeclass, if you enter the -call manually you have to give the full path to the class. The `create.create_object` function is -powerful and should be used for all coded object creating (so this is what you use when defining -your own building commands). - -This particular Rose class doesn't really do much, all it does it make sure the attribute -`desc`(which is what the `look` command looks for) is pre-set, which is pretty pointless since you -will usually want to change this at build time (using the `desc` command or using the [Spawner](./Prototypes.md)). +(The `create` command will auto-append the most likely path to your typeclass, if you enter the call manually you have to give the full path to the class. The `create.create_object` function is powerful and should be used for all coded object creating (so this is what you use when defining your own building commands). +This particular Rose class doesn't really do much, all it does it make sure the attribute `desc`(which is what the `look` command looks for) is pre-set, which is pretty pointless since you will usually want to change this at build time (using the `desc` command or using the [Spawner](./Prototypes.md)). + ### Properties and functions on Objects Beyond the properties assigned to all [typeclassed](./Typeclasses.md) objects (see that page for a list @@ -99,16 +107,14 @@ of those), the Object also has the following custom properties: - `aliases` - a handler that allows you to add and remove aliases from this object. Use `aliases.add()` to add a new alias and `aliases.remove()` to remove one. - `location` - a reference to the object currently containing this object. - `home` is a backup location. The main motivation is to have a safe place to move the object to if its `location` is destroyed. All objects should usually have a home location for safety. -- `destination` - this holds a reference to another object this object links to in some way. Its main use is for [Exits](./Objects.md#exits), it's otherwise usually unset. +- `destination` - this holds a reference to another object this object links to in some way. Its main use is for [Exits](./Exits.md), it's otherwise usually unset. - `nicks` - as opposed to aliases, a [Nick](./Nicks.md) holds a convenient nickname replacement for a real name, word or sequence, only valid for this object. This mainly makes sense if the Object is used as a game character - it can then store briefer shorts, example so as to quickly reference game commands or other characters. Use nicks.add(alias, realname) to add a new one. - `account` - this holds a reference to a connected [Account](./Accounts.md) controlling this object (if any). Note that this is set also if the controlling account is *not* currently online - to test if an account is online, use the `has_account` property instead. - `sessions` - if `account` field is set *and the account is online*, this is a list of all active sessions (server connections) to contact them through (it may be more than one if multiple connections are allowed in settings). - `has_account` - a shorthand for checking if an *online* account is currently connected to this object. - `contents` - this returns a list referencing all objects 'inside' this object (i,e. which has this object set as their `location`). - `exits` - this returns all objects inside this object that are *Exits*, that is, has the `destination` property set. - -The last two properties are special: - +- `appearance_template` - this helps formatting the look of the Object when someone looks at it (see next section).l - `cmdset` - this is a handler that stores all [command sets](./Command-Sets.md) defined on the object (if any). - `scripts` - this is a handler that manages [Scripts](./Scripts.md) attached to the object (if any). @@ -119,87 +125,40 @@ The Object also has a host of useful utility functions. See the function headers - `search()` - this is a convenient shorthand to search for a specific object, at a given location or globally. It's mainly useful when defining commands (in which case the object executing the command is named `caller` and one can do `caller.search()` to find objects in the room to operate on). - `execute_cmd()` - Lets the object execute the given string as if it was given on the command line. - `move_to` - perform a full move of this object to a new location. This is the main move method and will call all relevant hooks, do all checks etc. -- `clear_exits()` - will delete all [Exits](./Objects.md#exits) to *and* from this object. +- `clear_exits()` - will delete all [Exits](./Exits.md) to *and* from this object. - `clear_contents()` - this will not delete anything, but rather move all contents (except Exits) to their designated `Home` locations. - `delete()` - deletes this object, first calling `clear_exits()` and `clear_contents()`. +- `return_appearance` is the main hook letting the object visually describe itself. The Object Typeclass defines many more *hook methods* beyond `at_object_creation`. Evennia calls these hooks at various points. When implementing your custom objects, you will inherit from the base parent and overload these hooks with your own custom code. See `evennia.objects.objects` for an updated list of all the available hooks or the [API for DefaultObject here](evennia.objects.objects.DefaultObject). -## Characters +## Changing an Object's appearance -The [DefaultCharacters](evennia.objects.objects.DefaultCharacter) is the root class for player in-game entities. They are usually _puppeted_ by [Accounts](./Accounts.md). +When you type `look `, this is the sequence of events that happen: -When a new Account logs in to Evennia for the first time, a new `Character` object is created and the Account object is assigned to the `account` attribute (but Evennia supports [alternative connection-styles](../Concepts/Connection-Styles.md) if so desired). +1. The command checks if the `caller` of the command (the 'looker') passes the `view` [lock](./Locks.md) of the target `obj`. If not, they will not find anything to look at (this is how you make objects invisible). +1. The `look` command calls `caller.at_look(obj)` - that is, the `at_look` hook on the 'looker' (the caller of the command) is called to perform the look on the target object. The command will echo whatever this hook returns. +2. `caller.at_look` calls and returns the outcome of `obj.return_apperance(looker, **kwargs)`. Here `looker` is the `caller` of the command. In other words, we ask the `obj` to descibe itself to `looker`. +3. `obj.return_appearance` makes use of its `.appearance_template` property and calls a slew of helper-hooks to populate this template. This is how the template looks by default: -A `Character` object must have a [Default Commandset](./Command-Sets.md) set on itself at creation, or the account will not be able to issue any commands! + ```python + appearance_template = """ + {header} + |c{name}|n + {desc} + {exits}{characters}{things} + {footer} + """``` -If you want to change the default character created by the default commands, you can change it in settings: +4. Each field of the template is populated by a matching helper method (and their default returns): + - `name` -> `obj.get_display_name(looker, **kwargs)` - returns `obj.name`. + - `desc` -> `obj.get_display_desc(looker, **kwargs)` - returns `obj.db.desc`. + - `header` -> `obj.get_display_header(looker, **kwargs)` - empty by default. + - `footer` -> `obj.get_display_footer(looker, **kwargs)` - empty by default. + - `exits` -> `obj.get_display_exits(looker, **kwargs)` - a list of `DefaultExit`-inheriting objects found inside this object (usually only present if `obj` is a `Room`). + - `characters` -> `obj.get_display_characters(looker, **kwargs)` - a list of `DefaultCharacter`-inheriting entities inside this object. + - `things` -> `obj.get_display_things(looker, **kwargs)` - a list of all other Objects inside `obj`. +5. `obj.format_appearance(string, looker, **kwargs)` is the last step the populated template string goes through. This can be used for final adjustments, such as stripping whitespace. The return from this method is what the user will see. - BASE_CHARACTER_TYPECLASS = "typeclasses.characters.Character" - -This deafult points at the empty class in `mygame/typeclasses/characters.py` , ready for you to modify as you please. - -## Rooms - -[Rooms](evennia.objects.objects.DefaultRoom) are the root containers of all other objects. - -The only thing really separating a room from any other object is that they have no `location` of their own and that default commands like `dig` creates objects of this class - so if you want to expand your rooms with more functionality, just inherit from `evennia.DefaultRoom`. - -To change the default room created by `dig`, `tunnel` and other commands, change it in settings: - - BASE_ROOM_TYPECLASS = "typeclases.rooms.Room" - -The empty class in `mygame/typeclasses/rooms.py` is a good place to start! - -## Exits - -*Exits* are objects connecting other objects (usually *Rooms*) together. An object named *North* or *in* might be an exit, as well as *door*, *portal* or *jump out the window*. An exit has two things that separate them from other objects. Firstly, their *destination* property is set and points to a valid object. This fact makes it easy and fast to locate exits in the database. Secondly, exits define a special [Transit Command](./Commands.md) on themselves when they are created. This command is named the same as the exit object and will, when called, handle the practicalities of moving the character to the Exits's *destination* - this allows you to just enter the name of the exit on its own to move around, just as you would expect. - -The exit functionality is all defined on the Exit typeclass, so you could in principle completely change how exits work in your game (it's not recommended though, unless you really know what you are doing). Exits are [locked](./Locks.md) using an access_type called *traverse* and also make use of a few hook methods for giving feedback if the traversal fails. See `evennia.DefaultExit` for more info. - -Exits are normally overridden on a case-by-case basis, but if you want to change the default exit createad by rooms like `dig` , `tunnel` or `open` you can change it in settings: - - BASE_EXIT_TYPECLASS = "typeclasses.exits.Exit" - -In `mygame/typeclasses/exits.py` there is an empty `Exit` class for you to modify. - -### Exit details - -The process of traversing an exit is as follows: - -1. The traversing `obj` sends a command that matches the Exit-command name on the Exit object. The [cmdhandler](./Commands.md) detects this and triggers the command defined on the Exit. Traversal always involves the "source" (the current location) and the `destination` (this is stored on the Exit object). -1. The Exit command checks the `traverse` lock on the Exit object -1. The Exit command triggers `at_traverse(obj, destination)` on the Exit object. -1. In `at_traverse`, `object.move_to(destination)` is triggered. This triggers the following hooks, in order: - 1. `obj.at_pre_move(destination)` - if this returns False, move is aborted. - 1. `origin.at_pre_leave(obj, destination)` - 1. `obj.announce_move_from(destination)` - 1. Move is performed by changing `obj.location` from source location to `destination`. - 1. `obj.announce_move_to(source)` - 1. `destination.at_object_receive(obj, source)` - 1. `obj.at_post_move(source)` -1. On the Exit object, `at_post_traverse(obj, source)` is triggered. - -If the move fails for whatever reason, the Exit will look for an Attribute `err_traverse` on itself and display this as an error message. If this is not found, the Exit will instead call `at_failed_traverse(obj)` on itself. - -## Adding common functionality - -`Object`, `Character`, `Room` and `Exit` also inherit from `mygame.typeclasses.objects.ObjectParent`. -This is an empty 'mixin' class. Optionally, you can modify this class if you want to easily add some _common_ functionality to all your Objects, Characters, Rooms and Exits at once. You can still customize each subclass separately (see the Python docs on [multiple inheritance](https://docs.python.org/3/tutorial/classes.html#multiple-inheritance) for details). - -Here is an example: - -```python -# in mygame/typeclasses/objects.py -# ... - -from evennia.objects.objects import DefaultObject - -class ObjectParent: - def at_pre_get(self, getter, **kwargs): - # make all entities by default un-pickable - return False -``` - -Now all of `Object`, `Exit`. `Room` and `Character` default to not being able to be picked up using the `get` command. +As each of these hooks (and the template itself) can be overridden in your child class, you can customize your look extensively. You can also have objects look different depending on who is looking at them. The extra `**kwargs` are not used by default, but are there to allow you to pass extra data into the system if you need it (like light conditions etc.) \ No newline at end of file diff --git a/docs/source/Components/Rooms.md b/docs/source/Components/Rooms.md new file mode 100644 index 0000000000..1543c4629d --- /dev/null +++ b/docs/source/Components/Rooms.md @@ -0,0 +1,31 @@ + +# Rooms + +**Inheritance Tree:** +``` +┌─────────────┐ +│DefaultObject│ +└─────▲───────┘ + │ +┌─────┴─────┐ +│DefaultRoom│ +└─────▲─────┘ + │ ┌────────────┐ + │ ┌─────►ObjectParent│ + │ │ └────────────┘ + ┌─┴─┴┐ + │Room│ + └────┘ +``` + +[Rooms](evennia.objects.objects.DefaultRoom) are in-game [Objects](./Objects.md) representing the root containers of all other objects. + +The only thing technically separating a room from any other object is that they have no `location` of their own and that default commands like `dig` creates objects of this class - so if you want to expand your rooms with more functionality, just inherit from `evennia.DefaultRoom`. + +To change the default room created by `dig`, `tunnel` and other default commands, change it in settings: + + BASE_ROOM_TYPECLASS = "typeclases.rooms.Room" + +The empty class in `mygame/typeclasses/rooms.py` is a good place to start! + +While the default Room is very simple, there are several Evennia [contribs](../Contribs/Contribs-Overview.md) customizing and extending rooms with more functionality. \ No newline at end of file diff --git a/docs/source/Contribs/Contrib-Ingame-Python.md b/docs/source/Contribs/Contrib-Ingame-Python.md index 6deaf769a7..7e2dd05559 100644 --- a/docs/source/Contribs/Contrib-Ingame-Python.md +++ b/docs/source/Contribs/Contrib-Ingame-Python.md @@ -39,7 +39,7 @@ using ingame-python events. defines the context in which we would like to call some arbitrary code. For instance, one event is defined on exits and will fire every time a character traverses through this exit. Events are described on a [typeclass](../Components/Typeclasses.md) -([exits](../Components/Objects.md#exits) in our example). All objects inheriting from this +([exits](../Components/Exits.md) in our example). All objects inheriting from this typeclass will have access to this event. - **Callbacks** can be set on individual objects, on events defined in code. These **callbacks** can contain arbitrary code and describe a specific diff --git a/docs/source/Evennia-API.md b/docs/source/Evennia-API.md index a187164dfa..e0c588b7ae 100644 --- a/docs/source/Evennia-API.md +++ b/docs/source/Evennia-API.md @@ -50,9 +50,9 @@ The flat API is defined in `__init__.py` [viewable here](github:evennia/__init__ - [evennia.DefaultAccount](evennia.accounts.accounts.DefaultAccount) - player account class ([docs](Components/Accounts.md)) - [evennia.DefaultGuest](evennia.accounts.accounts.DefaultGuest) - base guest account class - [evennia.DefaultObject](evennia.objects.objects.DefaultObject) - base class for all objects ([docs](Components/Objects.md)) -- [evennia.DefaultCharacter](evennia.objects.objects.DefaultCharacter) - base class for in-game characters ([docs](Components/Objects.md#characters)) -- [evennia.DefaultRoom](evennia.objects.objects.DefaultRoom) - base class for rooms ([docs](Components/Objects.md#rooms)) -- [evennia.DefaultExit](evennia.objects.objects.DefaultExit) - base class for exits ([docs](Components/Objects.md#exits)) +- [evennia.DefaultCharacter](evennia.objects.objects.DefaultCharacter) - base class for in-game characters ([docs](Components/Characters.md)) +- [evennia.DefaultRoom](evennia.objects.objects.DefaultRoom) - base class for rooms ([docs](Components/Rooms.md)) +- [evennia.DefaultExit](evennia.objects.objects.DefaultExit) - base class for exits ([docs](Components/Exits.md)) - [evennia.DefaultScript](evennia.scripts.scripts.DefaultScript) - base class for OOC-objects ([docs](Components/Scripts.md)) - [evennia.DefaultChannel](evennia.comms.comms.DefaultChannel) - base class for in-game channels ([docs](Components/Channels.md)) diff --git a/evennia/contrib/base_systems/ingame_python/README.md b/evennia/contrib/base_systems/ingame_python/README.md index 115d387c93..43f6a6f276 100644 --- a/evennia/contrib/base_systems/ingame_python/README.md +++ b/evennia/contrib/base_systems/ingame_python/README.md @@ -39,7 +39,7 @@ using ingame-python events. defines the context in which we would like to call some arbitrary code. For instance, one event is defined on exits and will fire every time a character traverses through this exit. Events are described on a [typeclass](Typeclasses) -([exits](Objects#exits) in our example). All objects inheriting from this +([exits](Exits) in our example). All objects inheriting from this typeclass will have access to this event. - **Callbacks** can be set on individual objects, on events defined in code. These **callbacks** can contain arbitrary code and describe a specific From a9bf05734e137435a2cc016c0d751f84064ce2dd Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 19 Mar 2023 23:13:46 +0100 Subject: [PATCH 29/32] Update tickerhandler docs. Resolve #3148 --- docs/source/Components/TickerHandler.md | 88 ++++++++----------------- 1 file changed, 27 insertions(+), 61 deletions(-) diff --git a/docs/source/Components/TickerHandler.md b/docs/source/Components/TickerHandler.md index 6362f03243..329132d940 100644 --- a/docs/source/Components/TickerHandler.md +++ b/docs/source/Components/TickerHandler.md @@ -1,28 +1,20 @@ # TickerHandler -One way to implement a dynamic MUD is by using "tickers", also known as "heartbeats". A ticker is a -timer that fires ("ticks") at a given interval. The tick triggers updates in various game systems. +One way to implement a dynamic MUD is by using "tickers", also known as "heartbeats". A ticker is a timer that fires ("ticks") at a given interval. The tick triggers updates in various game systems. -## About Tickers +Tickers are very common or even unavoidable in other mud code bases. Certain code bases are even hard-coded to rely on the concept of the global 'tick'. Evennia has no such notion - the decision to use tickers is very much up to the need of your game and which requirements you have. The "ticker recipe" is just one way of cranking the wheels. -Tickers are very common or even unavoidable in other mud code bases. Certain code bases are even -hard-coded to rely on the concept of the global 'tick'. Evennia has no such notion - the decision to -use tickers is very much up to the need of your game and which requirements you have. The "ticker -recipe" is just one way of cranking the wheels. +The most fine-grained way to manage the flow of time is to use [utils.delay](evennia.utils.utils.delay) (using the [TaskHandler](evennia.scripts.taskhandler.TaskHandler)). Another is to use the time-repeat capability of [Scripts](./Scripts.md). These tools operate on individual objects. + +Many types of operations (weather being the classic example) are however done on multiple objects in the same way at regular intervals, and for this, it's inefficient to set up separate delays/scripts for every such object. -The most fine-grained way to manage the flow of time is of course to use [Scripts](./Scripts.md). Many -types of operations (weather being the classic example) are however done on multiple objects in the -same way at regular intervals, and for this, storing separate Scripts on each object is inefficient. The way to do this is to use a ticker with a "subscription model" - let objects sign up to be -triggered at the same interval, unsubscribing when the updating is no longer desired. +triggered at the same interval, unsubscribing when the updating is no longer desired. This means that the time-keeping mechanism is only set up once for all objects, making subscribing/unsubscribing faster. -Evennia offers an optimized implementation of the subscription model - the *TickerHandler*. This is -a singleton global handler reachable from `evennia.TICKER_HANDLER`. You can assign any *callable* (a -function or, more commonly, a method on a database object) to this handler. The TickerHandler will -then call this callable at an interval you specify, and with the arguments you supply when adding -it. This continues until the callable un-subscribes from the ticker. The handler survives a reboot -and is highly optimized in resource usage. +Evennia offers an optimized implementation of the subscription model - the *TickerHandler*. This is a singleton global handler reachable from [evennia.TICKER_HANDLER](evennia.utils.tickerhandler.TickerHandler). You can assign any *callable* (a function or, more commonly, a method on a database object) to this handler. The TickerHandler will then call this callable at an interval you specify, and with the arguments you supply when adding it. This continues until the callable un-subscribes from the ticker. The handler survives a reboot and is highly optimized in resource usage. + +## Usage Here is an example of importing `TICKER_HANDLER` and using it: @@ -32,10 +24,13 @@ Here is an example of importing `TICKER_HANDLER` and using it: tickerhandler.add(20, obj.at_tick) ``` - That's it - from now on, `obj.at_tick()` will be called every 20 seconds. -You can also import function and tick that: +```{important} +Everything you supply to `TickerHandler.add` will need to be pickled at some point to be saved into the database - also if you use `persistent=False`. Most of the time the handler will correctly store things like database objects, but the same restrictions as for [Attributes](./Attributes.md) apply to what the TickerHandler may store. +``` + +You can also import a function and tick that: ```python from evennia import TICKER_HANDLER as tickerhandler @@ -51,9 +46,7 @@ Removing (stopping) the ticker works as expected: tickerhandler.remove(30, myfunc) ``` -Note that you have to also supply `interval` to identify which subscription to remove. This is -because the TickerHandler maintains a pool of tickers and a given callable can subscribe to be -ticked at any number of different intervals. +Note that you have to also supply `interval` to identify which subscription to remove. This is because the TickerHandler maintains a pool of tickers and a given callable can subscribe to be ticked at any number of different intervals. The full definition of the `tickerhandler.add` method is @@ -63,74 +56,47 @@ The full definition of the `tickerhandler.add` method is ``` Here `*args` and `**kwargs` will be passed to `callback` every `interval` seconds. If `persistent` -is `False`, this subscription will not survive a server reload. +is `False`, this subscription will be wiped by a _server shutdown_ (it will still survive a normal reload). -Tickers are identified and stored by making a key of the callable itself, the ticker-interval, the -`persistent` flag and the `idstring` (the latter being an empty string when not given explicitly). +Tickers are identified and stored by making a key of the callable itself, the ticker-interval, the `persistent` flag and the `idstring` (the latter being an empty string when not given explicitly). -Since the arguments are not included in the ticker's identification, the `idstring` must be used to -have a specific callback triggered multiple times on the same interval but with different arguments: +Since the arguments are not included in the ticker's identification, the `idstring` must be used to have a specific callback triggered multiple times on the same interval but with different arguments: ```python tickerhandler.add(10, obj.update, "ticker1", True, 1, 2, 3) tickerhandler.add(10, obj.update, "ticker2", True, 4, 5) ``` -> Note that, when we want to send arguments to our callback within a ticker handler, we need to -specify `idstring` and `persistent` before, unless we call our arguments as keywords, which would -often be more readable: +> Note that, when we want to send arguments to our callback within a ticker handler, we need to specify `idstring` and `persistent` before, unless we call our arguments as keywords, which would often be more readable: ```python tickerhandler.add(10, obj.update, caller=self, value=118) ``` If you add a ticker with exactly the same combination of callback, interval and idstring, it will -overload the existing ticker. This identification is also crucial for later removing (stopping) the -subscription: +overload the existing ticker. This identification is also crucial for later removing (stopping) the subscription: ```python tickerhandler.remove(10, obj.update, idstring="ticker1") tickerhandler.remove(10, obj.update, idstring="ticker2") ``` -The `callable` can be on any form as long as it accepts the arguments you give to send to it in -`TickerHandler.add`. +The `callable` can be on any form as long as it accepts the arguments you give to send to it in `TickerHandler.add`. -> Note that everything you supply to the TickerHandler will need to be pickled at some point to be -saved into the database. Most of the time the handler will correctly store things like database -objects, but the same restrictions as for [Attributes](./Attributes.md) apply to what the TickerHandler -may store. - -When testing, you can stop all tickers in the entire game with `tickerhandler.clear()`. You can also -view the currently subscribed objects with `tickerhandler.all()`. +When testing, you can stop all tickers in the entire game with `tickerhandler.clear()`. You can also view the currently subscribed objects with `tickerhandler.all()`. See the [Weather Tutorial](../Howtos/Tutorial-Weather-Effects.md) for an example of using the TickerHandler. ### When *not* to use TickerHandler -Using the TickerHandler may sound very useful but it is important to consider when not to use it. -Even if you are used to habitually relying on tickers for everything in other code bases, stop and -think about what you really need it for. This is the main point: +Using the TickerHandler may sound very useful but it is important to consider when not to use it. Even if you are used to habitually relying on tickers for everything in other code bases, stop and think about what you really need it for. This is the main point: > You should *never* use a ticker to catch *changes*. -Think about it - you might have to run the ticker every second to react to the change fast enough. -Most likely nothing will have changed at a given moment. So you are doing pointless calls (since -skipping the call gives the same result as doing it). Making sure nothing's changed might even be -computationally expensive depending on the complexity of your system. Not to mention that you might -need to run the check *on every object in the database*. Every second. Just to maintain status quo -... +Think about it - you might have to run the ticker every second to react to the change fast enough. Most likely nothing will have changed at a given moment. So you are doing pointless calls (since skipping the call gives the same result as doing it). Making sure nothing's changed might even be computationally expensive depending on the complexity of your system. Not to mention that you might need to run the check *on every object in the database*. Every second. Just to maintain status quo ... -Rather than checking over and over on the off-chance that something changed, consider a more -proactive approach. Could you implement your rarely changing system to *itself* report when its -status changes? It's almost always much cheaper/efficient if you can do things "on demand". Evennia -itself uses hook methods for this very reason. +Rather than checking over and over on the off-chance that something changed, consider a more proactive approach. Could you implement your rarely changing system to *itself* report when its status changes? It's almost always much cheaper/efficient if you can do things "on demand". Evennia itself uses hook methods for this very reason. -So, if you consider a ticker that will fire very often but which you expect to have no effect 99% of -the time, consider handling things things some other way. A self-reporting on-demand solution is -usually cheaper also for fast-updating properties. Also remember that some things may not need to be -updated until someone actually is examining or using them - any interim changes happening up to that -moment are pointless waste of computing time. +So, if you consider a ticker that will fire very often but which you expect to have no effect 99% of the time, consider handling things things some other way. A self-reporting on-demand solution is usually cheaper also for fast-updating properties. Also remember that some things may not need to be updated until someone actually is examining or using them - any interim changes happening up to that moment are pointless waste of computing time. -The main reason for needing a ticker is when you want things to happen to multiple objects at the -same time without input from something else. \ No newline at end of file +The main reason for needing a ticker is when you want things to happen to multiple objects at the same time without input from something else. \ No newline at end of file From 1cb37ccc711a89a55a67c719b4941253abdbfb12 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 23 Mar 2023 23:19:14 +0100 Subject: [PATCH 30/32] Fix bug in logger. Resolve #3149 --- evennia/utils/logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/utils/logger.py b/evennia/utils/logger.py index eccee3b3d3..19b5e1214d 100644 --- a/evennia/utils/logger.py +++ b/evennia/utils/logger.py @@ -38,7 +38,7 @@ def _log(msg, logfunc, prefix="", **kwargs): try: msg = str(msg) except Exception as err: - msg = str(e) + msg = str(err) if kwargs: logfunc(msg, **kwargs) else: From 61b00eb2c631a06ea5c5f21eefd66a337d245eab Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 23 Mar 2023 23:31:29 +0100 Subject: [PATCH 31/32] Fix errors in wilderness contrib docs. Resolve #3144 --- docs/source/Contribs/Contrib-Wilderness.md | 17 ++++++++++++----- docs/source/Contribs/Contribs-Overview.md | 2 +- evennia/contrib/grid/wilderness/README.md | 17 ++++++++++++----- evennia/contrib/grid/wilderness/wilderness.py | 17 +++++++++-------- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/docs/source/Contribs/Contrib-Wilderness.md b/docs/source/Contribs/Contrib-Wilderness.md index 8a53fd7bcc..57d0ae161e 100644 --- a/docs/source/Contribs/Contrib-Wilderness.md +++ b/docs/source/Contribs/Contrib-Wilderness.md @@ -3,7 +3,7 @@ Contribution by titeuf87, 2017 This contrib provides a wilderness map without actually creating a large number -of rooms - as you move, you instead end up back in the same room but its description +of rooms - as you move, you instead end up back in the same room but its description changes. This means you can make huge areas with little database use as long as the rooms are relatively similar (e.g. only the names/descs changing). @@ -19,11 +19,11 @@ with their own name. If no name is provided, then a default one is used. Interna the wilderness is stored as a Script with the name you specify. If you don't specify the name, a script named "default" will be created and used. - @py from evennia.contrib.grid import wilderness; wilderness.create_wilderness() + py from evennia.contrib.grid import wilderness; wilderness.create_wilderness() Once created, it is possible to move into that wilderness map: - @py from evennia.contrib.grid import wilderness; wilderness.enter_wilderness(me) + py from evennia.contrib.grid import wilderness; wilderness.enter_wilderness(me) All coordinates used by the wilderness map are in the format of `(x, y)` tuples. x goes from left to right and y goes from bottom to top. So `(0, 0)` @@ -102,14 +102,21 @@ class PyramidMapProvider(wilderness.WildernessMapProvider): desc = "This is a room in the pyramid." if y == 3 : desc = "You can see far and wide from the top of the pyramid." - room.ndb.desc = desc + room.ndb.active_desc = desc ``` +Note that the currently active description is stored as `.ndb.active_desc`. When +looking at the room, this is what will be pulled and shown. + +> Exits on a room are always present, but locks hide those not used for a +> location. So make sure to `quell` if you are a superuser (since the superuser ignores +> locks, those exits will otherwise not be hidden) + Now we can use our new pyramid-shaped wilderness map. From inside Evennia we create a new wilderness (with the name "default") but using our new map provider: py from world import pyramid as p; p.wilderness.create_wilderness(mapprovider=p.PyramidMapProvider()) - py from evennia.contrib import wilderness; wilderness.enter_wilderness(me, coordinates=(4, 1)) + py from evennia.contrib.grid import wilderness; wilderness.enter_wilderness(me, coordinates=(4, 1)) ## Implementation details diff --git a/docs/source/Contribs/Contribs-Overview.md b/docs/source/Contribs/Contribs-Overview.md index f7d1442406..3a5d025929 100644 --- a/docs/source/Contribs/Contribs-Overview.md +++ b/docs/source/Contribs/Contribs-Overview.md @@ -494,7 +494,7 @@ and abort an ongoing traversal, respectively. _Contribution by titeuf87, 2017_ This contrib provides a wilderness map without actually creating a large number -of rooms - as you move, you instead end up back in the same room but its description +of rooms - as you move, you instead end up back in the same room but its description changes. This means you can make huge areas with little database use as long as the rooms are relatively similar (e.g. only the names/descs changing). diff --git a/evennia/contrib/grid/wilderness/README.md b/evennia/contrib/grid/wilderness/README.md index 261060eceb..ed4c194a80 100644 --- a/evennia/contrib/grid/wilderness/README.md +++ b/evennia/contrib/grid/wilderness/README.md @@ -3,7 +3,7 @@ Contribution by titeuf87, 2017 This contrib provides a wilderness map without actually creating a large number -of rooms - as you move, you instead end up back in the same room but its description +of rooms - as you move, you instead end up back in the same room but its description changes. This means you can make huge areas with little database use as long as the rooms are relatively similar (e.g. only the names/descs changing). @@ -19,11 +19,11 @@ with their own name. If no name is provided, then a default one is used. Interna the wilderness is stored as a Script with the name you specify. If you don't specify the name, a script named "default" will be created and used. - @py from evennia.contrib.grid import wilderness; wilderness.create_wilderness() + py from evennia.contrib.grid import wilderness; wilderness.create_wilderness() Once created, it is possible to move into that wilderness map: - @py from evennia.contrib.grid import wilderness; wilderness.enter_wilderness(me) + py from evennia.contrib.grid import wilderness; wilderness.enter_wilderness(me) All coordinates used by the wilderness map are in the format of `(x, y)` tuples. x goes from left to right and y goes from bottom to top. So `(0, 0)` @@ -102,14 +102,21 @@ class PyramidMapProvider(wilderness.WildernessMapProvider): desc = "This is a room in the pyramid." if y == 3 : desc = "You can see far and wide from the top of the pyramid." - room.ndb.desc = desc + room.ndb.active_desc = desc ``` +Note that the currently active description is stored as `.ndb.active_desc`. When +looking at the room, this is what will be pulled and shown. + +> Exits on a room are always present, but locks hide those not used for a +> location. So make sure to `quell` if you are a superuser (since the superuser ignores +> locks, those exits will otherwise not be hidden) + Now we can use our new pyramid-shaped wilderness map. From inside Evennia we create a new wilderness (with the name "default") but using our new map provider: py from world import pyramid as p; p.wilderness.create_wilderness(mapprovider=p.PyramidMapProvider()) - py from evennia.contrib import wilderness; wilderness.enter_wilderness(me, coordinates=(4, 1)) + py from evennia.contrib.grid import wilderness; wilderness.enter_wilderness(me, coordinates=(4, 1)) ## Implementation details diff --git a/evennia/contrib/grid/wilderness/wilderness.py b/evennia/contrib/grid/wilderness/wilderness.py index 5607f3f4c3..78e0d6d5cd 100644 --- a/evennia/contrib/grid/wilderness/wilderness.py +++ b/evennia/contrib/grid/wilderness/wilderness.py @@ -92,9 +92,16 @@ class PyramidMapProvider(wilderness.WildernessMapProvider): desc = "This is a room in the pyramid." if y == 3 : desc = "You can see far and wide from the top of the pyramid." - room.ndb.desc = desc + room.ndb.active_desc = desc ``` +Note that the currently active description is stored as `.ndb.active_desc`. When +looking at the room, this is what will be pulled and shown. + +> Exits on a room are always present, but locks hide those not used for a +> location. So make sure to `quell` if you are a superuser (since the superuser ignores +> locks, those exits will otherwise not be hidden) + Now we can use our new pyramid-shaped wilderness map. From inside Evennia we create a new wilderness (with the name "default") but using our new map provider: @@ -116,13 +123,7 @@ create a new wilderness (with the name "default") but using our new map provider """ -from evennia import ( - DefaultExit, - DefaultRoom, - DefaultScript, - create_object, - create_script, -) +from evennia import DefaultExit, DefaultRoom, DefaultScript, create_object, create_script from evennia.typeclasses.attributes import AttributeProperty from evennia.utils import inherits_from From 1e21fbd3923839beae007e7dca999dc9d8b554c5 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 23 Mar 2023 23:40:34 +0100 Subject: [PATCH 32/32] Fix mention of guest in permission docs. Resolve #3141 --- CHANGELOG.md | 1 + docs/source/Components/Permissions.md | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a0fa4856d..3e475f97ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Fix: More unit tests for scripts (Storsorken) - Docs: Made separate doc pages for Exits, Characters and Rooms. Expanded on how to change the description of an in-game object with templating. +- Docs: Fixed a multitude of doc issues. ## Evennia 1.2.1 diff --git a/docs/source/Components/Permissions.md b/docs/source/Components/Permissions.md index 1e3e39e025..c117b5de53 100644 --- a/docs/source/Components/Permissions.md +++ b/docs/source/Components/Permissions.md @@ -55,7 +55,8 @@ Selected permission strings can be organized in a *permission hierarchy* by edit `settings.PERMISSION_HIERARCHY`. Evennia's default permission hierarchy is as follows (in increasing order of power): - Player # can chat and send tells (default level) (lowest) + Guest # temporary account, only used if GUEST_ENABLED=True (lowest) + Player # can chat and send tells (default level) Helper # can edit help files Builder # can edit the world Admin # can administrate accounts @@ -63,9 +64,9 @@ Selected permission strings can be organized in a *permission hierarchy* by edit (Besides being case-insensitive, hierarchical permissions also understand the plural form, so you could use `Developers` and `Developer` interchangeably). -> There is also a `Guest` level below `Player` that is only active if `settings.GUEST_ENABLED` is set. The Guest is is never part of `settings.PERMISSION_HIERARCHY`. +When checking a hierarchical permission (using one of the methods to follow), you will pass checks for your level *and below*. That is, if you have the "Admin" hierarchical permission, you will also pass checks asking for "Builder", "Helper" and so on. -When checking a hierarchical permission (using one of the methods to follow), you will pass checks for your level and all *below* you. That is, even if the check explicitly checks for "Builder" level access, you will actually pass if you have one of "Builder", "Admin" or "Developer". By contrast, if you check for a non-hierarchical permission, like "Blacksmith" you *must* have exactly that permission to pass. +By contrast, if you check for a non-hierarchical permission, like "Blacksmith" you must have *exactly* that permission to pass. ### Checking permissions