[BUG - Develop] xyzgrid spawn doesn't call at_object_creation
#### Describe the bug
When creating rooms in the game using the xyzgrid module and a custom typeclass, the `at_object_creation` hook is never called.

#### To Reproduce
Steps to reproduce the behavior:
1. Create custom room and exit classes that inherit from the XYZGrid room and exits
2. Add a simple `at_object_creation` hook to each class with something easily identifiable, e.g. setting a new attribute to `True`
3. Create a basic xyzgrid map module using those room and exit classes
4. Add the map module and spawn using the xyzgrid launcher commands.
5. Go into the game and examine any spawned room or exit. Everything looks fine _except that_ the new attribute is not there.

#### Develop-branch commit
473f2e379

#### Additional context
I tested with these bare-bones classes, but on the more complicated classes where I discovered it, all of the other hooks I overloaded worked correctly.
```python
# Room
class CustomXYZRoom(XYZRoom):
    def at_object_creation(self):
        self.db.checkmark = True

# Exit
class CustomXYZExit(XYZExit):
    def at_object_creation(self):
        self.db.checkmark = True
```
My rooms and exits using those classes look like this, suggesting the hook never ran:
```
Name/key: A major street (#13)
Typeclass: CustomXYZRoom (typeclasses.rooms.CustomXYZRoom)
Home: Limbo (#2)
Locks:   call:true(); control:id(13) or perm(Admin);  delete:id(13) or perm(Admin);
 drop:holds();  edit:id(13) or perm(Admin); examine:perm(Builder);
 get:false(); puppet:false(); tell:perm(Admin); view:all()
Tags:   1[room_y_coordinate], 5[room_x_coordinate], main_streets[zone],
 prototype-4ee42f8[from_prototype], the city[room_z_coordinate]
Persistent Attributes:
  desc=A wide cobbled street.
Exits: west(#84), east(#85), north(#86), southeast(#87)
Characters: admin(#1)

Name/key: east (#85)
Aliases: e
Typeclass: CustomXYZExit (typeclasses.exits.CustomXYZExit)
Location: A major street (#13)
Home: Limbo (#2)
Destination: A street in Spiritgate (#14)
Locks:   call:true(); control:id(85) or perm(Admin);  delete:id(85) or perm(Admin);
 drop:holds();  edit:id(85) or perm(Admin); examine:perm(Builder);
 get:false(); puppet:false(); tell:perm(Admin); traverse:all(); view:all()
Stored Cmdset(s):
  evennia.commands.cmdset.CmdSet [ExitCmdSet] (Union, prio 101, duplicates: T)
Merged Cmdset(s):
  evennia.commands.cmdset.CmdSet [ExitCmdSet] (Union, prio 101, duplicates: T)
Commands vailable to east (result of Merged Cmdset(s)):
  east, north, southeast, west
Tags:   1[exit_dest_y_coordinate], 1[room_y_coordinate], 5[room_x_coordinate],
 7[exit_dest_x_coordinate], prototype-3c7b431[from_prototype], the
 city[exit_dest_z_coordinate], the city[room_z_coordinate]
Persistent Attributes:
  desc=A path to the next location.
```
diff --git a/evennia/contrib/grid/xyzgrid/README.md b/evennia/contrib/grid/xyzgrid/README.md
index 02cc11836e..3a2ab909f2 100644
--- a/evennia/contrib/grid/xyzgrid/README.md
+++ b/evennia/contrib/grid/xyzgrid/README.md
@@ -53,28 +53,31 @@ Exits: northeast and east
         (then go back to your mygame/ folder)
 
    This will install all optional requirements of Evennia.
-2. Import and add the `evennia.contrib.commands.XYZGridCmdSet` to the
+2. Import and [add] the `evennia.contrib.grid.xyzgrid.commands.XYZGridCmdSet` to the
    `CharacterCmdset` cmdset in `mygame/commands.default_cmds.py`. Reload
    the server. This makes the `map`, `goto/path` and the modified `teleport` and
    `open` commands available in-game.
-3. Edit `mygame/server/conf/settings.py` and add
 
-       EXTRA_LAUNCHER_COMMANDS['xyzgrid'] = 'evennia.contrib.launchcmd.xyzcommand'
+[add]: docs/source/Command-Sets.md#defining-command-sets
 
-   and
+3. Edit `mygame/server/conf/settings.py` and add
 
-       PROTOTYPE_MODULES += [’evennia.contrib.grid.xyzgrid.prototypes’]
+       EXTRA_LAUNCHER_COMMANDS['xyzgrid'] = 'evennia.contrib.grid.xyzgrid.launchcmd.xyzcommand'
+       PROTOTYPE_MODULES += ['evennia.contrib.grid.xyzgrid.prototypes']
 
    This will add the new ability to enter `evennia xyzgrid <option>` on the
    command line.  It will also make the `xyz_room` and `xyz_exit` prototypes
    available for use as prototype-parents when spawning the grid.
+
 4. Run `evennia xyzgrid help` for available options.
+
 5. (Optional): By default, the xyzgrid will only spawn module-based
-   [prototypes](../Components/Prototypes.md). This is an optimization and usually makes sense
+   [prototypes]. This is an optimization and usually makes sense
    since the grid is entirely defined outside the game anyway. If you want to
    also make use of in-game (db-) created prototypes, add
    `XYZGRID_USE_DB_PROTOTYPES = True` to settings.
 
+[prototypes]: ../Components/Prototypes.md
 
 ## Overview
 
diff --git a/evennia/contrib/grid/xyzgrid/tests.py b/evennia/contrib/grid/xyzgrid/tests.py
index 8e3d727625..8abda9c0fa 100644
--- a/evennia/contrib/grid/xyzgrid/tests.py
+++ b/evennia/contrib/grid/xyzgrid/tests.py
@@ -3,6 +3,7 @@
 Tests for the XYZgrid system.
 
 """
+from unittest import mock
 
 from random import randint
 from parameterized import parameterized
@@ -1415,3 +1416,78 @@ class TestBuildExampleGrid(BaseEvenniaTest):
         self.assertTrue(room2a.db.desc.startswith("This is the entrance to"))
         self.assertEqual(room2b.key, "North-west corner of the atrium")
         self.assertTrue(room2b.db.desc.startswith("Sunlight sifts down"))
+
+
+mock_room_callbacks = mock.MagicMock()
+mock_exit_callbacks = mock.MagicMock()
+
+class TestXyzRoom(xyzroom.XYZRoom):
+  def at_object_creation(self):
+    mock_room_callbacks.at_object_creation()
+
+class TestXyzExit(xyzroom.XYZExit):
+  def at_object_creation(self):
+    mock_exit_callbacks.at_object_creation()
+
+MAP_DATA = {
+  "map": """
+
+    + 0 1
+
+    0 #-#
+
+    + 0 1
+
+  """,
+  "zcoord": "map1",
+  "prototypes": {
+     ("*", "*"): {
+       "key": "room",
+       "desc": "A room.",
+       "prototype_parent": "xyz_room",
+     },
+     ("*", "*", "*"): {
+       "desc": "A passage.",
+       "prototype_parent": "xyz_exit",
+     }
+  },
+  "options": {
+    "map_visual_range": 1,
+    "map_mode": "scan",
+  }
+}
+
+class TestCallbacks(BaseEvenniaTest):
+    def setUp(self):
+        super().setUp()
+        mock_room_callbacks.reset_mock()
+        mock_exit_callbacks.reset_mock()
+        
+    def setup_grid(self, map_data):
+        self.grid, err = xyzgrid.XYZGrid.create("testgrid")
+
+        def _log(msg):
+             print(msg)
+        self.grid.log = _log
+
+        self.map_data = map_data
+        self.grid.add_maps(map_data)
+
+    def tearDown(self):
+        super().tearDown()
+        self.grid.delete()
+
+    def test_typeclassed_xyzroom_and_xyzexit_with_at_object_creation_are_called(self):
+        map_data = dict(MAP_DATA)
+        for prototype_key, prototype_value in map_data["prototypes"].items():
+            if len(prototype_key) == 2:
+                prototype_value["typeclass"] = "evennia.contrib.grid.xyzgrid.tests.TestXyzRoom"
+            if len(prototype_key) == 3:
+                prototype_value["typeclass"] = "evennia.contrib.grid.xyzgrid.tests.TestXyzExit"
+        self.setup_grid(map_data)
+
+        self.grid.spawn()
+
+        # Two rooms and 2 exits, Each one should have gotten one `at_object_creation` callback.
+        self.assertEqual(mock_room_callbacks.at_object_creation.mock_calls, [mock.call(), mock.call()])
+        self.assertEqual(mock_exit_callbacks.at_object_creation.mock_calls, [mock.call(), mock.call()])
diff --git a/evennia/contrib/grid/xyzgrid/xymap_legend.py b/evennia/contrib/grid/xyzgrid/xymap_legend.py
index ff5ea6b0b7..21f820f0c4 100644
--- a/evennia/contrib/grid/xyzgrid/xymap_legend.py
+++ b/evennia/contrib/grid/xyzgrid/xymap_legend.py
@@ -22,8 +22,9 @@ from collections import defaultdict
 
 from django.core import exceptions as django_exceptions
 from evennia.prototypes import spawner
+from evennia.utils.utils import class_from_module
 
-from .utils import MAPSCAN, REVERSE_DIRECTIONS, MapParserError, BIGVAL
+from .utils import MAPSCAN, REVERSE_DIRECTIONS, MapParserError, BIGVAL, MapError
 
 NodeTypeclass = None
 ExitTypeclass = None
@@ -316,13 +317,14 @@ class MapNode:
         try:
             nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz)
         except django_exceptions.ObjectDoesNotExist:
-            # create a new entity with proper coordinates etc
-            tclass = self.prototype["typeclass"]
-            tclass = (
-                f" ({tclass})" if tclass != "evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom" else ""
-            )
-            self.log(f"  spawning room at xyz={xyz}{tclass}")
-            nodeobj, err = NodeTypeclass.create(self.prototype.get("key", "An empty room"), xyz=xyz)
+            # create a new entity, using the specified typeclass (if there's one) and
+            # with proper coordinates etc
+            typeclass = self.prototype.get("typeclass")
+            if typeclass is None:
+                raise MapError(f"The prototype {self.prototype} for this node has no 'typeclass' key.", self)
+            self.log(f"  spawning room at xyz={xyz} ({typeclass})")
+            Typeclass = class_from_module(typeclass)
+            nodeobj, err = Typeclass.create(self.prototype.get("key", "An empty room"), xyz=xyz)
             if err:
                 raise RuntimeError(err)
         else:
@@ -400,7 +402,14 @@ class MapNode:
                     continue
 
                 exitnode = self.links[direction]
-                exi, err = ExitTypeclass.create(
+                prot = maplinks[key.lower()][3].prototype
+                typeclass = prot.get("typeclass")
+                if typeclass is None:
+                    raise MapError(f"The prototype {self.prototype} for this node has no 'typeclass' key.", self)
+                self.log(f"  spawning/updating exit xyz={xyz}, direction={key} ({typeclass})")
+
+                Typeclass = class_from_module(typeclass)
+                exi, err = Typeclass.create(
                     key,
                     xyz=xyz,
                     xyz_destination=exitnode.get_spawn_xyz(),
@@ -408,15 +417,8 @@ class MapNode:
                 )
                 if err:
                     raise RuntimeError(err)
+
                 linkobjs[key.lower()] = exi
-                prot = maplinks[key.lower()][3].prototype
-                tclass = prot["typeclass"]
-                tclass = (
-                    f" ({tclass})"
-                    if tclass != "evennia.contrib.grid.xyzgrid.xyzroom.XYZExit"
-                    else ""
-                )
-                self.log(f"  spawning/updating exit xyz={xyz}, direction={key}{tclass}")
 
         # apply prototypes to catch any changes
         for key, linkobj in linkobjs.items():
['evennia/contrib/grid/xyzgrid/tests.py', 'evennia/contrib/grid/xyzgrid/xymap_legend.py', 'evennia/contrib/grid/xyzgrid/README.md']