Repository: threedworld-mit/tdw (language: python)

Issue:
Control Humanoid model
Hi there,

I would like to control humanoid model (to move there joints, maybe like Magnebot). Except for animated movement, I couldn't find a way to do so.

Thanks,
Replicants output data
- Added `Replicants` output data (transform data for each body part)
- Added collision detection to each Replicant body part

To test:

1. In `tdw`, `git fetch` and `git checkout replicant_output_data`
2. In `TDWBase`, `git fetch` and `git checkout replicant_output_data`

Below is an example controller with a minimal `Replicant` add-on plus static and dynamic data. You can combine these classes into your existing classes:

```python
from enum import Enum
from typing import Dict, List, Optional, Union
from pathlib import Path
from tdw.controller import Controller
from tdw.tdw_utils import TDWUtils
from tdw.add_ons.add_on import AddOn
from tdw.add_ons.object_manager import ObjectManager
from tdw.object_data.transform import Transform
from tdw.output_data import OutputData, Replicants, Collision, EnvironmentCollision

"""
For the sake of making it easy to send you this example, all of these classes are in the same file.
In the final version, they should each be in a separate file.
"""

class ReplicantBodyPart(Enum):
    """
    The name of each replicant body part.
    """

    hand_l = 0
    hand_r = 1
    head = 2
    lowerarm_l = 3
    lowerarm_r = 4
    lowerleg_l = 5
    lowerleg_r = 6
    spine_01 = 7
    spine_02 = 8
    spine_03 = 9
    upperarm_l = 10
    upperarm_r = 11
    upperleg_l = 12
    upperleg_r = 13

# The body part enum values in a list. Put this in the same file as ReplicantBodyPart
BODY_PARTS: List[ReplicantBodyPart] = [r for r in ReplicantBodyPart]


class ReplicantStatic:
    """
    Static replicant data. This can be combined with your existing ReplicantStatic class.
    """

    def __init__(self, replicant_id: int, resp: List[bytes]):
        """
        :param replicant_id: The ID of this Replicant.
        :param resp: The response from the build.
        """

        self.replicant_id: int = replicant_id
        # Body parts by name. Key = The name. Value = Object ID.
        self.body_parts: Dict[ReplicantBodyPart, int] = dict()
        got_data = False
        for i in range(len(resp) - 1):
            r_id = OutputData.get_data_type_id(resp[i])
            # Get Replicants data.
            if r_id == "repl":
                replicants = Replicants(resp[i])
                for j in range(replicants.get_num()):
                    object_id = replicants.get_id(j)
                    # We found the ID of this replicant.
                    if object_id == self.replicant_id:
                        # The order of the data is always:
                        # [replicant_0, replicant_0_hand_l, replicant_0_hand_r, ... ,replicant_1, replicant_1_hand_l, ... ]
                        # So, having found the ID of this replicant, we know that the next IDs are those of its body parts.
                        for k in range(len(BODY_PARTS)):
                            # Cache the ID.
                            self.body_parts[BODY_PARTS[k]] = replicants.get_id(j + k + 1)
                        # Stop reading output data. We have what we need.
                        got_data = True
                        break
            if got_data:
                break


class ReplicantDynamic:
    """
    Static replicant data. This can be combined with your existing ReplicantDynamic class.
    """

    def __init__(self, replicant_id: int, resp: List[bytes]):
        """
        :param replicant_id: The ID of this Replicant.
        :param resp: The response from the build.
        """

        # Transform data for each body part.
        self.body_part_transforms: Dict[int, Transform] = dict()
        # A dictionary of collisions. Key = Body part ID. Value = Collision data.
        self.collisions: Dict[int, List[Union[Collision, EnvironmentCollision]]] = dict()
        got_data = False
        for i in range(len(resp) - 1):
            r_id = OutputData.get_data_type_id(resp[i])
            # Get replicants data.
            if r_id == "repl":
                replicants = Replicants(resp[i])
                for j in range(replicants.get_num()):
                    object_id = replicants.get_id(j)
                    # We found the ID of this replicant.
                    if object_id == replicant_id:
                        # The order of the data is always:
                        # [replicant_0, replicant_0_hand_l, replicant_0_hand_r, ... ,replicant_1, replicant_1_hand_l, ... ]
                        # So, having found the ID of this replicant, we know that the next IDs are those of its body parts.
                        for k in range(len(BODY_PARTS)):
                            body_part_index = j + k + 1
                            body_part_transform = Transform(position=replicants.get_position(body_part_index),
                                                            forward=replicants.get_forward(body_part_index),
                                                            rotation=replicants.get_rotation(body_part_index))
                            # Cache the transform.
                            self.body_part_transforms[replicants.get_id(body_part_index)] = body_part_transform
                        # Stop reading output data. We have what we need.
                        got_data = True
                        break
            if got_data:
                break
        # Now that we have the body part IDs, iterate through the output data a second time to get collisions.
        for i in range(len(resp) - 1):
            r_id = OutputData.get_data_type_id(resp[i])
            # This is a collision.
            if r_id == "coll":
                collision = Collision(resp[i])
                collider_id = collision.get_collider_id()
                collidee_id = collision.get_collidee_id()
                # This collision included body parts.
                if collider_id in self.body_part_transforms:
                    body_part_id = collider_id
                elif collidee_id in self.body_part_transforms:
                    body_part_id = collidee_id
                else:
                    body_part_id = None
                if body_part_id is not None:
                    if body_part_id not in self.collisions:
                        self.collisions[body_part_id] = list()
                    # Record the collision.
                    self.collisions[body_part_id].append(collision)
            # This is an environment collision.
            elif r_id == "enco":
                collision = EnvironmentCollision(resp[i])
                collider_id = collision.get_object_id()
                if collider_id not in self.collisions:
                    self.collisions[collider_id] = list()
                # Record the collision.
                self.collisions[collider_id].append(collision)



class Replicant(AddOn):
    def __init__(self, replicant_id: int):
        super().__init__()
        self.replicant_id: int = replicant_id
        self.static: Optional[ReplicantStatic] = None
        self.dynamic: Optional[ReplicantDynamic] = None

    def get_initialization_commands(self) -> List[dict]:
        path = "file:///" + str(Path.home().joinpath("Downloads/replicant").resolve()).replace("\\", "/")
        print("Path to replicant file: " + path)
        return [{"$type": "add_replicant",
                 "name": "replicant",
                 "url": path,
                 "position": TDWUtils.VECTOR3_ZERO,
                 "rotation": TDWUtils.VECTOR3_ZERO,
                 "id": self.replicant_id},
                {"$type": "send_replicants",
                 "frequency": "always"},
                {"$type": "send_collisions",
                 "enter": True,
                 "stay": True,
                 "exit": True,
                 "collision_types": ["obj", "env"]}]

    def on_send(self, resp: List[bytes]) -> None:
        # Cache static data.
        if self.static is None:
            self.static = ReplicantStatic(replicant_id=self.replicant_id, resp=resp)
        # Update dynamic data.
        self.dynamic = ReplicantDynamic(replicant_id=self.replicant_id, resp=resp)



c = Controller(launch_build=False)
replicant = Replicant(replicant_id=0)
object_manager = ObjectManager()
c.add_ons.extend([object_manager, replicant])
box_id = Controller.get_unique_id()
c.communicate([TDWUtils.create_empty_room(12, 12),
               Controller.get_add_object(model_name="iron_box",
                                         position={"x": 2, "y": 1, "z": 0},
                                         object_id=box_id),
               {"$type": "apply_force_to_object",
                "id": box_id,
                "force": {"x": -10, "y": 0, "z": 0}}])
# Print the static replicant data.
for body_part in replicant.static.body_parts:
    print(body_part, replicant.static.body_parts[body_part])
for i in range(20):
    c.communicate([])
    for body_part_id in replicant.dynamic.collisions:
        for collision in replicant.dynamic.collisions[body_part_id]:
            if isinstance(collision, Collision):
                collider_id = collision.get_collider_id()
                collidee_id = collision.get_collidee_id()
                state = collision.get_state()
                print("collision:", collider_id, collidee_id, state)
            elif isinstance(collision, EnvironmentCollision):
                collider_id = collision.get_object_id()
                state = collision.get_state()
                print("environment_collision:", collider_id, state)
c.communicate({"$type": "terminate"})
```


Discussion / hints:

Fix patch:
diff --git a/Documentation/Changelog.md b/Documentation/Changelog.md
index 73e01ead1..a67f97983 100644
--- a/Documentation/Changelog.md
+++ b/Documentation/Changelog.md
@@ -1,5 +1,162 @@
 # CHANGELOG
 
+# v1.11.x
+
+To upgrade from TDW v1.9 to v1.10, read [this guide](upgrade_guides/v1.10_to_v1.11.md).
+
+### Command API
+
+#### New Commands
+
+| Command                                    | Description                                                  |
+| ------------------------------------------ | ------------------------------------------------------------ |
+| `add_replicant`                            | Add a Replicant to the scene.                                |
+| `teleport_empty_object`                    | Teleport an empty object to a new position.                  |
+| `add_replicant_rigidbody`                  | Add a Rigidbody to a Replicant.                              |
+| `parent_avatar_to_replicant`               | Parent an avatar to a Replicant. The avatar's position and rotation will always be relative to the Replicant's head. Usually you'll want to do this to add a camera to the replicant. |
+| `replicant_resolve_collider_intersections` | Try to resolve intersections between the Replicant's colliders and any other colliders. If there are other objects intersecting with the Replicant, the objects will be moved away along a given directional vector. |
+| `replicant_drop_object`                    | Drop a held object.                                          |
+| `replicant_grasp_object`                   | Grasp a target object.                                       |
+| `replicant_set_grasped_object_rotation`    | Start to rotate a grasped object relative to the rotation of the hand. This will update per communicate() call until the object is dropped. |
+| `replicant_reset_arm`                      | Tell the Replicant to start to reset the arm on a humanoid to its neutral position. |
+| `replicant_reach_for_object`               | Tell the Replicant to start to reach for a target object. The Replicant will try to reach for the nearest empty object attached to the target. If there aren't any empty objects, the Replicant will reach for the nearest bounds position. |
+| `replicant_reach_for_position`             | Tell the Replicant to start to reach for a target position.  |
+| `replicant_look_at_object`                 | Tell the Replicant to start to look at an object.            |
+| `replicant_look_at_position`               | Tell the Replicant to start to look at a position.           |
+| `replicant_reset_head`                     | Tell the Replicant to start to reset its head to its neutral position. |
+| `send_dynamic_empty_objects`               | Send the positions of each empty object in the scene.        |
+| `send_replicants`                          | Send data of each Replicant in the scene.                    |
+| `send_static_empty_objects`                | Send the IDs of each empty object and the IDs of their parent objects. |
+| `teleport_object_by`                       | Translate an object by an amount, optionally in local or world space. |
+
+### Modified Commands
+
+| Command                                                      | Modification                                                 |
+| ------------------------------------------------------------ | ------------------------------------------------------------ |
+| `add_object`                                                 | Added optional parameter `affordance_points`.  A list of affordance points. Can be empty. |
+| `add_model_report`                                           | Added optional parameter `affordance_points`.  A list of affordance points. Can be empty. |
+| `teleport_object`                                            | Add optional parameter `absolute`. If True, set the position in world coordindate space. If False, set the position in local coordinate space. |
+| `add_box_container`<br>`add_cylinder_container`<br>`add_sphere_container` | Added parameter `tag`. The semantic container tag.           |
+| `play_humanoid_animation`                                    | Added optional parameter `forward`. If True, play the animation normally. If False, play the naimation in reverse. |
+| `send_containment`                                           | Sends `Containment` data instead of `Overlap` data.          |
+
+### Removed Commands
+
+| Command              | Reason                                                       |
+| -------------------- | ------------------------------------------------------------ |
+| `send_robots`        | Obsolete; the `Robot` add-on sends `send_dynamic_robots` instead. |
+| `send_empty_objects` | Replaced with `send_static_empty_objects` and `send_dynamic_empty_objects` |
+
+### Output Data
+
+#### New Output Data
+
+| Output Data           | Description                                                  |
+| --------------------- | ------------------------------------------------------------ |
+| `Containment`         | The IDs of every object that a shape overlaps plus parent IDs and the semantic containment tag. |
+| `DynamicEmptyObjects` | The position of each empty object in the scene.              |
+| `Replicants`          | Data about each Replicant in the scene.                      |
+| `StaticEmptyObjects`  | Static data for empty objects in the scene.                  |
+
+#### Removed Output Data
+
+| Output Data    | Reason                                                       |
+| -------------- | ------------------------------------------------------------ |
+| `EmptyObjects` | Replaced with `DynamicEmptyObjects` and `StaticEmptyObjects` |
+| `Robot`        | Replaced with `DynamicRobots`                                |
+
+### `tdw` module
+
+- **Added: `Replicant` add-on.** A Replicant is a human-like agent that can interact with the scene with pseudo-physics behavior.
+  - Added Replicant action classes:
+    - `Action`
+    - `Animate`
+    - `ArmMotion`
+    - `Drop`
+    - `Grasp`
+    - `HeadMotion`
+    - `LookAt`
+    - `MoveBy`
+    - `MoveTo`
+    - `ReachFor`
+    - `ResetArm`
+    - `ResetHead`
+    - `TurnBy`
+    - `TurnTo`
+
+  - Added helper data classes and enum classes:
+    - `ActionStatus`
+    - `Arm`
+    - `CollisionDetection`
+    - `ImageFrequency`
+    - `ReplicantBodyPart`
+    - `ReplicantDynamic`
+    - `ReplicantStatic`
+
+- Removed from `ContainerManager`:  `add_box()`, `add_cylinder()`, `add_sphere()`
+- `Controller.get_add_object()` automatically adds affordance points from the model record.
+- `Controller.get_add_physics_object()` automatically adds affordance points from the model record. It also adds container shapes.
+
+### Model Library
+
+- Added affordance points for: basket_18inx18inx12iin_bamboo, basket_18inx18inx12iin_plastic_lattice, basket_18inx18inx12iin_wicker, basket_18inx18inx12iin_wood_mesh, coffeemug, rh10, alivar_tech_bench_sofa, arflex_strips_sofa
+
+### Replicants Library
+
+- Added: replicants.json library
+
+### Example Controllers
+
+- Moved composite object example controllers to `composite_objects/`
+- Added Replicant example controllers:
+    - replicant/animate.py
+    - replicant/animate_collision_detection.py
+    - replicants/carry_couch.py
+    - replicant/clap.py
+    - replicant/collision_detection_tests.py
+    - replicant/crash.py
+    - replicant/egocentric_images.py
+    - replicant/grasp_basket_with_object.py
+    - replicant/grasp_basket_with_object_both_hands.py
+    - replicant/look_at.py
+    - replicant/minimal_custom_action.py
+    - replicant/move_by.py
+    - replicant/move_grasp_drop.py
+    - replicant/move_to.py
+    - replicant/multi_navigate.py
+    - replicant/multi_replicant.py
+    - replicant/multi_replicant_state_machine.py
+    - replicant/navigate.py
+    - replicant/non_kinematic.py
+    - replicant/obstacle_avoidance.py
+    - replicant/pathfind.py
+    - replicant/physics_test.py
+    - replicant/reach_for_follow.py
+    - replicant/reach_for_move.py
+    - replicant/reach_for_object.py
+    - replicant/reach_for_position.py
+    - replicant/reach_too_far.py
+    - replicant/reset.py
+    - replicant/reset_arm.py
+    - replicant/smpl_animate.py
+
+### Documentation
+
+#### New Documentation
+
+| Document                                                     | Description                                          |
+| ------------------------------------------------------------ | ---------------------------------------------------- |
+| `upgrade_guides/v1.10_to_v1.11.md`                           | Upgrade guide.                                       |
+| `lessons/replicants/overview.md`<br/>`lessons/replicants/actions.md`<br/>`lessons/replicants/output_data.md`<br/>`lessons/replicants/collision_detection.md`<br/>`lessons/replicants/movement.md`<br/>`lessons/replicants/animations.md`<br/>`lessons/replicants/arm_articulation.md`<br/>`lessons/replicants/grasp_drop.md`<br/>`lessons/replicants/head_rotation.md`<br/>`lessons/replicants/navigation.md`<br/>`lessons/replicants/custom_actions.md`<br/>`lessons/replicants/multiple_replicants.md`<br/>`lessons/replicants/reset.md` | Tutorial documentation for how to use the Replicant. |
+| `python/add_ons/replicant.md`<br/>`python/replicant/action_status.md`<br/>`python/replicant/arm.md`<br/>`python/replicant/collision_detection.md`<br/>`python/replicant/image_frequency.md`<br/>`python/replicant/replicant_body_part.md`<br/>`python/replicant/replicant_dynamic.md`<br/>`python/replicant/replicant_static.md`<br/>`python/replicant/actions/action.md`<br/>`python/replicant/actions/animate.md`<br/>`python/replicant/actions/arm_motion.md`<br/>`python/replicant/actions/drop.md`<br/>`python/replicant/actions/grasp.md`<br/>`python/replicant/actions/head_motion.md`<br/>`python/replicant/actions/look_at.md`<br/>`python/replicant/actions/move_by.md`<br/>`python/replicant/actions/move_to.md`<br/>`python/replicant/actions/reach_for.md`<br/>`python/replicant/actions/reset_arm.md`<br/>`python/replicant/actions/reset_head.md`<br/>`python/replicant/actions/turn_by.md`<br/>`python/replicant/actions/turn_to.md` | API documentation for how to use the Replicant.      |
+
+#### Modified Documentation
+
+| Document                                 | Modification                                  |
+| ---------------------------------------- | --------------------------------------------- |
+| `lessons/semantic_states/containment.md` | Updated explanation of how containment works. |
+| `lessons/agents/overview.md`             | Added a section for Replicants.               |
+
 # v1.10.x
 
 To upgrade from TDW v1.9 to v1.10, read [this guide](upgrade_guides/v1.9_to_v1.10.md).
diff --git a/Documentation/api/command_api.md b/Documentation/api/command_api.md
index fb6181cf7..0e7097750 100644
--- a/Documentation/api/command_api.md
+++ b/Documentation/api/command_api.md
@@ -22,7 +22,6 @@
 | [`pause_editor`](#pause_editor) | Pause Unity Editor.  |
 | [`perlin_noise_terrain`](#perlin_noise_terrain) | Initialize a scene environment with procedurally generated "terrain" using Perlin noise. This command will return Meshes output data which will contain the mesh data of the terrain.  |
 | [`rotate_hdri_skybox_by`](#rotate_hdri_skybox_by) | Rotate the HDRI skybox by a given value and the sun light by the same value in the opposite direction, to maintain alignment. |
-| [`send_nav_mesh_path`](#send_nav_mesh_path) | Tell the build to send data of a path on the NavMesh from the origin to the destination.  |
 | [`set_ambient_intensity`](#set_ambient_intensity) | Set how much the ambient light fom the source affects the scene. Low values will darken the scene overall, to simulate evening /night light levels. |
 | [`set_cursor`](#set_cursor) | Set cursor parameters. |
 | [`set_download_timeout`](#set_download_timeout) | Set the timeout after which an Asset Bundle Command (e.g. add_object) will retry a download. The default timeout is 30 minutes, which should always be sufficient. Send this command only if your computer or Internet connection is very slow. |
@@ -63,6 +62,7 @@
 | Command | Description |
 | --- | --- |
 | [`add_humanoid`](#add_humanoid) | Add a humanoid model to the scene.  |
+| [`add_replicant`](#add_replicant) | Add a Replicant to the scene.  |
 | [`add_smpl_humanoid`](#add_smpl_humanoid) | Add a parameterized humanoid to the scene using <ulink url="https://smpl.is.tue.mpg.de/en">SMPL</ulink>. Each parameter scales an aspect of the humanoid and must be between -1 and 1. For example, if the height is -1, then the humanoid will be the shortest possible height. Because all of these parameters blend together to create the overall shape, it isn't possible to document specific body shape values, such as overall height, that might correspond to this command's parameters.  |
 
 **Add Material Command**
@@ -276,7 +276,6 @@
 | Command | Description |
 | --- | --- |
 | [`add_trigger_collider`](#add_trigger_collider) | Add a trigger collider to an object. Trigger colliders are non-physics colliders that will merely detect if they intersect with something. You can use this to detect whether one object is inside another. The side, position, and rotation of the trigger collider always matches that of the parent object. Per trigger event, the trigger collider will send output data depending on which of the enter, stay, and exit booleans are True.  |
-| [`attach_empty_object`](#attach_empty_object) | Attach an empty object to an object in the scene. This is useful for tracking local space positions as the object rotates. See: send_empty_objects |
 | [`create_obi_colliders`](#create_obi_colliders) | Create Obi colliders for an object if there aren't any.  |
 | [`destroy_object`](#destroy_object) | Destroy an object.  |
 | [`make_nav_mesh_obstacle`](#make_nav_mesh_obstacle) | Make a specific object a NavMesh obstacle. If it is already a NavMesh obstacle, change its properties. An object is already a NavMesh obstacle if you've sent the bake_nav_mesh or make_nav_mesh_obstacle command.  |
@@ -295,6 +294,7 @@
 | [`set_physic_material`](#set_physic_material) | Set the physic material of an object and apply friction and bounciness values to the object. These settings can be overriden by sending the command again, or by assigning a semantic material via set_semantic_material_to. |
 | [`set_vr_graspable`](#set_vr_graspable) | Make an object graspable for a VR rig, with Oculus touch controllers. Uses the AutoHand plugin for grasping and physics interaction behavior.  |
 | [`teleport_object`](#teleport_object) | Teleport an object to a new position. |
+| [`teleport_object_by`](#teleport_object_by) | Translate an object by an amount, optionally in local or world space. |
 | [`unparent_object`](#unparent_object) | Unparent an object from an object. If the textured quad doesn't have a parent, this command doesn't do anything. |
 
 **Add Container Shape Command**
@@ -305,6 +305,13 @@
 | [`add_cylinder_container`](#add_cylinder_container) | Add a cylindrical container shape to an object. The object will send output data whenever other objects overlap with this volume.  |
 | [`add_sphere_container`](#add_sphere_container) | Add a spherical container shape to an object. The object will send output data whenever other objects overlap with this volume.  |
 
+**Empty Object Command**
+
+| Command | Description |
+| --- | --- |
+| [`attach_empty_object`](#attach_empty_object) | Attach an empty object to an object in the scene. This is useful for tracking local space positions as the object rotates. See: send_empty_objects |
+| [`teleport_empty_object`](#teleport_empty_object) | Teleport an empty object to a new position. |
+
 **Flex Object Command**
 
 | Command | Description |
@@ -349,9 +356,46 @@
 | Command | Description |
 | --- | --- |
 | [`destroy_humanoid`](#destroy_humanoid) | Destroy a humanoid.  |
-| [`play_humanoid_animation`](#play_humanoid_animation) | Play a motion capture animation on a humanoid. The animation must already be in memory via the add_humanoid_animation command. |
+| [`play_humanoid_animation`](#play_humanoid_animation) | Play a motion capture animation on a humanoid. The animation must already be in memory via the add_humanoid_animation command.  |
 | [`stop_humanoid_animation`](#stop_humanoid_animation) | Stop a motion capture animation on a humanoid. |
 
+**Replicant Command**
+
+| Command | Description |
+| --- | --- |
+| [`add_replicant_rigidbody`](#add_replicant_rigidbody) | Add a Rigidbody to a Replicant. |
+| [`parent_avatar_to_replicant`](#parent_avatar_to_replicant) | Parent an avatar to a Replicant. The avatar's position and rotation will always be relative to the Replicant's head. Usually you'll want to do this to add a camera to the Replicant. |
+| [`replicant_resolve_collider_intersections`](#replicant_resolve_collider_intersections) | Try to resolve intersections between the Replicant's colliders and any other colliders. If there are other objects intersecting with the Replicant, the objects will be moved away along a given directional vector. |
+
+**Replicant Arm Command**
+
+| Command | Description |
+| --- | --- |
+| [`replicant_drop_object`](#replicant_drop_object) | Drop a held object. |
+| [`replicant_grasp_object`](#replicant_grasp_object) | Grasp a target object. |
+| [`replicant_set_grasped_object_rotation`](#replicant_set_grasped_object_rotation) | Start to rotate a grasped object relative to the rotation of the hand. This will update per communicate() call until the object is dropped. |
+
+**Replicant Arm Motion Command**
+
+| Command | Description |
+| --- | --- |
+| [`replicant_reset_arm`](#replicant_reset_arm) | Tell the Replicant to start to reset the arm on a humanoid to its neutral position.  |
+
+**Replicant Reach For Command**
+
+| Command | Description |
+| --- | --- |
+| [`replicant_reach_for_object`](#replicant_reach_for_object) | Tell the Replicant to start to reach for a target object. The Replicant will try to reach for the nearest empty object attached to the target. If there aren't any empty objects, the Replicant will reach for the nearest bounds position.  |
+| [`replicant_reach_for_position`](#replicant_reach_for_position) | Instruct a Replicant to start to reach for a target position.  |
+
+**Replicant Look At Command**
+
+| Command | Description |
+| --- | --- |
+| [`replicant_look_at_object`](#replicant_look_at_object) | Tell the Replicant to start to look at an object.  |
+| [`replicant_look_at_position`](#replicant_look_at_position) | Tell the Replicant to start to look at a position.  |
+| [`replicant_reset_head`](#replicant_reset_head) | Tell the Replicant to start to reset its head to its neutral position.  |
+
 **Sub Object Command**
 
 | Command | Description |
@@ -518,6 +562,10 @@
 
 **Send Multiple Data Once Command**
 
+| Command | Description |
+| --- | --- |
+| [`send_nav_mesh_path`](#send_nav_mesh_path) | Tell the build to send data of a path on the NavMesh from the origin to the destination.  |
+
 **Send Overlap Command**
 
 | Command | Description |
@@ -549,7 +597,6 @@
 | [`send_collider_intersections`](#send_collider_intersections) | Send data for collider intersections between pairs of objects and between single objects and the environment (e.g. walls). Note that each intersection is a separate output data object, and that each pair of objects/environment meshes might intersect more than once because they might have more than one collider.  |
 | [`send_containment`](#send_containment) | Send containment data using container shapes. See: <computeroutput>add_box_container</computeroutput>, <computeroutput>add_cylinder_container</computeroutput>, and <computeroutput>add_sphere_container</computeroutput>. Container shapes will check for overlaps with other objects.  |
 | [`send_magnebots`](#send_magnebots) | Send data for each Magnebot in the scene.  |
-| [`send_robots`](#send_robots) | Send dynamic data of each robot and each robot's body parts in the scene. See also: send_static_robots  |
 | [`send_robot_joint_velocities`](#send_robot_joint_velocities) | Send velocity data for each joint of each robot in the scene. This is separate from DynamicRobots output data for the sake of speed in certain simulations.  |
 | [`send_static_oculus_touch`](#send_static_oculus_touch) | Send static data for the Oculus Touch rig.  |
 | [`send_static_robots`](#send_static_robots) | Send static data that doesn't update per frame (such as segmentation colors) for each robot in the scene. See also: send_robots  |
@@ -577,8 +624,8 @@
 | [`send_audio_sources`](#send_audio_sources) | Send data regarding whether each object in the scene is currently playing a sound.  |
 | [`send_categories`](#send_categories) | Send data for the category names and colors of each object in the scene.  |
 | [`send_dynamic_composite_objects`](#send_dynamic_composite_objects) | Send dynamic data for every composite object in the scene.  |
+| [`send_dynamic_empty_objects`](#send_dynamic_empty_objects) | Send the positions of each empty object in the scene.  |
 | [`send_dynamic_robots`](#send_dynamic_robots) | Send dynamic robot data for each robot in the scene.  |
-| [`send_empty_objects`](#send_empty_objects) | Send data each empty object in the scene. See: attach_empty_object  |
 | [`send_humanoids`](#send_humanoids) | Send transform (position, rotation, etc.) data for humanoids in the scene.  |
 | [`send_junk`](#send_junk) | Send junk data.  |
 | [`send_keyboard`](#send_keyboard) | Request keyboard input data.  |
@@ -586,8 +633,10 @@
 | [`send_mouse`](#send_mouse) | Send mouse output data.  |
 | [`send_obi_particles`](#send_obi_particles) | Send particle data for all Obi actors in the scene.  |
 | [`send_oculus_touch_buttons`](#send_oculus_touch_buttons) | Send data for buttons pressed on Oculus Touch controllers.  |
+| [`send_replicants`](#send_replicants) | Send data of each Replicant in the scene.  |
 | [`send_scene_regions`](#send_scene_regions) | Receive data about the sub-regions within a scene in the scene. Only send this command after initializing the scene.  |
 | [`send_static_composite_objects`](#send_static_composite_objects) | Send static data for every composite object in the scene.  |
+| [`send_static_empty_objects`](#send_static_empty_objects) | Send the IDs of each empty object and the IDs of their parent objects.  |
 | [`send_version`](#send_version) | Receive data about the build version.  |
 | [`send_vr_rig`](#send_vr_rig) | Send data for a VR Rig currently in the scene.  |
 
@@ -967,31 +1016,6 @@ Rotate the HDRI skybox by a given value and the sun light by the same value in t
 
 ***
 
-## **`send_nav_mesh_path`**
-
-Tell the build to send data of a path on the NavMesh from the origin to the destination. 
-
-- <font style="color:blue">**Requires a NavMesh**: This command requires a NavMesh.Scenes created via [add_scene](#add_scene) already have NavMeshes.Proc-gen scenes don't; send [bake_nav_mesh](#bake_nav_mesh) to create one.</font>
-- <font style="color:green">**Sends data**: This command instructs the build to send output data.</font>
-
-    - <font style="color:green">**Type:** [`NavMeshPath`](output_data.md#NavMeshPath)</font>
-
-```python
-{"$type": "send_nav_mesh_path", "origin": {"x": 1.1, "y": 0.0, "z": 0}, "destination": {"x": 1.1, "y": 0.0, "z": 0}}
-```
-
-```python
-{"$type": "send_nav_mesh_path", "origin": {"x": 1.1, "y": 0.0, "z": 0}, "destination": {"x": 1.1, "y": 0.0, "z": 0}, "id": 0}
-```
-
-| Parameter | Type | Description | Default |
-| --- | --- | --- | --- |
-| `"origin"` | Vector3 | The origin of the path. | |
-| `"destination"` | Vector3 | The destination of the path. | |
-| `"id"` | int | The ID of the path. The output data will contain a matching ID. | 0 |
-
-***
-
 ## **`set_ambient_intensity`**
 
 Set how much the ambient light fom the source affects the scene. Low values will darken the scene overall, to simulate evening /night light levels.
@@ -1497,6 +1521,30 @@ Add a humanoid model to the scene.
 
 ***
 
+## **`add_replicant`**
+
+Add a Replicant to the scene. 
+
+- <font style="color:orange">**Downloads an asset bundle**: This command will download an asset bundle from TDW's asset bundle library. The first time this command is sent during a simulation, it will be slow (because it needs to download the file). Afterwards, the file data will be cached until the simulation is terminated, and this command will be much faster. See: `python/librarian/replicant_librarian.md`</font>
+
+```python
+{"$type": "add_replicant", "id": 1, "name": "string", "url": "string"}
+```
+
+```python
+{"$type": "add_replicant", "id": 1, "name": "string", "url": "string", "position": {"x": 0, "y": 0, "z": 0}, "rotation": {"x": 0, "y": 0, "z": 0}}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"id"` | int | The unique ID of the humanoid. | |
+| `"position"` | Vector3 | Position of the humanoid. | {"x": 0, "y": 0, "z": 0} |
+| `"rotation"` | Vector3 | Rotation of the humanoid, in Euler angles. | {"x": 0, "y": 0, "z": 0} |
+| `"name"` | string | The name of the asset bundle. | |
+| `"url"` | string | The location of the asset bundle. If the asset bundle is remote, this must be a valid URL. If the asset is a local file, this must begin with the prefix "file:///" | |
+
+***
+
 ## **`add_smpl_humanoid`**
 
 Add a parameterized humanoid to the scene using <ulink url="https://smpl.is.tue.mpg.de/en">SMPL</ulink>. Each parameter scales an aspect of the humanoid and must be between -1 and 1. For example, if the height is -1, then the humanoid will be the shortest possible height. Because all of these parameters blend together to create the overall shape, it isn't possible to document specific body shape values, such as overall height, that might correspond to this command's parameters. 
@@ -1615,7 +1663,7 @@ Add a single object from a model library or from a local asset bundle to the sce
 ```
 
 ```python
-{"$type": "add_object", "id": 1, "name": "string", "url": "string", "position": {"x": 0, "y": 0, "z": 0}, "rotation": {"x": 0, "y": 0, "z": 0}, "scale_factor": 1, "category": ""}
+{"$type": "add_object", "id": 1, "name": "string", "url": "string", "position": {"x": 0, "y": 0, "z": 0}, "rotation": {"x": 0, "y": 0, "z": 0}, "scale_factor": 1, "category": "", "affordance_points": []}
 ```
 
 | Parameter | Type | Description | Default |
@@ -1625,6 +1673,7 @@ Add a single object from a model library or from a local asset bundle to the sce
 | `"id"` | int | The unique ID of the object. | |
 | `"scale_factor"` | float | The default scale factor of a model. | 1 |
 | `"category"` | string | The model category. | "" |
+| `"affordance_points"` | Vector3 [] | A list of affordance points. Can be empty. | [] |
 | `"name"` | string | The name of the asset bundle. | |
 | `"url"` | string | The location of the asset bundle. If the asset bundle is remote, this must be a valid URL. If the asset is a local file, this must begin with the prefix "file:///" | |
 
@@ -1647,7 +1696,7 @@ Tell the build to send a report of a model asset bundle. Each report will be a s
 ```
 
 ```python
-{"$type": "send_model_report", "flex": True, "id": 1, "name": "string", "url": "string", "scale_factor": 1, "category": ""}
+{"$type": "send_model_report", "flex": True, "id": 1, "name": "string", "url": "string", "scale_factor": 1, "category": "", "affordance_points": []}
 ```
 
 | Parameter | Type | Description | Default |
@@ -1656,6 +1705,7 @@ Tell the build to send a report of a model asset bundle. Each report will be a s
 | `"id"` | int | The unique ID of the object. | |
 | `"scale_factor"` | float | The default scale factor of a model. | 1 |
 | `"category"` | string | The model category. | "" |
+| `"affordance_points"` | Vector3 [] | A list of affordance points. Can be empty. | [] |
 | `"name"` | string | The name of the asset bundle. | |
 | `"url"` | string | The location of the asset bundle. If the asset bundle is remote, this must be a valid URL. If the asset is a local file, this must begin with the prefix "file:///" | |
 
@@ -3724,23 +3774,6 @@ The shape of the trigger collider.
 
 ***
 
-## **`attach_empty_object`**
-
-Attach an empty object to an object in the scene. This is useful for tracking local space positions as the object rotates. See: send_empty_objects
-
-
-```python
-{"$type": "attach_empty_object", "empty_object_id": 1, "position": {"x": 1.1, "y": 0.0, "z": 0}, "id": 1}
-```
-
-| Parameter | Type | Description | Default |
-| --- | --- | --- | --- |
-| `"empty_object_id"` | int | The ID of the empty object. This doesn't have to be the same as the object ID. | |
-| `"position"` | Vector3 | The position of the empty object relative to the parent object, in the parent object's local coordinate space. | |
-| `"id"` | int | The unique object ID. | |
-
-***
-
 ## **`create_obi_colliders`**
 
 Create Obi colliders for an object if there aren't any. 
@@ -4133,13 +4166,35 @@ Teleport an object to a new position.
 ```
 
 ```python
-{"$type": "teleport_object", "position": {"x": 1.1, "y": 0.0, "z": 0}, "id": 1, "physics": False}
+{"$type": "teleport_object", "position": {"x": 1.1, "y": 0.0, "z": 0}, "id": 1, "physics": False, "absolute": True}
 ```
 
 | Parameter | Type | Description | Default |
 | --- | --- | --- | --- |
 | `"position"` | Vector3 | New position of the object. | |
 | `"physics"` | bool | This should almost always be False (the default). If True, apply a "physics-based" teleportation to the object. This only works if the object has a rigidbody (i.e. is a model from a model library) and is slightly slower than a non-physics teleport. Set this to True only if you are having persistent and rare physics glitches. | False |
+| `"absolute"` | bool | If True, set the position in world coordindate space. If False, set the position in local coordinate space. | True |
+| `"id"` | int | The unique object ID. | |
+
+***
+
+## **`teleport_object_by`**
+
+Translate an object by an amount, optionally in local or world space.
+
+
+```python
+{"$type": "teleport_object_by", "position": {"x": 1.1, "y": 0.0, "z": 0}, "id": 1}
+```
+
+```python
+{"$type": "teleport_object_by", "position": {"x": 1.1, "y": 0.0, "z": 0}, "id": 1, "absolute": True}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"position"` | Vector3 | The positional offset. | |
+| `"absolute"` | bool | If True, set the position in world coordindate space. If False, set the position in local coordinate space. | True |
 | `"id"` | int | The unique object ID. | |
 
 ***
@@ -4172,11 +4227,11 @@ Add a box container shape to an object. The object will send output data wheneve
     - <font style="color:green">**Type:** [`Overlap`](output_data.md#Overlap)</font>
 
 ```python
-{"$type": "add_box_container", "id": 1}
+{"$type": "add_box_container", "tag": "on", "id": 1}
 ```
 
 ```python
-{"$type": "add_box_container", "id": 1, "half_extents": {"x": 1, "y": 1, "z": 1}, "rotation": {"x": 0, "y": 0, "z": 0}, "container_id": 0, "position": {"x": 0, "y": 0, "z": 0}}
+{"$type": "add_box_container", "tag": "on", "id": 1, "half_extents": {"x": 1, "y": 1, "z": 1}, "rotation": {"x": 0, "y": 0, "z": 0}, "container_id": 0, "position": {"x": 0, "y": 0, "z": 0}}
 ```
 
 | Parameter | Type | Description | Default |
@@ -4185,8 +4240,19 @@ Add a box container shape to an object. The object will send output data wheneve
 | `"rotation"` | Vector3 | The rotation of the box in Euler angles relative to the parent object. | {"x": 0, "y": 0, "z": 0} |
 | `"container_id"` | int | The ID of this container shape. This can be used to differentiate between multiple container shapes belonging to the same object. | 0 |
 | `"position"` | Vector3 | The position of the container shape relative to the parent object. | {"x": 0, "y": 0, "z": 0} |
+| `"tag"` | ContainerTag | The container tag. | |
 | `"id"` | int | The unique object ID. | |
 
+#### ContainerTag
+
+A tag for a container shape.
+
+| Value | Description |
+| --- | --- |
+| `"on"` | An object on top of a surface, for example a plate on a table. |
+| `"inside"` | An object inside a cavity or basin, for example a toy in a basket or a plate in a sink. |
+| `"enclosed"` | An object inside an enclosed cavity, for example a pan in an oven. |
+
 ***
 
 ## **`add_cylinder_container`**
@@ -4198,11 +4264,11 @@ Add a cylindrical container shape to an object. The object will send output data
     - <font style="color:green">**Type:** [`Overlap`](output_data.md#Overlap)</font>
 
 ```python
-{"$type": "add_cylinder_container", "id": 1}
+{"$type": "add_cylinder_container", "tag": "on", "id": 1}
 ```
 
 ```python
-{"$type": "add_cylinder_container", "id": 1, "radius": 0.5, "height": 1, "rotation": {"x": 0, "y": 0, "z": 0}, "container_id": 0, "position": {"x": 0, "y": 0, "z": 0}}
+{"$type": "add_cylinder_container", "tag": "on", "id": 1, "radius": 0.5, "height": 1, "rotation": {"x": 0, "y": 0, "z": 0}, "container_id": 0, "position": {"x": 0, "y": 0, "z": 0}}
 ```
 
 | Parameter | Type | Description | Default |
@@ -4212,8 +4278,19 @@ Add a cylindrical container shape to an object. The object will send output data
 | `"rotation"` | Vector3 | The rotation of the cylinder in Euler angles relative to the parent object. | {"x": 0, "y": 0, "z": 0} |
 | `"container_id"` | int | The ID of this container shape. This can be used to differentiate between multiple container shapes belonging to the same object. | 0 |
 | `"position"` | Vector3 | The position of the container shape relative to the parent object. | {"x": 0, "y": 0, "z": 0} |
+| `"tag"` | ContainerTag | The container tag. | |
 | `"id"` | int | The unique object ID. | |
 
+#### ContainerTag
+
+A tag for a container shape.
+
+| Value | Description |
+| --- | --- |
+| `"on"` | An object on top of a surface, for example a plate on a table. |
+| `"inside"` | An object inside a cavity or basin, for example a toy in a basket or a plate in a sink. |
+| `"enclosed"` | An object inside an enclosed cavity, for example a pan in an oven. |
+
 ***
 
 ## **`add_sphere_container`**
@@ -4225,11 +4302,11 @@ Add a spherical container shape to an object. The object will send output data w
     - <font style="color:green">**Type:** [`Overlap`](output_data.md#Overlap)</font>
 
 ```python
-{"$type": "add_sphere_container", "id": 1}
+{"$type": "add_sphere_container", "tag": "on", "id": 1}
 ```
 
 ```python
-{"$type": "add_sphere_container", "id": 1, "radius": 0.5, "container_id": 0, "position": {"x": 0, "y": 0, "z": 0}}
+{"$type": "add_sphere_container", "tag": "on", "id": 1, "radius": 0.5, "container_id": 0, "position": {"x": 0, "y": 0, "z": 0}}
 ```
 
 | Parameter | Type | Description | Default |
@@ -4237,6 +4314,61 @@ Add a spherical container shape to an object. The object will send output data w
 | `"radius"` | float | The radius of the sphere. | 0.5 |
 | `"container_id"` | int | The ID of this container shape. This can be used to differentiate between multiple container shapes belonging to the same object. | 0 |
 | `"position"` | Vector3 | The position of the container shape relative to the parent object. | {"x": 0, "y": 0, "z": 0} |
+| `"tag"` | ContainerTag | The container tag. | |
+| `"id"` | int | The unique object ID. | |
+
+#### ContainerTag
+
+A tag for a container shape.
+
+| Value | Description |
+| --- | --- |
+| `"on"` | An object on top of a surface, for example a plate on a table. |
+| `"inside"` | An object inside a cavity or basin, for example a toy in a basket or a plate in a sink. |
+| `"enclosed"` | An object inside an enclosed cavity, for example a pan in an oven. |
+
+# EmptyObjectCommand
+
+These commands add or adjust an empty object attached to an object.
+
+***
+
+## **`attach_empty_object`**
+
+Attach an empty object to an object in the scene. This is useful for tracking local space positions as the object rotates. See: send_empty_objects
+
+
+```python
+{"$type": "attach_empty_object", "position": {"x": 1.1, "y": 0.0, "z": 0}, "empty_object_id": 1, "id": 1}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"position"` | Vector3 | The position of the empty object relative to the parent object, in the parent object's local coordinate space. | |
+| `"empty_object_id"` | int | The ID of the empty object. This doesn't have to be the same as the object ID. | |
+| `"id"` | int | The unique object ID. | |
+
+***
+
+## **`teleport_empty_object`**
+
+Teleport an empty object to a new position.
+
+
+```python
+{"$type": "teleport_empty_object", "position": {"x": 1.1, "y": 0.0, "z": 0}, "empty_object_id": 1, "id": 1}
+```
+
+```python
+{"$type": "teleport_empty_object", "position": {"x": 1.1, "y": 0.0, "z": 0}, "empty_object_id": 1, "id": 1, "rotation": {"w": 1, "x": 0, "y": 0, "z": 0}, "absolute": True}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"position"` | Vector3 | The location to teleport to. | |
+| `"rotation"` | Quaternion | The new local rotation of the empty object. | {"w": 1, "x": 0, "y": 0, "z": 0} |
+| `"absolute"` | bool | If True, teleport the empty object in world coordinate space. If False, teleport the empty object in local coordinate space. | True |
+| `"empty_object_id"` | int | The ID of the empty object. This doesn't have to be the same as the object ID. | |
 | `"id"` | int | The unique object ID. | |
 
 # FlexObjectCommand
@@ -4903,21 +5035,27 @@ Destroy a humanoid.
 
 ## **`play_humanoid_animation`**
 
-Play a motion capture animation on a humanoid. The animation must already be in memory via the add_humanoid_animation command.
+Play a motion capture animation on a humanoid. The animation must already be in memory via the add_humanoid_animation command. 
+
+- <font style="color:green">**Sends data**: This command instructs the build to send output data.</font>
+
+    - <font style="color:green">**Exactly once**</font>
 
+    - <font style="color:green">**Type:** [`HumanoidMotionComplete`](output_data.md#HumanoidMotionComplete)</font>
 
 ```python
 {"$type": "play_humanoid_animation", "name": "string", "id": 1}
 ```
 
 ```python
-{"$type": "play_humanoid_animation", "name": "string", "id": 1, "framerate": -1}
+{"$type": "play_humanoid_animation", "name": "string", "id": 1, "framerate": -1, "forward": True}
 ```
 
 | Parameter | Type | Description | Default |
 | --- | --- | --- | --- |
 | `"name"` | string | Name of the animation clip to play. | |
 | `"framerate"` | int | If greater than zero, play the animation at this framerate instead of the animation's framerate. | -1 |
+| `"forward"` | bool | If True, play the animation normally. If False, play the naimation in reverse. | True |
 | `"id"` | int | The unique object ID. | |
 
 ***
@@ -4935,141 +5073,478 @@ Stop a motion capture animation on a humanoid.
 | --- | --- | --- | --- |
 | `"id"` | int | The unique object ID. | |
 
-# SubObjectCommand
+# ReplicantCommand
 
-These commands can only be used for sub-objects of a composite object. Additionally, these commands may require the object to be a particular "machine type". To determine which objects are sub-objects of a given parent, send send_composite_objects to receive CompositeObjects output data.
+These commands affect a Replicant currently in the scene.
 
 ***
 
-## **`set_hinge_limits`**
-
-Set the angle limits of a hinge joint. This will work with hinges, motors, and springs. 
+## **`add_replicant_rigidbody`**
 
-- <font style="color:deepskyblue">**Sub-Object**: This command will only work with a sub-object of a Composite Object. The sub-object must be of the correct type. To determine which Composite Objects are currently in the scene, and the types of their sub-objects, send the [send_composite_objects](#send_composite_objects) command.</font>
+Add a Rigidbody to a Replicant.
 
-    - <font style="color:deepskyblue">**Type:** `hinge`</font>
 
 ```python
-{"$type": "set_hinge_limits", "id": 1}
+{"$type": "add_replicant_rigidbody", "id": 1}
 ```
 
 ```python
-{"$type": "set_hinge_limits", "id": 1, "min_limit": 0, "max_limit": 0}
+{"$type": "add_replicant_rigidbody", "id": 1, "is_kinematic": True, "use_gravity": False}
 ```
 
 | Parameter | Type | Description | Default |
 | --- | --- | --- | --- |
-| `"min_limit"` | float | The minimum angle in degrees. | 0 |
-| `"max_limit"` | float | The maximum angle in degrees. | 0 |
+| `"is_kinematic"` | bool | If True, the Rigidbody will be kinematic, and won't respond to physics. | True |
+| `"use_gravity"` | bool | If True, the object will respond to gravity. | False |
 | `"id"` | int | The unique object ID. | |
 
 ***
 
-## **`set_motor_force`**
+## **`parent_avatar_to_replicant`**
 
-Set the force a motor. 
+Parent an avatar to a Replicant. The avatar's position and rotation will always be relative to the Replicant's head. Usually you'll want to do this to add a camera to the Replicant.
 
-- <font style="color:deepskyblue">**Sub-Object**: This command will only work with a sub-object of a Composite Object. The sub-object must be of the correct type. To determine which Composite Objects are currently in the scene, and the types of their sub-objects, send the [send_composite_objects](#send_composite_objects) command.</font>
 
-    - <font style="color:deepskyblue">**Type:** `motor`</font>
+```python
+{"$type": "parent_avatar_to_replicant", "position": {"x": 1.1, "y": 0.0, "z": 0}, "id": 1}
+```
 
 ```python
-{"$type": "set_motor_force", "force": 0.125, "id": 1}
+{"$type": "parent_avatar_to_replicant", "position": {"x": 1.1, "y": 0.0, "z": 0}, "id": 1, "avatar_id": "a"}
 ```
 
 | Parameter | Type | Description | Default |
 | --- | --- | --- | --- |
-| `"force"` | float | The force of the motor. | |
+| `"avatar_id"` | string | The ID of the avatar. It must already exist in the scene. | "a" |
+| `"position"` | Vector3 | The position of the avatar relative to the Replicant's head. | |
 | `"id"` | int | The unique object ID. | |
 
 ***
 
-## **`set_motor_target_velocity`**
-
-Set the target velocity a motor. 
+## **`replicant_resolve_collider_intersections`**
 
-- <font style="color:deepskyblue">**Sub-Object**: This command will only work with a sub-object of a Composite Object. The sub-object must be of the correct type. To determine which Composite Objects are currently in the scene, and the types of their sub-objects, send the [send_composite_objects](#send_composite_objects) command.</font>
+Try to resolve intersections between the Replicant's colliders and any other colliders. If there are other objects intersecting with the Replicant, the objects will be moved away along a given directional vector.
 
-    - <font style="color:deepskyblue">**Type:** `motor`</font>
 
 ```python
-{"$type": "set_motor_target_velocity", "target_velocity": 0.125, "id": 1}
+{"$type": "replicant_resolve_collider_intersections", "direction": {"x": 1.1, "y": 0.0, "z": 0}, "id": 1}
 ```
 
 | Parameter | Type | Description | Default |
 | --- | --- | --- | --- |
-| `"target_velocity"` | float | The target velocity of the motor. | |
+| `"direction"` | Vector3 | The direction along which objects should be moved. | |
 | `"id"` | int | The unique object ID. | |
 
+# ReplicantArmCommand
+
+These commands involve a Replicant's arm.
+
 ***
 
-## **`set_spring_damper`**
+## **`replicant_drop_object`**
 
-Set the damper value of a spring. 
+Drop a held object.
 
-- <font style="color:deepskyblue">**Sub-Object**: This command will only work with a sub-object of a Composite Object. The sub-object must be of the correct type. To determine which Composite Objects are currently in the scene, and the types of their sub-objects, send the [send_composite_objects](#send_composite_objects) command.</font>
 
-    - <font style="color:deepskyblue">**Type:** `spring`</font>
+```python
+{"$type": "replicant_drop_object", "arm": "left", "id": 1}
+```
 
 ```python
-{"$type": "set_spring_damper", "damper": 0.125, "id": 1}
+{"$type": "replicant_drop_object", "arm": "left", "id": 1, "offset_distance": 0.1}
 ```
 
 | Parameter | Type | Description | Default |
 | --- | --- | --- | --- |
-| `"damper"` | float | The damper value of the spring. | |
+| `"offset_distance"` | float | Prior to being dropped, the object will be moved by this distance along its forward directional vector. | 0.1 |
+| `"arm"` | Arm | The arm doing the action. | |
 | `"id"` | int | The unique object ID. | |
 
+#### Arm
+
+A left or right arm.
+
+| Value | Description |
+| --- | --- |
+| `"left"` |  |
+| `"right"` |  |
+
 ***
 
-## **`set_spring_force`**
+## **`replicant_grasp_object`**
 
-Set the force of a spring. 
+Grasp a target object.
 
-- <font style="color:deepskyblue">**Sub-Object**: This command will only work with a sub-object of a Composite Object. The sub-object must be of the correct type. To determine which Composite Objects are currently in the scene, and the types of their sub-objects, send the [send_composite_objects](#send_composite_objects) command.</font>
 
-    - <font style="color:deepskyblue">**Type:** `spring`</font>
+```python
+{"$type": "replicant_grasp_object", "object_id": 1, "arm": "left", "id": 1}
+```
 
 ```python
-{"$type": "set_spring_force", "spring_force": 0.125, "id": 1}
+{"$type": "replicant_grasp_object", "object_id": 1, "arm": "left", "id": 1, "rotate": True}
 ```
 
 | Parameter | Type | Description | Default |
 | --- | --- | --- | --- |
-| `"spring_force"` | float | The force of the spring. | |
+| `"object_id"` | int | The target object ID. | |
+| `"rotate"` | bool | If true, rotate the object to match the rotation of the hand. | True |
+| `"arm"` | Arm | The arm doing the action. | |
 | `"id"` | int | The unique object ID. | |
 
-***
+#### Arm
 
-## **`set_spring_target_position`**
+A left or right arm.
 
-Set the target position of a spring. 
+| Value | Description |
+| --- | --- |
+| `"left"` |  |
+| `"right"` |  |
 
-- <font style="color:deepskyblue">**Sub-Object**: This command will only work with a sub-object of a Composite Object. The sub-object must be of the correct type. To determine which Composite Objects are currently in the scene, and the types of their sub-objects, send the [send_composite_objects](#send_composite_objects) command.</font>
+***
+
+## **`replicant_set_grasped_object_rotation`**
+
+Start to rotate a grasped object relative to the rotation of the hand. This will update per communicate() call until the object is dropped.
 
-    - <font style="color:deepskyblue">**Type:** `spring`</font>
 
 ```python
-{"$type": "set_spring_target_position", "target_position": 0.125, "id": 1}
+{"$type": "replicant_set_grasped_object_rotation", "angle": 0.125, "axis": "pitch", "arm": "left", "id": 1}
 ```
 
 | Parameter | Type | Description | Default |
 | --- | --- | --- | --- |
-| `"target_position"` | float | The target position of the spring, expressed as degrees in a circle. | |
+| `"angle"` | float | Rotate the object by this many degrees relative to the hand's rotation. | |
+| `"axis"` | Axis | Rotate the object around this axis relative to the hand's rotation. | |
+| `"arm"` | Arm | The arm doing the action. | |
 | `"id"` | int | The unique object ID. | |
 
-***
+#### Arm
 
-## **`set_sub_object_light`**
+A left or right arm.
 
-Turn a light on or off. 
+| Value | Description |
+| --- | --- |
+| `"left"` |  |
+| `"right"` |  |
 
-- <font style="color:deepskyblue">**Sub-Object**: This command will only work with a sub-object of a Composite Object. The sub-object must be of the correct type. To determine which Composite Objects are currently in the scene, and the types of their sub-objects, send the [send_composite_objects](#send_composite_objects) command.</font>
+#### Axis
 
-    - <font style="color:deepskyblue">**Type:** `light`</font>
+An axis of rotation.
+
+| Value | Description |
+| --- | --- |
+| `"pitch"` | Nod your head "yes". |
+| `"yaw"` | Shake your head "no". |
+| `"roll"` | Put your ear to your shoulder. |
+
+# ReplicantArmMotionCommand
+
+These commands involve the motion of a Replicant's arm.
+
+***
+
+## **`replicant_reset_arm`**
+
+Tell the Replicant to start to reset the arm on a humanoid to its neutral position. 
+
+- <font style="color:green">**Replicant motion motion**: This tells the Replicant to begin a motion. The Replicant will continue the motion per communicate() call until the motion is complete.</font>
 
 ```python
-{"$type": "set_sub_object_light", "is_on": True, "id": 1}
+{"$type": "replicant_reset_arm", "duration": 0.125, "arm": "left", "id": 1}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"duration"` | float | The duration of the motion in seconds. | |
+| `"arm"` | Arm | The arm doing the action. | |
+| `"id"` | int | The unique object ID. | |
+
+#### Arm
+
+A left or right arm.
+
+| Value | Description |
+| --- | --- |
+| `"left"` |  |
+| `"right"` |  |
+
+# ReplicantReachForCommand
+
+These commands instruct a replicant to start to reach for a target.
+
+***
+
+## **`replicant_reach_for_object`**
+
+Tell the Replicant to start to reach for a target object. The Replicant will try to reach for the nearest empty object attached to the target. If there aren't any empty objects, the Replicant will reach for the nearest bounds position. 
+
+- <font style="color:green">**Replicant motion motion**: This tells the Replicant to begin a motion. The Replicant will continue the motion per communicate() call until the motion is complete.</font>
+
+```python
+{"$type": "replicant_reach_for_object", "object_id": 1, "duration": 0.125, "arm": "left", "id": 1}
+```
+
+```python
+{"$type": "replicant_reach_for_object", "object_id": 1, "duration": 0.125, "arm": "left", "id": 1, "max_distance": 1.5, "arrived_at": 0.02}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"object_id"` | int | The target object ID. | |
+| `"max_distance"` | float | The maximum distance that the Replicant can reach. | 1.5 |
+| `"arrived_at"` | float | If the hand is this distance from the target position or less, the action succeeded. | 0.02 |
+| `"duration"` | float | The duration of the motion in seconds. | |
+| `"arm"` | Arm | The arm doing the action. | |
+| `"id"` | int | The unique object ID. | |
+
+#### Arm
+
+A left or right arm.
+
+| Value | Description |
+| --- | --- |
+| `"left"` |  |
+| `"right"` |  |
+
+***
+
+## **`replicant_reach_for_position`**
+
+Instruct a Replicant to start to reach for a target position. 
+
+- <font style="color:green">**Replicant motion motion**: This tells the Replicant to begin a motion. The Replicant will continue the motion per communicate() call until the motion is complete.</font>
+
+```python
+{"$type": "replicant_reach_for_position", "position": {"x": 1.1, "y": 0.0, "z": 0}, "duration": 0.125, "arm": "left", "id": 1}
+```
+
+```python
+{"$type": "replicant_reach_for_position", "position": {"x": 1.1, "y": 0.0, "z": 0}, "duration": 0.125, "arm": "left", "id": 1, "max_distance": 1.5, "arrived_at": 0.02}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"position"` | Vector3 | The target position. | |
+| `"max_distance"` | float | The maximum distance that the Replicant can reach. | 1.5 |
+| `"arrived_at"` | float | If the hand is this distance from the target position or less, the action succeeded. | 0.02 |
+| `"duration"` | float | The duration of the motion in seconds. | |
+| `"arm"` | Arm | The arm doing the action. | |
+| `"id"` | int | The unique object ID. | |
+
+#### Arm
+
+A left or right arm.
+
+| Value | Description |
+| --- | --- |
+| `"left"` |  |
+| `"right"` |  |
+
+# ReplicantLookAtCommand
+
+These commands tell a Replicant to look at a target position or object.
+
+***
+
+## **`replicant_look_at_object`**
+
+Tell the Replicant to start to look at an object. 
+
+- <font style="color:green">**Replicant motion motion**: This tells the Replicant to begin a motion. The Replicant will continue the motion per communicate() call until the motion is complete.</font>
+
+```python
+{"$type": "replicant_look_at_object", "object_id": 1, "id": 1}
+```
+
+```python
+{"$type": "replicant_look_at_object", "object_id": 1, "id": 1, "use_centroid": True, "duration": 0.1}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"object_id"` | int | The ID of the target object. | |
+| `"use_centroid"` | bool | If True, look at the centroid of the object. If False, look at the position of the object (y=0). | True |
+| `"duration"` | float | The duration of the motion. | 0.1 |
+| `"id"` | int | The unique object ID. | |
+
+***
+
+## **`replicant_look_at_position`**
+
+Tell the Replicant to start to look at a position. 
+
+- <font style="color:green">**Replicant motion motion**: This tells the Replicant to begin a motion. The Replicant will continue the motion per communicate() call until the motion is complete.</font>
+
+```python
+{"$type": "replicant_look_at_position", "position": {"x": 1.1, "y": 0.0, "z": 0}, "id": 1}
+```
+
+```python
+{"$type": "replicant_look_at_position", "position": {"x": 1.1, "y": 0.0, "z": 0}, "id": 1, "duration": 0.1}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"position"` | Vector3 | The position. | |
+| `"duration"` | float | The duration of the motion. | 0.1 |
+| `"id"` | int | The unique object ID. | |
+
+***
+
+## **`replicant_reset_head`**
+
+Tell the Replicant to start to reset its head to its neutral position. 
+
+- <font style="color:green">**Replicant motion motion**: This tells the Replicant to begin a motion. The Replicant will continue the motion per communicate() call until the motion is complete.</font>
+
+```python
+{"$type": "replicant_reset_head", "id": 1}
+```
+
+```python
+{"$type": "replicant_reset_head", "id": 1, "duration": 0.1}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"duration"` | float | The duration of the motion. | 0.1 |
+| `"id"` | int | The unique object ID. | |
+
+# SubObjectCommand
+
+These commands can only be used for sub-objects of a composite object. Additionally, these commands may require the object to be a particular "machine type". To determine which objects are sub-objects of a given parent, send send_composite_objects to receive CompositeObjects output data.
+
+***
+
+## **`set_hinge_limits`**
+
+Set the angle limits of a hinge joint. This will work with hinges, motors, and springs. 
+
+- <font style="color:deepskyblue">**Sub-Object**: This command will only work with a sub-object of a Composite Object. The sub-object must be of the correct type. To determine which Composite Objects are currently in the scene, and the types of their sub-objects, send the [send_composite_objects](#send_composite_objects) command.</font>
+
+    - <font style="color:deepskyblue">**Type:** `hinge`</font>
+
+```python
+{"$type": "set_hinge_limits", "id": 1}
+```
+
+```python
+{"$type": "set_hinge_limits", "id": 1, "min_limit": 0, "max_limit": 0}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"min_limit"` | float | The minimum angle in degrees. | 0 |
+| `"max_limit"` | float | The maximum angle in degrees. | 0 |
+| `"id"` | int | The unique object ID. | |
+
+***
+
+## **`set_motor_force`**
+
+Set the force a motor. 
+
+- <font style="color:deepskyblue">**Sub-Object**: This command will only work with a sub-object of a Composite Object. The sub-object must be of the correct type. To determine which Composite Objects are currently in the scene, and the types of their sub-objects, send the [send_composite_objects](#send_composite_objects) command.</font>
+
+    - <font style="color:deepskyblue">**Type:** `motor`</font>
+
+```python
+{"$type": "set_motor_force", "force": 0.125, "id": 1}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"force"` | float | The force of the motor. | |
+| `"id"` | int | The unique object ID. | |
+
+***
+
+## **`set_motor_target_velocity`**
+
+Set the target velocity a motor. 
+
+- <font style="color:deepskyblue">**Sub-Object**: This command will only work with a sub-object of a Composite Object. The sub-object must be of the correct type. To determine which Composite Objects are currently in the scene, and the types of their sub-objects, send the [send_composite_objects](#send_composite_objects) command.</font>
+
+    - <font style="color:deepskyblue">**Type:** `motor`</font>
+
+```python
+{"$type": "set_motor_target_velocity", "target_velocity": 0.125, "id": 1}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"target_velocity"` | float | The target velocity of the motor. | |
+| `"id"` | int | The unique object ID. | |
+
+***
+
+## **`set_spring_damper`**
+
+Set the damper value of a spring. 
+
+- <font style="color:deepskyblue">**Sub-Object**: This command will only work with a sub-object of a Composite Object. The sub-object must be of the correct type. To determine which Composite Objects are currently in the scene, and the types of their sub-objects, send the [send_composite_objects](#send_composite_objects) command.</font>
+
+    - <font style="color:deepskyblue">**Type:** `spring`</font>
+
+```python
+{"$type": "set_spring_damper", "damper": 0.125, "id": 1}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"damper"` | float | The damper value of the spring. | |
+| `"id"` | int | The unique object ID. | |
+
+***
+
+## **`set_spring_force`**
+
+Set the force of a spring. 
+
+- <font style="color:deepskyblue">**Sub-Object**: This command will only work with a sub-object of a Composite Object. The sub-object must be of the correct type. To determine which Composite Objects are currently in the scene, and the types of their sub-objects, send the [send_composite_objects](#send_composite_objects) command.</font>
+
+    - <font style="color:deepskyblue">**Type:** `spring`</font>
+
+```python
+{"$type": "set_spring_force", "spring_force": 0.125, "id": 1}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"spring_force"` | float | The force of the spring. | |
+| `"id"` | int | The unique object ID. | |
+
+***
+
+## **`set_spring_target_position`**
+
+Set the target position of a spring. 
+
+- <font style="color:deepskyblue">**Sub-Object**: This command will only work with a sub-object of a Composite Object. The sub-object must be of the correct type. To determine which Composite Objects are currently in the scene, and the types of their sub-objects, send the [send_composite_objects](#send_composite_objects) command.</font>
+
+    - <font style="color:deepskyblue">**Type:** `spring`</font>
+
+```python
+{"$type": "set_spring_target_position", "target_position": 0.125, "id": 1}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"target_position"` | float | The target position of the spring, expressed as degrees in a circle. | |
+| `"id"` | int | The unique object ID. | |
+
+***
+
+## **`set_sub_object_light`**
+
+Turn a light on or off. 
+
+- <font style="color:deepskyblue">**Sub-Object**: This command will only work with a sub-object of a Composite Object. The sub-object must be of the correct type. To determine which Composite Objects are currently in the scene, and the types of their sub-objects, send the [send_composite_objects](#send_composite_objects) command.</font>
+
+    - <font style="color:deepskyblue">**Type:** `light`</font>
+
+```python
+{"$type": "set_sub_object_light", "is_on": True, "id": 1}
 ```
 
 | Parameter | Type | Description | Default |
@@ -6572,6 +7047,33 @@ Instantaneously set the angles of a spherical joint. Only use this command to se
 
 These commands send data exactly once to the controller (not per-frame). Unlike most output data such as Tranforms, there can be more than one output data object of this type in the build's response. For example, the build can send multiple Raycast objects in the same list.
 
+***
+
+## **`send_nav_mesh_path`**
+
+Tell the build to send data of a path on the NavMesh from the origin to the destination. 
+
+- <font style="color:blue">**Requires a NavMesh**: This command requires a NavMesh.Scenes created via [add_scene](#add_scene) already have NavMeshes.Proc-gen scenes don't; send [bake_nav_mesh](#bake_nav_mesh) to create one.</font>
+- <font style="color:green">**Sends data**: This command instructs the build to send output data.</font>
+
+    - <font style="color:green">**Exactly once**</font>
+
+    - <font style="color:green">**Type:** [`NavMeshPath`](output_data.md#NavMeshPath)</font>
+
+```python
+{"$type": "send_nav_mesh_path", "origin": {"x": 1.1, "y": 0.0, "z": 0}, "destination": {"x": 1.1, "y": 0.0, "z": 0}}
+```
+
+```python
+{"$type": "send_nav_mesh_path", "origin": {"x": 1.1, "y": 0.0, "z": 0}, "destination": {"x": 1.1, "y": 0.0, "z": 0}, "id": 0}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"origin"` | Vector3 | The origin of the path. | |
+| `"destination"` | Vector3 | The destination of the path. | |
+| `"id"` | int | The ID of the output data object. This can be used to match the output data back to the command that created it. | 0 |
+
 # SendOverlapCommand
 
 These commands create an overlap shape and then check which objects are within that shape.
@@ -6857,7 +7359,7 @@ Send containment data using container shapes. See: <computeroutput>add_box_conta
 
 - <font style="color:green">**Sends data**: This command instructs the build to send output data.</font>
 
-    - <font style="color:green">**Type:** [`Overlap`](output_data.md#Overlap)</font>
+    - <font style="color:green">**Type:** [`Containment`](output_data.md#Containment)</font>
 
 ```python
 {"$type": "send_containment"}
@@ -6916,39 +7418,6 @@ Options for when to send data.
 
 ***
 
-## **`send_robots`**
-
-Send dynamic data of each robot and each robot's body parts in the scene. See also: send_static_robots 
-
-- <font style="color:green">**Sends data**: This command instructs the build to send output data.</font>
-
-    - <font style="color:green">**Type:** [`Robot`](output_data.md#Robot)</font>
-- <font style="color:orange">**Deprecated**: This command has been deprecated. In the next major TDW update (1.x.0), this command will be removed.</font>
-
-```python
-{"$type": "send_robots"}
-```
-
-```python
-{"$type": "send_robots", "frequency": "once"}
-```
-
-| Parameter | Type | Description | Default |
-| --- | --- | --- | --- |
-| `"frequency"` | Frequency | The frequency at which data is sent. | "once" |
-
-#### Frequency
-
-Options for when to send data.
-
-| Value | Description |
-| --- | --- |
-| `"once"` | Send the data for this frame only. |
-| `"always"` | Send the data every frame. |
-| `"never"` | Never send the data. |
-
-***
-
 ## **`send_robot_joint_velocities`**
 
 Send velocity data for each joint of each robot in the scene. This is separate from DynamicRobots output data for the sake of speed in certain simulations. 
@@ -7521,20 +7990,20 @@ Options for when to send data.
 
 ***
 
-## **`send_dynamic_robots`**
+## **`send_dynamic_empty_objects`**
 
-Send dynamic robot data for each robot in the scene. 
+Send the positions of each empty object in the scene. 
 
 - <font style="color:green">**Sends data**: This command instructs the build to send output data.</font>
 
-    - <font style="color:green">**Type:** [`DynamicRobots`](output_data.md#DynamicRobots)</font>
+    - <font style="color:green">**Type:** [`DynamicEmptyObjects`](output_data.md#DynamicEmptyObjects)</font>
 
 ```python
-{"$type": "send_dynamic_robots"}
+{"$type": "send_dynamic_empty_objects"}
 ```
 
 ```python
-{"$type": "send_dynamic_robots", "frequency": "once"}
+{"$type": "send_dynamic_empty_objects", "frequency": "once"}
 ```
 
 | Parameter | Type | Description | Default |
@@ -7553,20 +8022,20 @@ Options for when to send data.
 
 ***
 
-## **`send_empty_objects`**
+## **`send_dynamic_robots`**
 
-Send data each empty object in the scene. See: attach_empty_object 
+Send dynamic robot data for each robot in the scene. 
 
 - <font style="color:green">**Sends data**: This command instructs the build to send output data.</font>
 
-    - <font style="color:green">**Type:** [`EmptyObjects`](output_data.md#EmptyObjects)</font>
+    - <font style="color:green">**Type:** [`DynamicRobots`](output_data.md#DynamicRobots)</font>
 
 ```python
-{"$type": "send_empty_objects"}
+{"$type": "send_dynamic_robots"}
 ```
 
 ```python
-{"$type": "send_empty_objects", "frequency": "once"}
+{"$type": "send_dynamic_robots", "frequency": "once"}
 ```
 
 | Parameter | Type | Description | Default |
@@ -7814,6 +8283,38 @@ Options for when to send data.
 
 ***
 
+## **`send_replicants`**
+
+Send data of each Replicant in the scene. 
+
+- <font style="color:green">**Sends data**: This command instructs the build to send output data.</font>
+
+    - <font style="color:green">**Type:** [`Replicants`](output_data.md#Replicants)</font>
+
+```python
+{"$type": "send_replicants"}
+```
+
+```python
+{"$type": "send_replicants", "frequency": "once"}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"frequency"` | Frequency | The frequency at which data is sent. | "once" |
+
+#### Frequency
+
+Options for when to send data.
+
+| Value | Description |
+| --- | --- |
+| `"once"` | Send the data for this frame only. |
+| `"always"` | Send the data every frame. |
+| `"never"` | Never send the data. |
+
+***
+
 ## **`send_scene_regions`**
 
 Receive data about the sub-regions within a scene in the scene. Only send this command after initializing the scene. 
@@ -7878,6 +8379,38 @@ Options for when to send data.
 
 ***
 
+## **`send_static_empty_objects`**
+
+Send the IDs of each empty object and the IDs of their parent objects. 
+
+- <font style="color:green">**Sends data**: This command instructs the build to send output data.</font>
+
+    - <font style="color:green">**Type:** [`StaticEmptyObjects`](output_data.md#StaticEmptyObjects)</font>
+
+```python
+{"$type": "send_static_empty_objects"}
+```
+
+```python
+{"$type": "send_static_empty_objects", "frequency": "once"}
+```
+
+| Parameter | Type | Description | Default |
+| --- | --- | --- | --- |
+| `"frequency"` | Frequency | The frequency at which data is sent. | "once" |
+
+#### Frequency
+
+Options for when to send data.
+
+| Value | Description |
+| --- | --- |
+| `"once"` | Send the data for this frame only. |
+| `"always"` | Send the data every frame. |
+| `"never"` | Never send the data. |
+
+***
+
 ## **`send_version`**
 
 Receive data about the build version. 
diff --git a/Documentation/api/output_data.md b/Documentation/api/output_data.md
index dfe2546c9..4516105bc 100644
--- a/Documentation/api/output_data.md
+++ b/Documentation/api/output_data.md
@@ -59,9 +59,10 @@ Objects in arrays can't be directly accessed (this is due to how the backend cod
 | [CameraMatrices](#CameraMatrices) | An avatar's camera matrix data. Each matrix is arranged as m00, m01... m10, m11, etc. | `cama` |
 | [Categories](#Categories) | Color segmentation data for object categories. | `cate` |
 | [Collision](#Collision) | Data for a collision between objects occurring on this frame. | `coll` |
+| [Containment](#Containment) | The IDs of every object that a shape overlaps plus parent IDs and the semantic containment tag. | `cont` |
 | [DynamicCompositeObjects](#DynamicCompositeObjects) | Dynamic data for composite objects. | `dcom` |
+| [DynamicEmptyObjects](#DynamicEmptyObjects) | The position of each empty object in the scene. | `dyem` |
 | [DynamicRobots](#DynamicRobots) | Dynamic robot output data. | `drob` |
-| [EmptyObjects](#EmptyObjects) | The position of each empty object in the scene. | `empt` |
 | [EnvironmentColliderIntersection](#EnvironmentColliderIntersection) | Data for an whose colliders are intersecting with an environment collider such as a wall. | `enci` |
 | [EnvironmentCollision](#EnvironmentCollision) | Data for a collision between and object and the scene environment on this frame. | `enco` |
 | [FieldOfView](#FieldOfView) | An avatar's camera field of view and focal length. | `fofv` |
@@ -87,13 +88,14 @@ Objects in arrays can't be directly accessed (this is due to how the backend cod
 | [Overlap](#Overlap) | The IDs of every object that a shape overlaps. | `over` |
 | [QuitSignal](#QuitSignal) | A message sent by the build when it quits. | `quit` |
 | [Raycast](#Raycast) | A ray cast from an origin to a destination and what, if anything, it hit. | `rayc` |
+| [Replicants](#Replicants) | Data about each Replicant in the scene. | `repl` |
 | [Rigidbodies](#Rigidbodies) | Dynamic rigibody data (velocity, angular velocity, etc.) for objects in the scene. | `rigi` |
-| [Robot](#Robot) | Data for a robot in the scene. See also: `StaticRobot` | `robo` |
 | [RobotJointVelocities](#RobotJointVelocities) | Velocity for a robot in the scene. | `rojv` |
 | [SceneRegions](#SceneRegions) | Data regarding the scene regions. | `sreg` |
 | [ScreenPosition](#ScreenPosition) | A position on the screen converted from a worldspace position. | `scre` |
 | [SegmentationColors](#SegmentationColors) | Color segmentation data for objects in the scene. | `segm` |
 | [StaticCompositeObjects](#StaticCompositeObjects) | Static data for composite objects. | `scom` |
+| [StaticEmptyObjects](#StaticEmptyObjects) | Static data for empty objects in the scene. | `stem` |
 | [StaticOculusTouch](#StaticOculusTouch) | Static data for the Oculus Touch rig. | `soct` |
 | [StaticRigidbodies](#StaticRigidbodies) | Static rigibody data (mass, kinematic state, etc.) for objects in the scene. | `srig` |
 | [StaticRobot](#StaticRobot) | Static data for a robot in the scene. | `srob` |
@@ -267,6 +269,23 @@ Data for a collision between objects occurring on this frame.
 | `get_contact_normal(index)` | The normal of the contact. | `Tuple[float, float, float]` |
 | `get_contact_point(index)` | The point of the contact. | `Tuple[float, float, float]` |
 
+## Containment
+
+`c = Containment(byte_array)`
+
+**Identifier:** `cont`
+
+The IDs of every object that a shape overlaps plus parent IDs and the semantic containment tag.
+
+| Function | Description | Return type |
+| --- | --- | --- |
+| `get_object_id()` | The ID of the object. | `int` |
+| `get_container_id()` | The ID of the container. | `int` |
+| `get_tag()` | The semantic tag. | `ContainerTag` |
+| `get_overlap_ids()` | The IDs of every object in the overlap shape. | `np.array` |
+| `get_env()` | If true, the overlap shape includes at least one environment object (such as the floor). | `bool` |
+| `get_walls()` | If true, the overlap shape includes at least one environment object that isn't the floor. | `bool` |
+
 ## DynamicCompositeObjects
 
 `d = DynamicCompositeObjects(byte_array)`
@@ -287,6 +306,19 @@ Dynamic data for composite objects.
 | `get_light_id(index)` | The ID of the light. | `int` |
 | `get_light_is_on(index)` | The on of the light is. | `bool` |
 
+## DynamicEmptyObjects
+
+`d = DynamicEmptyObjects(byte_array)`
+
+**Identifier:** `dyem`
+
+The position of each empty object in the scene.
+
+| Function | Description | Return type |
+| --- | --- | --- |
+| `get_num()` | The number of positions. | `int` |
+| `get_position(index)` | The position. | `np.ndarray` |
+
 ## DynamicRobots
 
 `d = DynamicRobots(byte_array)`
@@ -305,20 +337,6 @@ Dynamic robot output data.
 | `get_joint_angles(index)` | The angles of the joint. | `np.array` |
 | `get_joint_sleeping(index)` | The sleeping of the joint. | `bool` |
 
-## EmptyObjects
-
-`e = EmptyObjects(byte_array)`
-
-**Identifier:** `empt`
-
-The position of each empty object in the scene.
-
-| Function | Description | Return type |
-| --- | --- | --- |
-| `get_num()` | The number of objects. | `int` |
-| `get_id(index)` | The id. | `int` |
-| `get_position(index)` | The position. | `np.array` |
-
 ## EnvironmentColliderIntersection
 
 `e = EnvironmentColliderIntersection(byte_array)`
@@ -712,41 +730,48 @@ A ray cast from an origin to a destination and what, if anything, it hit.
 | `get_normal()` | The normal of the surface that the raycast hit. | `Tuple[float, float, float]` |
 | `get_point()` | The point that the raycast hit. | `Tuple[float, float, float]` |
 
-## Rigidbodies
+## Replicants
 
-`r = Rigidbodies(byte_array)`
+`r = Replicants(byte_array)`
 
-**Identifier:** `rigi`
+**Identifier:** `repl`
 
-Dynamic rigibody data (velocity, angular velocity, etc.) for objects in the scene.
+Data about each Replicant in the scene.
 
 | Function | Description | Return type |
 | --- | --- | --- |
 | `get_num()` | The number of objects. | `int` |
 | `get_id(index)` | The id. | `int` |
-| `get_velocity(index)` | The velocity. | `np.array` |
-| `get_angular_velocity(index)` | The angular velocity. | `np.array` |
-| `get_sleeping(index)` | The sleeping. | `bool` |
+| `get_position(index)` | The position. | `np.ndarray` |
+| `get_forward(index)` | The forward. | `np.ndarray` |
+| `get_rotation(index)` | The rotation. | `np.ndarray` |
+| `get_body_part_id(index, body_part_index)` | The ID of the body part. | `int` |
+| `get_body_part_position(index, body_part_index)` | The position of the body part. | `np.ndarray` |
+| `get_body_part_rotation(index, body_part_index)` | The rotation of the body part. | `np.ndarray` |
+| `get_body_part_forward(index, body_part_index)` | The forward of the body part. | `np.ndarray` |
+| `get_is_holding_left(index)` | The left of the is holding. | `bool` |
+| `get_held_left(index)` | The left of the held. | `int` |
+| `get_is_holding_right(index)` | The right of the is holding. | `bool` |
+| `get_held_right(index)` | The right of the held. | `int` |
+| `get_is_collision(index, body_part_index, collision_index)` | The collision of the is. | `bool` |
+| `get_collision_id(index, body_part_index, collision_index)` | The ID of the collision. | `int` |
+| `get_status(index)` | The status. | `ActionStatus` |
 
-## Robot
+## Rigidbodies
 
-`r = Robot(byte_array)`
+`r = Rigidbodies(byte_array)`
 
-**Identifier:** `robo`
+**Identifier:** `rigi`
 
-Data for a robot in the scene. See also: `StaticRobot`
+Dynamic rigibody data (velocity, angular velocity, etc.) for objects in the scene.
 
 | Function | Description | Return type |
 | --- | --- | --- |
-| `get_id()` | The ID of the robot. | `int` |
-| `get_position()` | The (x, y, z) position of this body part. | `Tuple[float, float, float]` |
-| `get_rotation()` | The rotation. | `Tuple[float, float, float, float]` |
-| `get_forward()` | The forward. | `Tuple[float, float, float]` |
-| `get_num_joints()` | The number of joints. | `int` |
-| `get_joint_id(index)` | The ID of the joint. | `int` |
-| `get_joint_position(index)` | The position of the joint. | `np.array` |
-| `get_joint_positions(index)` | The positions of the joint. | `np.array` |
-| `get_immovable()` | True if the root object of the robot is currently immovable. | `bool` |
+| `get_num()` | The number of objects. | `int` |
+| `get_id(index)` | The id. | `int` |
+| `get_velocity(index)` | The velocity. | `np.array` |
+| `get_angular_velocity(index)` | The angular velocity. | `np.array` |
+| `get_sleeping(index)` | The sleeping. | `bool` |
 
 ## RobotJointVelocities
 
@@ -854,6 +879,20 @@ Static data for composite objects.
 | `get_prismatic_joint_limit(index, prismatic_joint_index)` | The limit of the prismatic joint. | `float` |
 | `get_prismatic_joint_axis(index, prismatic_joint_index)` | The axis of the prismatic joint. | `Tuple[float, float, float]` |
 
+## StaticEmptyObjects
+
+`s = StaticEmptyObjects(byte_array)`
+
+**Identifier:** `stem`
+
+Static data for empty objects in the scene.
+
+| Function | Description | Return type |
+| --- | --- | --- |
+| `get_num()` | The number of ids. | `int` |
+| `get_object_id(index)` | The ID of the object. | `int` |
+| `get_empty_object_id(index)` | The ID of the empty object. | `int` |
+
 ## StaticOculusTouch
 
 `s = StaticOculusTouch(byte_array)`
diff --git a/Documentation/lessons/agents/overview.md b/Documentation/lessons/agents/overview.md
index 6bb2617b4..f59268ff1 100644
--- a/Documentation/lessons/agents/overview.md
+++ b/Documentation/lessons/agents/overview.md
@@ -9,6 +9,7 @@ Unlike most simulation platforms, TDW has a very loose definition for "agent". S
 - [Avatars](../core_concepts/avatars.md) have image sensors but are often not embodied.
 - [Objects](../core_concepts/objects.md) don't have image sensors but [it is possible to directly apply forces to objects](../physx/forces.md) and thereby make them act as if they are embodied agents.
 - [Robots](../robots/overview.md) can act as agents but don't have image sensors by default.
+- [Replicants](../replicants/overview.md) are humanoid agents that don't have image sensors by default.
 
 TDW includes higher-level add-ons to effectively "create" agents from lower-level functionality. The [`Robot` add-on](../../python/add_ons/robot.md), for example, is a robotics wrapper class that has been designed assuming that the user wants to use robots as agents.
 
@@ -42,6 +43,16 @@ c.move_by(2)
 
 ![](images/reach_high.gif)
 
+### [Replicant](../replicants/overview.md)
+
+[Replicants](../replicants/overview.md) are humanoid agents that are partially physics-driven. Replicants can cause physics events (for example, they can push other objects). Replicant hands can be procedurally animated using inverse kinematics (IK) and pre-recorded animation.
+
+The Replicant API is a high-level action space API very similar to the Magnebot API.
+
+![](../replicants/images/crash.gif)
+
+**Trade-offs:** Replicants can *cause* physics events (e.g. pushing objects) but won't *respond* to physics events (e.g. getting pushed by objects). The Replicant is incomplete, and will be improved and extended in the near future.
+
 ### [Virtual reality (VR)](../vr/overview.md)
 
 A human agent can directly control an embodied virtual reality agent.
diff --git a/Documentation/lessons/composite_objects/create_from_prefab.md b/Documentation/lessons/composite_objects/create_from_prefab.md
index eff86a5a1..dab2f3fcc 100644
--- a/Documentation/lessons/composite_objects/create_from_prefab.md
+++ b/Documentation/lessons/composite_objects/create_from_prefab.md
@@ -10,7 +10,7 @@ When you are editing the prefab, it should be located within the [Asset Bundle C
 
 ## Requirements
 
-See requirements for using the [`ModelCreator`](https://github.com/threedworld-mit/tdw/blob/master/Documentation/lessons/3d_models/custom_models.md). 
+See requirements for using the [`ModelCreator`](../custom_models/custom_models.md). 
 
 ## Step 1: Create the prefab
 
diff --git a/Documentation/lessons/replicants/actions.md b/Documentation/lessons/replicants/actions.md
new file mode 100644
index 000000000..96fd9b129
--- /dev/null
+++ b/Documentation/lessons/replicants/actions.md
@@ -0,0 +1,229 @@
+##### Replicants
+
+# Actions
+
+The `Replicant` has a pre-defined action space, divided into separate function calls:
+
+- `turn_by(angle)`
+- `turn_to(target)`
+- `move_by(distance)`
+- `move_to(target)`
+- `reach_for(target, arm)`
+- `reset_arm(arm)`
+- `grasp(target, arm)`
+- `drop(arm)`
+- `animate(animation)`
+- `look_at(target)`
+- `reset_head()`
+
+Calling one of these action functions will *start* the action but won't actually *do* the action. This means that Replicants can move concurrently in a [multi-agent simulation](../multi_agent/overview.md).
+
+This example doesn't actually do anything:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+
+c = Controller()
+replicant = Replicant()
+c.add_ons.append(replicant)
+c.communicate(TDWUtils.create_empty_room(12, 12))
+
+# This doesn't do anything!
+replicant.move_by(2)
+```
+
+All actions require multiple `communicate()` calls to complete.
+
+In this example, we'll tell the Replicant to walk 2 meters forward. We'll also add a [camera](../core_concepts/add_ons.md) to the scene and enable [image capture](../core_concepts/images.md). Note that this example is *not* an optimal way to use a Replicant and that subsequent examples will fix some naïve assumptions we're making. 
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+c = Controller()
+replicant = Replicant(position={"x": 0, "y": 0, "z": 2})
+camera = ThirdPersonCamera(position={"x": 2, "y": 1.6, "z": 1},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("move_by")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=["a"],
+                       path=path)
+# Note the order in which the add-ons are added. The replicant needs to be first so that the camera can look at it.
+c.add_ons.extend([replicant, camera, capture])
+# Create the scene.
+c.communicate(TDWUtils.create_empty_room(12, 12))
+# Start walking.
+replicant.move_by(2)
+# Loop for a while.
+for i in range(100):
+    c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/move_by.gif)
+
+## Framerate
+
+This first "problem" with the above example is that the framerate is fast (it's been slowed down in the .gif). This is desirable in the context of machine learning, where the simulation *should* be faster than realtime, but for the sake of a video, we might want to slow time by sending [`set_target_framerate`](../../api/command_api.md#set_target_framerate):
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+c = Controller()
+replicant = Replicant(position={"x": 0, "y": 0, "z": 2})
+camera = ThirdPersonCamera(position={"x": 2, "y": 1.6, "z": 1},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("move_by")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=["a"],
+                       path=path)
+# Note the order in which the add-ons are added. The replicant needs to be first so that the camera can look at it.
+c.add_ons.extend([replicant, camera, capture])
+# Create the scene. Set the framerate.
+c.communicate([TDWUtils.create_empty_room(12, 12),
+               {"$type": "set_target_framerate",
+                "framerate": 30}])
+# Start walking.
+replicant.move_by(2)
+# Loop for a while.
+for i in range(100):
+    c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+The rest of the tutorial will assume that you want to use the Replicant in machine-learning context, as opposed to a video-generation context, and therefore the example code will omit the `set_target_framerate` command.
+
+## Action Status
+
+A more significant problem with the initial action is the loop we're using; we're not checking for when the action actually ends, or if it succeeds.
+
+In the case of `move_by(distance)`, the action can end in success (the Replicant walked the target distance), or failure ([a collision or near-collision](movement.md)).
+
+Within the add-on, every action is a data object: `replicant.action`. This is a subclass of [`Action`](../../python/replicant/actions/action.md).
+
+When we call `replicant.move_by(distance)`, we actually set the `replicant.action` field, like this:
+
+```
+        self.action = MoveBy(distance=distance,
+                             dynamic=self.dynamic,
+                             collision_detection=self.collision_detection,
+                             previous=self._previous_action,
+                             reset_arms=reset_arms,
+                             reset_arms_duration=reset_arms_duration,
+                             arrived_at=arrived_at,
+                             max_walk_cycles=max_walk_cycles)
+```
+
+The [`Action`](../../python/replicant/actions/action.md) (in this case, a [`MoveBy`](../../python/replicant/actions/move_by.md) action), has a `status` field. While the action is ongoing, `action.status == ActionStatus.ongoing`. If it is ever something else, we know that the action is done.
+
+This example replaces the `for i in range(100):` loop with a `while` loop that continuously checks the action's status for completion. Note that there is an additional, `c.communicate([])` call at the end; this is to send commands that end the action.
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+from tdw.replicant.action_status import ActionStatus
+
+c = Controller()
+replicant = Replicant(position={"x": 0, "y": 0, "z": 2})
+camera = ThirdPersonCamera(position={"x": 2, "y": 1.6, "z": 1},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("move_by")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=["a"],
+                       path=path)
+# Note the order in which the add-ons are added. The replicant needs to be first so that the camera can look at it.
+c.add_ons.extend([replicant, camera, capture])
+# Create the scene. Set the framerate.
+c.communicate(TDWUtils.create_empty_room(12, 12))
+# Start walking.
+replicant.move_by(2)
+# Continue walking until the action ends.
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+# End the action.
+c.communicate([])
+print(replicant.action.status)
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+```
+ActionStatus.success
+```
+
+It is *not* necessary to wrap all Replicant actions in a loop like this. For example, in the context of a multi-agent simulation you won't want to loop like this because you'll want to be checking multiple agent actions at the same time. For the sake of subsequent example code, we know we have only one agent, so this simple loop is good enough for showcasing the rest of the Replicant's behavior.
+
+## Low-level description
+
+Actions are very similar to add-ons in that they have a list of commands that can be returned. Actions are handled within `replicant.on_send(resp)`. Thus, the full sequence of how commands are injected into the controller is:
+
+- The `Action` returns a list of commands to the `Replicant`.
+- The `Replicant` returns that list of commands to the `Controller`.
+- The `Controller` appends the `Replicant`'s commands to the rest of the commands it's about to send.
+- The `Controller` sends all of its commands to the build.
+
+Thus, in the remainder of this description, `communicate()` (from the Controller) should be understood as equivalent to `replicant.on_send(resp)`.
+
+Each [`Action`](../../python/replicant/actions/action.md) has an `initialized` boolean. In most cases, this is initially set to False but sometimes will be True, usually because the action fails before it's allowed to begin.
+
+If `action.initialized = False`, the action is initialized on the first `replicant.on_send(resp)` call. The action sets `initialized = True` and then returns `get_initialization_commands(resp, static, dynamic, image_frequency)`. - 
+
+- Depending on the [`image_frequency`](../../python/replicant/image_frequency.md) value,`get_initialization_commands(resp, static, dynamic, image_frequency)` may return [`enable_image_sensor`](../../api/command_api.md#enable_image_sensor),  [`send_images`](../../api/command_api.md#send_images), and/or  [`send_camera_matrices`](../../api/command_api.md#send_camera_matrices). 
+- Most actions' `get_initialization_commands(resp, static, dynamic, image_frequency)` will include additional commands, e.g. commands to download and play an animation.
+
+Actions have a `status` parameter. An action is ongoing if `status == ActionStatus.ongoing`. An action has  ended if `status` is anything else.
+
+It is possible for an `Action` to end immediately after initialization, in which case its end commands are appended to the list of initialization commands (see below).
+
+If the `Action` is ongoing after initialization, then on every *subsequent* `replicant.on_send(resp)` call, it will return `get_ongoing_commands(resp, static, dynamic)`. The commands returned by this function vary depending on the action. In most cases, the action will read `resp` and `dynamic` to decide which commands to send, whether the action is done, etc.
+
+When the action ends, it returns `get_end_commands(resp, static, dynamic, image_frequency)`. 
+
+- If `image_frequency == ImageFrequency.once`, this returns [`enable_image_sensor`](../../api/command_api.md#enable_image_sensor),  [`send_images`](../../api/command_api.md#send_images), and [`send_camera_matrices`](../../api/command_api.md#send_camera_matrices). 
+- Most actions' `get_end_commands(resp, static, dynamic, image_frequency)` will include additional commands, e.g. commands to stop an animation.
+
+***
+
+**Next: [Output data](output_data.md)**
+
+[Return to the README](../../../README.md)
+
+***
+
+Example controllers:
+
+- [move_by.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/move_by.py) Move a Replicant by a target distance and print its status.
+
+Commands API:
+
+- [`set_target_framerate`](../../api/command_api.md#set_target_framerate)
+- [`enable_image_sensor`](../../api/command_api.md#enable_image_sensor)
+- [`send_images`](../../api/command_api.md#send_images)
+- [`send_camera_matrices`](../../api/command_api.md#send_camera_matrices)
+
+Python API:
+
+- [`Replicant`](../../python/add_ons/replicant.md)
+-  [`Action`](../../python/replicant/actions/action.md)
+-  [`MoveBy`](../../python/replicant/actions/move_by.md)
\ No newline at end of file
diff --git a/Documentation/lessons/replicants/animations.md b/Documentation/lessons/replicants/animations.md
new file mode 100644
index 000000000..5e7aa28e4
--- /dev/null
+++ b/Documentation/lessons/replicants/animations.md
@@ -0,0 +1,248 @@
+##### Replicants
+
+# Animations
+
+It is possible to tell the Replicant to move according to a prerecorded animation by calling  [`replicant.animate(animation)`](../../python/add_ons/replicant.md):
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.action_status import ActionStatus
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": -0.5, "y": 1.175, "z": 3},
+                           look_at={"x": 0, "y": 1, "z": 0},
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_animate")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=["a"], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate(TDWUtils.create_empty_room(12, 12))
+# Play an animation.
+replicant.animate(animation="kitchen_refrigerator_f")
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/animations/animate.gif)
+
+## Animation asset bundles and metadata records
+
+Like the [Replicant itself](overview.md), the Replicant's animations are stored as asset bundles that need to be downloaded and loaded into memory. Animation metadata is stored in the [`HumanoidAnimationLibrarian`](../../python/librarian/humanoid_animation_librarian.md):
+
+```python
+from tdw.librarian import HumanoidAnimationLibrarian
+
+lib = HumanoidAnimationLibrarian()
+for record in lib.records:
+    print(record.name)
+```
+
+To fetch a specific record:
+
+```python
+from tdw.librarian import HumanoidAnimationLibrarian
+
+lib = HumanoidAnimationLibrarian()
+record = lib.get_record("walking_1")
+print(record.name, record.framerate, record.get_num_frames(), record.loop)
+```
+
+Output:
+
+```
+walking_1 30 69 True
+```
+
+There are additional animations extracted from [SMPL](https://smpl.is.tue.mpg.de) in a separate librarian that can be used by Replicants:
+
+```python
+from tdw.librarian import HumanoidAnimationLibrarian
+
+lib = HumanoidAnimationLibrarian("smpl_animations.json")
+for record in lib.records:
+    print(record.name)
+```
+
+To use `smpl_animations.json` or other animation libraries, set the `library` parameter:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.action_status import ActionStatus
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": -0.5, "y": 1.175, "z": 3},
+                           look_at={"x": 0, "y": 1, "z": 0},
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_idle_to_crouch")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=["a"], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate(TDWUtils.create_empty_room(12, 12))
+# Play an animation. Note that we've set the library parameter.
+replicant.animate(animation="idle_to_crouch",
+                  library="smpl_animations.json")
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/animations/idle_to_crouch.gif)
+
+## Why there is a brief pause at the start of the animation
+
+Each animation is stored as a [humanoid animation asset bundle](../non_physics_humanoids/overview.md). The first time you call `animate(animation)`, the animation needs to be downloaded and loaded into memory, hence the delay at the start of the action. Like all asset bundles, this is a one-time requirement. If you call the same `animate(animation)` again, it will be begin immediately.
+
+## Physics and collision detection
+
+*For more information regarding collision detection, [read this.](collision_detection.md)*
+
+The `animate(animation)` action is responsive to physics in the scene. If the Replicant bumps into an object while moving, the object will move (assuming that the object is non-kinematic and that the Replicant's collision detection isn't actively avoiding objects).
+
+The `animate(animation)` action succeeds if the animation reaches its end. The action fails if the Replicant collides with an object during the animation, in which case `replicant.action.status` will be `ActionStatus.collision`.
+
+- If you try to play the same animation twice in a row but the first animation ends with `ActionStatus.collision`, the second animation will immediately end in `ActionStatus.collision`. This won't happen if the animations are different. To prevent this, set `replicant.collision_detection.previous_was_same = False`
+- To ignore a specific object, add its ID to `replicant.collision_detection.exclude_objects` (a list of integers).
+- To ignore all objects, set `replicant.collision_detection.objects = False`.
+
+This example controller demonstrates how physics and collision detection settings can affect an animation:
+
+ ```python
+ from tdw.controller import Controller
+ from tdw.tdw_utils import TDWUtils
+ from tdw.add_ons.third_person_camera import ThirdPersonCamera
+ from tdw.add_ons.image_capture import ImageCapture
+ from tdw.add_ons.replicant import Replicant
+ from tdw.replicant.action_status import ActionStatus
+ from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+ 
+ 
+ class AnimateCollisionDetection(Controller):
+     """
+     A minimal demo of how collision detection parameters affect animations.
+     """
+ 
+     def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+         super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+         self.replicant = Replicant(position={"x": -2, "y": 0, "z": 0},
+                                    rotation={"x": 0, "y": 90, "z": 0})
+         self.camera = ThirdPersonCamera(position={"x": -0.5, "y": 1.175, "z": 3},
+                                         look_at={"x": 0, "y": 1, "z": 0},
+                                         avatar_id="a")
+         path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_animate_collision_detection")
+         print(f"Images will be saved to: {path}")
+         self.capture = ImageCapture(avatar_ids=["a"], path=path)
+         self.add_ons.extend([self.replicant, self.camera, self.capture])
+         self.object_id_0 = Controller.get_unique_id()
+         self.object_id_1 = Controller.get_unique_id()
+ 
+     def initialize_scene(self):
+         self.communicate([TDWUtils.create_empty_room(12, 12),
+                           Controller.get_add_object(model_name="rh10",
+                                                     object_id=self.object_id_0,
+                                                     position={"x": -0.6, "y": 0, "z": 0.01}),
+                           Controller.get_add_object(model_name="chair_billiani_doll",
+                                                     object_id=self.object_id_1,
+                                                     position={"x": -1.5, "y": 0, "z": 0},
+                                                     rotation={"x": 0, "y": 30, "z": 0})])
+         self.camera.look_at(target=self.replicant.replicant_id)
+ 
+     def animate(self):
+         self.replicant.animate(animation="wading_through_water")
+         while self.replicant.action.status == ActionStatus.ongoing:
+             self.communicate([])
+         self.communicate([])
+         print(self.replicant.action.status)
+ 
+ 
+ if __name__ == "__main__":
+     c = AnimateCollisionDetection()
+     c.initialize_scene()
+     print("Trying to animate with default collision detection:")
+     c.animate()
+     print("Trying to ignore the rh10 model:")
+     c.replicant.collision_detection.exclude_objects.append(c.object_id_0)
+     c.animate()
+     print("Trying to ignore the previous animation:")
+     c.replicant.collision_detection.previous_was_same = False
+     c.animate()
+     print("Trying to ignore all objects:")
+     c.replicant.collision_detection.objects = False
+     c.animate()
+     c.communicate({"$type": "terminate"})
+ ```
+
+Result:
+
+![](images/animations/animate_collision_detection.gif)
+
+Output:
+
+```
+Trying to animate with default collision detection:
+ActionStatus.collision
+Trying to ignore the rh10 model:
+ActionStatus.collision
+Trying to ignore the previous animation:
+ActionStatus.collision
+Trying to ignore all objects:
+ActionStatus.success
+```
+
+## Animations and non-physics humanoids
+
+Animation asset bundles are actually the same as those used by [non-physics humanoids](../non_physics_humanoids/overview.md). The same animation can be applied both to a non-physics humanoid and to a Replicant and, under the hood, the commands used are exactly the same. The only difference is that the Replicant add-on is structured to treat the animation as an action, automatically downloading and playing the animation while continuously checking for collisions.
+
+## Low-level description
+
+`replicant.animate(animation)` sets `replicant.action` to an [`Animate`](../../python/replicant/actions/animate.md) action. 
+
+In addition to [the usual `Action` initialization commands](actions.md), `Animate` sends [`add_humanoid_animation`](../../api/command_api.md#add_humanoid_animation) and [`play_humanoid_animation`](../../api/command_api.md#play_humanoid_animation).
+
+The action continues until there is a collision or until `replicant.dynamic.output_action_status == ActionStatus.success` (meaning that the build has signaled that the animation ended).
+
+In addition to [the usual `Action` end commands](actions.md), `Animate` sends [`stop_humanoid_animation`](../../api/command_api.md#stop_humanoid_animation).
+
+***
+
+**Next: [Arm articulation](arm_articulation.md)**
+
+[Return to the README](../../../README.md)
+
+***
+
+Example controllers:
+
+- [animate.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/animate.py) Minimal example of how to play an animation.
+- [animate_collision_detection.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/animate_collision_detection.py) A minimal demo of how collision detection parameters affect animations.
+
+Command API:
+
+- [`add_humanoid_animation`](../../api/command_api.md#add_humanoid_animation)
+- [`play_humanoid_animation`](../../api/command_api.md#play_humanoid_animation)
+- [`stop_humanoid_animation`](../../api/command_api.md#stop_humanoid_animation)
+
+Python API:
+
+- [`Replicant`](../../python/add_ons/replicant.md)
+- [`CollisionDetection`](../../python/replicant/collision_detection.md)
+- [`HumanoidAnimationLibrarian`](../../python/librarian/humanoid_animation_librarian.md)
+- [`Animate`](../../python/replicant/actions/animate.md)
\ No newline at end of file
diff --git a/Documentation/lessons/replicants/arm_articulation.md b/Documentation/lessons/replicants/arm_articulation.md
new file mode 100644
index 000000000..84c083580
--- /dev/null
+++ b/Documentation/lessons/replicants/arm_articulation.md
@@ -0,0 +1,586 @@
+##### Replicants
+
+# Arm articulation
+
+Unlike [movement](movement.md) and [animations](animations.md), which are controlled by pre-recorded animation data, Replicant arm articulation is procedural. It uses inverse kinematic (IK) and the [FinalIK Unity asset](https://root-motion.com/) to solve an end pose, given a target position that a hand is reaching for.
+
+## The `reach_for(target, arm)` action
+
+The Replicant can reach for a target position or object via `reach_for(target, arm)`.
+
+### Reach for a target position
+
+To reach for a target position, set `target` to an x, y, z, dictionary or numpy array, and `arm` to an [`Arm`](../../python/replicant/arm.md) value:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 0, "y": 1.5, "z": 2.1},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reach_for_position")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate(TDWUtils.create_empty_room(12, 12))
+replicant.reach_for(target={"x": 0.8, "y": 0.9, "z": 0.3}, arm=Arm.right)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/arm_articulation/reach_for_position.gif)
+
+The Replicant can reach with both hands at the same time. To do this, set  `arm` to a list of arms:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 0, "y": 1.5, "z": 2.1},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reach_for_position_both_hands")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate(TDWUtils.create_empty_room(12, 12))
+replicant.reach_for(target={"x": 0.8, "y": 0.9, "z": 0.3}, arm=[Arm.left, Arm.right])
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+![](images/arm_articulation/reach_for_position_both_hands.gif)
+
+### Reach for a target object
+
+To reach for a target object, set `target` to an object ID integer:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 0, "y": 1.5, "z": 2.5},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reach_for_object")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+# Set the object ID.
+object_id = Controller.get_unique_id()
+commands = [TDWUtils.create_empty_room(12, 12)]
+commands.extend(Controller.get_add_physics_object(model_name="basket_18inx18inx12iin_wicker",
+                                                  object_id=object_id,
+                                                  position={"x": -0.1, "y": 0, "z": 0.6}))
+c.communicate(commands)
+replicant.reach_for(target=object_id, arm=Arm.right)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/arm_articulation/reach_for_object.gif)
+
+**Notice that the Replicant does *not* reach for the center of the object.** This is intentional.
+
+Certain models in TDW have "**affordance points**" that are automatically attached to them upon instantiation. Affordance points don't have meshes, colliders, etc.; they are just an transform with a position and rotation. Affordance points are parented to objects, meaning that when the object moves and rotates, the empty objects will follow within the local coordinate space.
+
+When you call `replicant.reach_for(target, arm)`, the Replicant will always try to reach for the *affordance point closest to the hand*. If there are no affordance points, the Replicant will reach for the *bounds position closest to the hand.*
+
+In the above example, `basket_18inx18inx12iin_wicker` has 4 affordance points, one on each side of the lid. This means that the Replicant will always try to reach for one of the sides of the basket.
+
+Right now, only a small amount of models in TDW have affordance points. To get a list of objects with affordance positions, review `model_record.empty_objects`:
+
+```python
+from tdw.librarian import ModelLibrarian
+
+lib = ModelLibrarian()
+for record in lib.records:
+    if record.do_not_use:
+        continue
+    if len(record.affordance_points) > 0:
+        print(record.name)
+```
+
+As with target *positions*, it is possible to reach for target *objects* with both hands. Each hand will aim for the nearest affordance point or bounds position, but will never reach for the same point in space:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 0, "y": 1.5, "z": 2.5},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reach_for_object_two_hands")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+# Set the object ID.
+object_id = Controller.get_unique_id()
+commands = [TDWUtils.create_empty_room(12, 12)]
+commands.extend(Controller.get_add_physics_object(model_name="basket_18inx18inx12iin_wicker",
+                                                  object_id=object_id,
+                                                  position={"x": -0.1, "y": 0, "z": 0.6}))
+c.communicate(commands)
+replicant.reach_for(target=object_id, arm=[Arm.left, Arm.right])
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/arm_articulation/reach_for_object_both_hands.gif)
+
+### Action success and collision detection
+
+The action succeeds if, when it ends, the hand is near the target (see below, `arrived_at`).
+
+The action has several fail states:
+
+- If, at the start of the action, the target is too far away, the action immediately fails with `ActionStatus.cannot_reach`.
+- If, at the end of the action, the target is too far away, the action ends with `ActionStatus.failed_to_reach`.
+- If, during the action, the Replicant collides with an object, the action ends with `ActionStatus.collision`. To suppress this behavior, set `replicant.collision_detection.objects = False` or add an object ID to `replicant.collision_detection.exclude_objects`
+
+In this example, the Replicant will walk to an object, pick it up, walk away, and drop the object. Notice that we add the trunk's ID to `collision_detection.exclude_objects`. This is because the Replicant's lower arm will collide with the trunk while reaching for the mug. **Unlike most of our examples controllers, this example is a subclass of `Controller`.** This is much closer to how you should structure your controllers. We've written a `do_action()` function to handle the basic Replicant action loop.
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+class GoToReachFor(Controller):
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        # Set the replicant and the object IDs here because we need to reference them elsewhere.
+        self.replicant = Replicant()
+        self.trunk_id = 1
+        self.mug_id = 2
+
+    def do_action(self) -> None:
+        while self.replicant.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        self.communicate([])
+
+    def run(self) -> None:
+        camera = ThirdPersonCamera(position={"x": -1.5, "y": 1.175, "z": 5.25},
+                                   look_at={"x": 0.5, "y": 1, "z": 0},
+                                   avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_go_to_reach")
+        print(f"Images will be saved to: {path}")
+        capture = ImageCapture(avatar_ids=["a"], path=path)
+        self.add_ons.extend([self.replicant, camera, capture])
+        # Create the room.
+        commands = [TDWUtils.create_empty_room(12, 12)]
+        commands.extend(Controller.get_add_physics_object(model_name="trunck",
+                                                          object_id=self.trunk_id,
+                                                          position={"x": 0, "y": 0, "z": 3},
+                                                          kinematic=True))
+        commands.extend(Controller.get_add_physics_object(model_name="coffeemug",
+                                                          object_id=self.mug_id,
+                                                          position={"x": 0, "y": 0.9888946, "z": 3}))
+        self.communicate(commands)
+        self.replicant.move_to(target=self.trunk_id)
+        self.do_action()
+        # Ignore the trunk.
+        self.replicant.collision_detection.exclude_objects.append(self.trunk_id)
+        self.replicant.reach_for(target=self.mug_id, arm=Arm.right)
+        self.do_action()
+        print(self.replicant.action.status)
+        self.communicate({"$type": "terminate"})
+
+
+if __name__ == "__main__":
+    c = GoToReachFor()
+    c.run()
+```
+
+Result:
+
+![](images/arm_articulation/go_to_reach_for.gif)
+
+Output:
+
+```
+ActionStatus.success
+```
+
+### The `absolute` parameter
+
+If `target` is a position (a dictionary or a numpy array, as opposed to a position), it defaults to a world space position. It's often useful, however, to set `absolute=False`, which defines `target` as being relative to the Replicant's position (but *not* the Replicant's rotation).
+
+### The `offhand_follows` parameter
+
+If the `arm` parameter is a single value (e.g. `Arm.left`, not `[Arm.left, Arm.right]`), you can set the optional parameter `offhand_follows=True`. This will make the offhand (the opposite of whatever `arm` is set to) follow the primary hand:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 0, "y": 1.5, "z": 2.5},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reach_for_follow")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate(TDWUtils.create_empty_room(12, 12))
+# Reach for a target with the right hand.
+replicant.reach_for(target={"x": 0.6, "y": 1.5, "z": 0.3}, arm=Arm.right)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+# Reach for a target with the left hand.
+replicant.reach_for(target={"x": -0.4, "y": 1, "z": 0.1}, arm=Arm.left)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+# Reach for a target with the right hand and have the left hand follow.
+replicant.reach_for(target={"x": 0.8, "y": 0.8, "z": 0.3}, arm=Arm.right, offhand_follows=True)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/arm_articulation/reach_for_follow.gif)
+
+### The `duration` parameter
+
+`duration` is an optional parameter that controls the speed in seconds of the arm motion:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 2, "y": 3, "z": 2.53},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reach_for_object_slow")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+# Set the object ID.
+object_id = Controller.get_unique_id()
+commands = [TDWUtils.create_empty_room(12, 12)]
+commands.extend(Controller.get_add_physics_object(model_name="basket_18inx18inx12iin_wicker",
+                                                  object_id=object_id,
+                                                  position={"x": -0.1, "y": 0, "z": 0.6}))
+c.communicate(commands)
+replicant.reach_for(target=object_id,
+                    arm=[Arm.left, Arm.right],
+                    duration=1.5)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/arm_articulation/reach_for_object_slow.gif)
+
+### The `arrived_at` parameter
+
+`arrived_at` controls the distance that defines a successful action. If, at the end of the action, the hand is this distance or less from the target position (a position, affordance point, or bounds position), the action is successful.
+
+### The `max_distance` parameter
+
+`max_distance` controls the maximum distance from the hand to the target. If at the start of the action the target is too far away, the action will immediately fail with `ActionStatus.cannot_reach`. You can extend this maximum distance, but the Replicant may behave very strangely:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 3.2, "y": 3, "z": 3.5},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reach_too_far")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate([TDWUtils.create_empty_room(12, 12)])
+target = {"x": 3, "y": 0.9, "z": 6}
+# The target is too far away.
+replicant.reach_for(target=target, arm=Arm.right)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+print(replicant.action.status)
+# Try to reach the target anyway.
+replicant.reach_for(target=target,
+                    arm=Arm.right,
+                    max_distance=8)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+print(replicant.action.status)
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/arm_articulation/reach_too_far.gif)
+
+Output:
+
+```
+ActionStatus.cannot_reach
+ActionStatus.failed_to_reach
+```
+
+## The `reset_arm(arm)` action
+
+Reset a Replicant's arm to its neutral position by calling `replicant.reset_arm(arm)`:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 2, "y": 3, "z": 2.53},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reset_arm")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate(TDWUtils.create_empty_room(12, 12))
+replicant.reach_for(target={"x": 0.8, "y": 0.9, "z": 0.3}, arm=Arm.right)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+replicant.reset_arm(arm=Arm.right)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/arm_articulation/reset_arm.gif)
+
+The `arm` parameter can be a single value such as `Arm.right` or a list of values, such as `[Arm.left, Arm.right]`, in which case both arms are reset.
+
+### The `duration` parameter
+
+`duration` is an optional parameter that controls the speed in seconds of the arm motion; it works the same way as it does in `reach_for(target, arm)`.
+
+### Action success and collision detection
+
+The action succeeds when the arm(s) finish resetting. The action can end in `ActionStatus.collision`. The collision detection rules for `reset_arm(arm)` are the same as those for `reach_for(target, arm)`; see above for more information.
+
+The action can't end in `ActionStatus.cannot_reach` or `ActionStatus.failed_to_reach` because the Replicant isn't reaching for a target.
+
+## Movement and arm resets
+
+When you call [`move_by(distance)` or `move_to(target)`](movement.md),  the Replicant's arms will automatically reset. This is *not* equivalent to calling `reset_arm(arm)` followed by `move_by(distance)`/`move_to(target)` because the Replicant's arms will move during the start of the walk motion, rather than before the walk motion begins:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 2, "y": 3, "z": 2.53},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reach_for_move")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate(TDWUtils.create_empty_room(12, 12))
+replicant.reach_for(target={"x": 0.8, "y": 0.9, "z": 0.3}, arm=Arm.right)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+replicant.move_by(3)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
+
+```
+
+Result:
+
+![](images/arm_articulation/reach_for_move.gif)
+
+You might want the Replicant to hold a pose with its arms while walking (for example, if it is [holding an object](grasp_drop.md)). If so, you can set the optional parameter `reset_arms=False` for `move_by` or `move_to`:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 2, "y": 3, "z": 2.53},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reach_for_move")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate(TDWUtils.create_empty_room(12, 12))
+replicant.reach_for(target={"x": 0.8, "y": 0.9, "z": 0.3}, arm=Arm.right)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+replicant.move_by(distance=3,
+                  reset_arms=False)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/arm_articulation/reach_for_move_no_reset.gif)
+
+You can control the speed at which the arms reset by setting the `reset_arms_duration` parameter in `move_by` or `move_to`.
+
+## Low-level description
+
+### The `reach_for(target, arm)` action
+
+`replicant.reach_for(target, arm)` sets `replicant.action` to an [`ReachFor`](../../python/replicant/actions/reach_for.md) action. 
+
+In addition to [the usual `Action` initialization commands](actions.md), `ReachFor` sends [`replicant_reach_for_position`](../../api/command_api.md#replicant_reach_for_position) or [`replicant_reach_for_object`](../../api/command_api.md#replicant_reach_for_object).
+
+The action continues until there is a collision or until `replicant.dynamic.output_action_status != ActionStatus.ongoing` (meaning that the build has signaled that the animation ended).
+
+### The `reset_arm(arm)` action
+
+`replicant.reset_arm(arm)` sets `replicant.action` to an [`ResetArm`](../../python/replicant/actions/reset_arm.md) action. 
+
+In addition to [the usual `Action` initialization commands](actions.md), `ResetArm` sends [`replicant_reset_arm`](../../api/command_api.md#replicant_reset_arm).
+
+The action continues until there is a collision or until `replicant.dynamic.output_action_status == ActionStatus.success` (meaning that the build has signaled that the animation ended).
+
+***
+
+**Next: [Grasp and drop objects](grasp_drop.md)**
+
+[Return to the README](../../../README.md)
+
+***
+
+Example controllers:
+
+- [reach_for_position.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/reach_for_position.py) Reach for a target position.
+- [reach_for_object.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/reach_for_object.py) Reach for a target object.
+- [reach_too_far.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/reach_too_far.py) Reach for a target that is too far away.
+- [reset_arm.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/reset_arm.py) Reach for a target position and then reset the arm.
+- [reach_for_move.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/reach_for_move.py) Reach for a target position and then move forward, resetting the arm.
+- [reach_for_follow.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/reach_for_follow.py) Reach for a target position and have the offhand follow the main hand.
+
+Command API:
+
+- [`replicant_reach_for_position`](../../api/command_api.md#replicant_reach_for_position)
+- [`replicant_reach_for_object`](../../api/command_api.md#replicant_reach_for_object)
+- [`replicant_reset_arm`](../../api/command_api.md#replicant_reset_arm)
+
+Python API:
+
+- [`Replicant`](../../python/add_ons/replicant.md)
+- [`Arm`](../../python/replicant/arm.md)
+- [`CollisionDetection`](../../python/replicant/collision_detection.md)
+- [`ModelRecord`](../../python/librarian/model_librarian.md)
+- [`ReachFor`](../../python/replicant/actions/reach_for.md)
+- [`ResetArm`](../../python/replicant/actions/reset_arm.md)
diff --git a/Documentation/lessons/replicants/collision_detection.md b/Documentation/lessons/replicants/collision_detection.md
new file mode 100644
index 000000000..4c6bb5a25
--- /dev/null
+++ b/Documentation/lessons/replicants/collision_detection.md
@@ -0,0 +1,51 @@
+##### Replicants
+
+# Collision Detection
+
+The Replicant will stop a motion if it detects a collision with an obstacle. It can also physically interact with objects in the scene.
+
+## How Replicant physics works
+
+If a Replicant collides with an object, by default its [action](actions.md) will end. This behavior can be overridden (see below). Assuming that the Replicant is *allowed* to collide with an object, it will physically interact with an object.
+
+In most respects, the Replicant will interact within the [physics engine](../physx/physx.md) like any other object. There is a significant difference, however: The Replicant is non-kinematic.
+
+The Replicant won't interact with other kinematic objects or environment objects, which means that, barring additional collision detection rules, the Replicant can move through kinematic objects and walls.
+
+The Replicant can induce forces (by pushing an object, throwing an object, etc.) but won't respond to forces. If an object is thrown at a Replicant, the object will bounce off the Replicant but the Replicant's position, speed, etc. won't be affected.
+
+## `replicant.collision_detection`
+
+The Replicant has a set of collision detection rules stored in `replicant.collision_detection`, which is a [`CollisionDetection`](../../python/replicant/collision_detection.md) data object.
+
+These are *rules* that don't actually affect the physics behavior. They are just booleans that are referenced in the Python code to decide how to respond to physics events. For example, if `replicant.collision_detection.objects == True`, the Replicant will stop an action if it collides with an object. This *doesn't* affect whether a Replicant *can* collide with objects; it always can collide with objects.
+
+Exactly how this works, and how to adjust the Replicant's behavior, differs between actions; see [Movement](movement.md), [Animation](animations.md), and [Arm articulation](arm_articulation.md) for more information.
+
+## Enabling and disabling collision detection rules
+
+`replicant.collision_detection` is designed to be adjusted during a simulation. You can, for example, tell the Replicant to ignore objects while walking and, once it is at its destination,  you can start an animation but tell the Replicant to stop the animation on object collisions.
+
+## Low-level description
+
+Replicant collision data isn't actually [the standard collision data](../physx/collisions.md). Instead, collision data is sent within [`Replicants`](../../api/output_data.md#Replicants), stored in [replicant_dynamic.collisions](../../python/replicant/replicant_dynamic.md). This data includes the body part ID and a list of object IDs that that body part collided with.
+
+Within the build, the Replicant will [cast overlaps](https://docs.unity3d.com/ScriptReference/Physics.OverlapCapsule.html) rather than rely on standard collision detection.  This allows it to detect collisions with kinematic objects and objects without Rigidbodies and it also allows the Replicant to differentiate between body parts. The tradeoff is that there is no data for relative velocity, contact points, etc. (which in this case aren't actually needed).
+
+***
+
+**Next: [Movement](movement.md)**
+
+[Return to the README](../../../README.md)
+
+***
+
+Output Data API:
+
+- [`Replicants`](../../api/output_data.md#Replicants)
+
+Python API:
+
+- [`Replicant`](../../python/add_ons/replicant.md)
+- [`CollisionDetection`](../../python/replicant/collision_detection.md)
+- [`ReplicantDynamic`](../../python/replicant/replicant_dynamic.md)
diff --git a/Documentation/lessons/replicants/custom_actions.md b/Documentation/lessons/replicants/custom_actions.md
new file mode 100644
index 000000000..6f0aa3b3b
--- /dev/null
+++ b/Documentation/lessons/replicants/custom_actions.md
@@ -0,0 +1,1501 @@
+##### Replicants
+
+# Custom actions
+
+[Replicant actions](actions.md) are designed to be easily subclassed. This document will cover two basic strategies for creating custom actions, as well as how to improve [the previous NavMesh example.](navigation.md)
+
+## 1. How to create a minimal `DoNothing` action
+
+Every action needs to be a subclass of [`Action`](../../python/replicant/actions/action.md) or a subclass of one of its subclasses. Every action needs to define `get_initialization_commands(resp, static, dynamic, image_frequency)` and `get_ongoing_commands(resp, static, dynamic)`.
+
+This is a minimal action:
+
+```python
+from typing import List
+from tdw.replicant.actions.action import Action
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class DoNothing(Action):
+    """
+    A minimal custom action.
+    """
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        # Get the standard initialization commands.
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        self.status = ActionStatus.success
+        return []
+```
+
+- In both functions, `resp` is the latest response from the build. Many actions need this to parse arbitrary output data. `static` is the Replicant's [`ReplicantStatic`](../../python/replicant/replicant_static.md) and `dynamic` is the Replicant's [`ReplicantDynamic`](../../python/replicant/replicant_dynamic.md); these will be handled within the `Replicant` add-on.
+- *Always* add `commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic, image_frequency=image_frequency)`. This will set up the camera and image capture correctly.
+- In `self.ongoing_commands(resp, static, dynamic)`, we want the command to end immediately in success, so we added `self.status = ActionStatus.success`.
+
+You can optionally add `get_end_commands(resp, static, dynamic, image_frequency)`. *This is not necessary.* You only need to add it if you need the action to send extra commands when it ends. This function always gets called when an action ends, regardless of whether it succeeds:
+
+```python
+from typing import List
+from tdw.replicant.actions.action import Action
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class DoNothing(Action):
+    """
+    A minimal custom action.
+    """
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        # Get the standard initialization commands.
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        self.status = ActionStatus.success
+        return []
+    
+    def get_end_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                         image_frequency: ImageFrequency) -> List[dict]:
+        return super().get_end_commands(resp=resp, static=static, dynamic=dynamic, image_frequency=image_frequency)
+```
+
+To add this action do a controller, simply manually assign `replicant.action = DoNothing()`:
+
+```python
+from typing import List
+from tdw.tdw_utils import TDWUtils
+from tdw.controller import Controller
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.actions.action import Action
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class DoNothing(Action):
+    """
+    A minimal custom action.
+    """
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        # Get the standard initialization commands.
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        self.status = ActionStatus.success
+        return []
+    
+    def get_end_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                         image_frequency: ImageFrequency) -> List[dict]:
+        return super().get_end_commands(resp=resp, static=static, dynamic=dynamic, image_frequency=image_frequency)
+
+
+if __name__ == "__main__":
+    c = Controller()
+    replicant = Replicant()
+    c.add_ons.append(replicant)
+    c.communicate(TDWUtils.create_empty_room(12, 12))
+    replicant.action = DoNothing()
+    while replicant.action.status == ActionStatus.ongoing:
+        c.communicate([])
+    c.communicate([])
+    print(replicant.action.status)
+    c.communicate({"$type": "terminate"})
+```
+
+Output:
+
+```
+ActionStatus.success
+```
+
+You can wrap the new `DoNothing` action in a Replicant function such as `replicant.do_nothing()`. This is totally optional, though. If you wish to do this, create a custom subclass of Replicant and add your function:
+
+```python
+from typing import List
+from tdw.tdw_utils import TDWUtils
+from tdw.controller import Controller
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.actions.action import Action
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class DoNothing(Action):
+    """
+    A minimal custom action.
+    """
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        # Get the standard initialization commands.
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        self.status = ActionStatus.success
+        return []
+
+    def get_end_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                         image_frequency: ImageFrequency) -> List[dict]:
+        return super().get_end_commands(resp=resp, static=static, dynamic=dynamic, image_frequency=image_frequency)
+    
+
+class MyReplicant(Replicant):
+    def do_nothing(self) -> None:
+        self.action = DoNothing()
+
+
+if __name__ == "__main__":
+    c = Controller()
+    replicant = MyReplicant()
+    c.add_ons.append(replicant)
+    c.communicate(TDWUtils.create_empty_room(12, 12))
+    replicant.do_nothing()
+    while replicant.action.status == ActionStatus.ongoing:
+        c.communicate([])
+    c.communicate([])
+    print(replicant.action.status)
+    c.communicate({"$type": "terminate"})
+```
+
+## 2. How to create an action using a state machine and create a `Clap` action
+
+Now we're going to define a much more complicated action: `Clap`, which tells the Replicant to clap its hands.
+
+### 2.1 Create the state machine
+
+We're going to use a *state machine* to handle the motion. A state machine is a basic concept in algorithms that just means that the action has been divided into discrete *states*. In this case, there are three states:
+
+1. The Replicant raises its hands.
+2. The Replicant brings its hands together.
+3. The Replicant pulls its hands apart.
+
+When state 1 ends, state 2 begins. When state 2 ends, state 3 begins. When state 3 ends, the action ends.
+
+To start, we'll define our state machine as a subclass of `Enum`:
+
+```python
+from enum import Enum
+
+class ClapState(Enum):
+    raising_hands = 1
+    coming_together = 2
+    pulling_apart = 4
+```
+
+### 2.2 Create the `Clap` constructor
+
+Now we're going to start to write our `Clap` action. Instead of being a subclass of `Action`, this will be a subclass of [`ArmMotion`](../../python/replicant/actions/arm_motion.md), the abstract class used by [`ReachFor`](../../python/replicant/actions/reach_for.md) and [`ResetArm`](../../python/replicant/actions/reset_arm.md). We want to subclass `ArmMotion` because it automatically handles [collision detection](collision_detection.md) for us while the arms are moving:
+
+```python
+from enum import Enum
+from tdw.replicant.actions.arm_motion import ArmMotion
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.arm import Arm
+
+
+class ClapState(Enum):
+    raising_hands = 1
+    coming_together = 2
+    pulling_apart = 4
+
+
+class Clap(ArmMotion):
+    """
+    Clap your hands.
+    """
+    
+    def __init__(self, dynamic: ReplicantDynamic, collision_detection: CollisionDetection):
+        super().__init__(arms=[Arm.left, Arm.right], dynamic=dynamic, collision_detection=collision_detection,
+                         duration=0.1, previous=None)
+        self.clap_state: ClapState = ClapState.raising_hands
+```
+
+- We need to call `super().__init__(...)` because that will call the constructor for `ArmMotion` and set up the rest of the action correctly. We've set `arms` to both arms because you need both hands to clap, and `duration` to a fast speed.
+- `self.clap_state` will manage the state machine of this action. When the action begins, the state is `raising_hands`.
+
+### 2.3 Define helper functions
+
+We're going to use a few helper functions that will be used throughout the rest of the function.
+
+First, we need a function that defines the position where the clap will occur. This should be at chest-height, in front of the Replicant:
+
+```python
+from enum import Enum
+import numpy as np
+from tdw.replicant.actions.arm_motion import ArmMotion
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.arm import Arm
+
+
+class ClapState(Enum):
+    raising_hands = 1
+    coming_together = 2
+    pulling_apart = 4
+
+
+class Clap(ArmMotion):
+    """
+    Clap your hands.
+    """
+
+    def __init__(self, dynamic: ReplicantDynamic, collision_detection: CollisionDetection):
+        super().__init__(arms=[Arm.left, Arm.right], dynamic=dynamic, collision_detection=collision_detection,
+                         duration=0.1, previous=None)
+        self.clap_state: ClapState = ClapState.raising_hands
+
+    @staticmethod
+    def get_clap_position(dynamic: ReplicantDynamic) -> np.ndarray:
+        # Get a position in front of the Replicant.
+        position = dynamic.transform.position.copy()
+        # Move the position in front of the Replicant.
+        position += dynamic.transform.forward * 0.4
+        # Set the height of the position.
+        position[1] = 1.5
+        return position
+```
+
+Next, we need a function for the initial positions of the hands. We'll get the "clap position" and then rotate it -90 degrees and 90 degrees for the left and right hands. Then, we'll return two [`replicant_reach_for_position`](../../api/command_api.md#replicant_reach_for_position) commands:
+
+```python
+from enum import Enum
+import numpy as np
+from tdw.replicant.actions.arm_motion import ArmMotion
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.arm import Arm
+
+
+class ClapState(Enum):
+    raising_hands = 1
+    coming_together = 2
+    pulling_apart = 4
+
+
+class Clap(ArmMotion):
+    """
+    Clap your hands.
+    """
+
+    def __init__(self, dynamic: ReplicantDynamic, collision_detection: CollisionDetection):
+        super().__init__(arms=[Arm.left, Arm.right], dynamic=dynamic, collision_detection=collision_detection,
+                         duration=0.1, previous=None)
+        self.clap_state: ClapState = ClapState.raising_hands
+
+    @staticmethod
+    def get_clap_position(dynamic: ReplicantDynamic) -> np.ndarray:
+        # Get a position in front of the Replicant.
+        position = dynamic.transform.position.copy()
+        # Move the position in front of the Replicant.
+        position += dynamic.transform.forward * 0.4
+        # Set the height of the position.
+        position[1] = 1.5
+        return position
+    
+    def get_initial_position_commands(self, static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Get the position.
+        position = Clap.get_clap_position(dynamic=dynamic)
+        origin = dynamic.transform.position.copy()
+        origin[1] = position[1]
+        forward = dynamic.transform.forward * 0.4
+        # Rotate the position to the side.
+        left = TDWUtils.rotate_position_around(position=position,
+                                               origin=origin,
+                                               angle=-90)
+        # Move the position forward.
+        left += forward
+        # Rotate the position to the side.
+        right = TDWUtils.rotate_position_around(position=position,
+                                                origin=origin,
+                                                angle=90)
+        # Move the position forward.
+        right += forward
+        commands = []
+        # Reach for the initial positions.
+        for arm, position in zip(self.arms, [left, right]):
+            commands.append({"$type": "replicant_reach_for_position",
+                             "id": static.replicant_id,
+                             "position": TDWUtils.array_to_vector3(position),
+                             "duration": self.duration,
+                             "arm": arm.name})
+        return commands
+```
+
+### 2.4 Define `get_initialization_commands()`
+
+As with all actions, we need to define `get_initialization_commands(resp, static, dynamic, image_frequency)`. In this case, we'll get the standard initialization commands with `super()` and then extend that list with `get_initial_position_commands(static, dynamic)`:
+
+```python
+from typing import List
+from enum import Enum
+import numpy as np
+from tdw.replicant.actions.arm_motion import ArmMotion
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.arm import Arm
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class ClapState(Enum):
+    raising_hands = 1
+    coming_together = 2
+    pulling_apart = 4
+
+
+class Clap(ArmMotion):
+    """
+    Clap your hands.
+    """
+
+    def __init__(self, dynamic: ReplicantDynamic, collision_detection: CollisionDetection):
+        super().__init__(arms=[Arm.left, Arm.right], dynamic=dynamic, collision_detection=collision_detection,
+                         duration=0.1, previous=None)
+        self.clap_state: ClapState = ClapState.raising_hands
+        
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        # Get the standard initialization commands.
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        # Get the initial position of each hand.
+        commands.extend(self.get_initial_position_commands(static=static, dynamic=dynamic))
+        return commands
+
+    @staticmethod
+    def get_clap_position(dynamic: ReplicantDynamic) -> np.ndarray:
+        # Get a position in front of the Replicant.
+        position = dynamic.transform.position.copy()
+        # Move the position in front of the Replicant.
+        position += dynamic.transform.forward * 0.4
+        # Set the height of the position.
+        position[1] = 1.5
+        return position
+
+    def get_initial_position_commands(self, static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Get the position.
+        position = Clap.get_clap_position(dynamic=dynamic)
+        origin = dynamic.transform.position.copy()
+        origin[1] = position[1]
+        forward = dynamic.transform.forward * 0.4
+        # Rotate the position to the side.
+        left = TDWUtils.rotate_position_around(position=position,
+                                               origin=origin,
+                                               angle=-90)
+        # Move the position forward.
+        left += forward
+        # Rotate the position to the side.
+        right = TDWUtils.rotate_position_around(position=position,
+                                                origin=origin,
+                                                angle=90)
+        # Move the position forward.
+        right += forward
+        commands = []
+        # Reach for the initial positions.
+        for arm, position in zip(self.arms, [left, right]):
+            commands.append({"$type": "replicant_reach_for_position",
+                             "id": static.replicant_id,
+                             "position": TDWUtils.array_to_vector3(position),
+                             "duration": self.duration,
+                             "arm": arm.name})
+        return commands
+```
+
+### 2.5 Define `get_ongoing_commands()`
+
+This is where most of any action's logic is handled. This action gets call on every `communicate()` call. We'll use it to evaluate the current state of the Replicant and update the state if needed.
+
+First, we'll call `super()`, which, because this is a subclass of `ArmMotion`, will check for collisions:
+
+```
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Continue the action, checking for collisions.
+        commands = super().get_ongoing_commands(resp=resp, static=static, dynamic=dynamic)
+```
+
+If `self.status != ActionStatus.ongoing`, that doesn't mean that action has *actually* ended; it just means that the current *motion* has ended. Every time the Replicant finishes reaching for a target, the build will signal that the motion is complete. We're going to use that information to check whether we need to start the next *clap state*:
+
+```
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Continue the action, checking for collisions.
+        commands = super().get_ongoing_commands(resp=resp, static=static, dynamic=dynamic)
+        # The motion ended. Decide if we need to do more motions.
+        # It's ok in this case if the motion ends in failed_to_reach because we don't need it to be precise.
+        if self.status == ActionStatus.success or self.status == ActionStatus.failed_to_reach:
+```
+
+The initial clap state is `raising_hands`. If the *motion* ended and the *state* is `raising_hands`, then we need to set our *status* to `ongoing` and the *state* to `coming_together`. We also need to reach for a new target position:
+
+```
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Continue the action, checking for collisions.
+        commands = super().get_ongoing_commands(resp=resp, static=static, dynamic=dynamic)
+        # The motion ended. Decide if we need to do more motions.
+        # It's ok in this case if the motion ends in failed_to_reach because we don't need it to be precise.
+        if self.status == ActionStatus.success or self.status == ActionStatus.failed_to_reach:
+            # We're done raising the hands. Bring the hands together.
+            if self.clap_state == ClapState.raising_hands:
+                # The action is ongoing.
+                self.status = ActionStatus.ongoing
+                # The state is coming together.
+                self.clap_state = ClapState.coming_together
+                # Get a position in front of the Replicant.
+                position = self.get_clap_position(dynamic=dynamic)
+                # Tell both hands to reach for the target position.
+                commands = []
+                for arm in self.arms:
+                    commands.append({"$type": "replicant_reach_for_position",
+                                     "id": static.replicant_id,
+                                     "position": TDWUtils.array_to_vector3(position),
+                                     "duration": self.duration,
+                                     "arm": arm.name})
+```
+
+If the *motion* ends and the *state* is `coming_together`, then it's time to pull the hands apart:
+
+```
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Continue the action, checking for collisions.
+        commands = super().get_ongoing_commands(resp=resp, static=static, dynamic=dynamic)
+        # The motion ended. Decide if we need to do more motions.
+        # It's ok in this case if the motion ends in failed_to_reach because we don't need it to be precise.
+        if self.status == ActionStatus.success or self.status == ActionStatus.failed_to_reach:
+            # We're done raising the hands. Bring the hands together.
+            if self.clap_state == ClapState.raising_hands:
+                # The action is ongoing.
+                self.status = ActionStatus.ongoing
+                # The state is coming together.
+                self.clap_state = ClapState.coming_together
+                # Get a position in front of the Replicant.
+                position = self.get_clap_position(dynamic=dynamic)
+                # Tell both hands to reach for the target position.
+                commands = []
+                for arm in self.arms:
+                    commands.append({"$type": "replicant_reach_for_position",
+                                     "id": static.replicant_id,
+                                     "position": TDWUtils.array_to_vector3(position),
+                                     "duration": self.duration,
+                                     "arm": arm.name})
+            # We're done moving the hands together. Bring the hands apart again.
+            elif self.clap_state == ClapState.coming_together:
+                # The action is ongoing.
+                self.status = ActionStatus.ongoing
+                # The state is pulling apart.
+                self.clap_state = ClapState.pulling_apart
+                # Reach for the initial positions.
+                commands.extend(self.get_initial_position_commands(static=static, dynamic=dynamic))
+```
+
+If the *motion* ends and the *state* is `pulling_apart`, then the action is done.
+
+### 2.6 Use `Clap` in a controller
+
+We'll assign and run `Clap` the same way we assigned and ran `DoNothing`:
+
+```python
+from typing import List
+from enum import Enum
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.controller import Controller
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.actions.arm_motion import ArmMotion
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.arm import Arm
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class ClapState(Enum):
+    raising_hands = 1
+    coming_together = 2
+    pulling_apart = 4
+
+
+class Clap(ArmMotion):
+    """
+    Clap your hands.
+    """
+
+    def __init__(self, dynamic: ReplicantDynamic, collision_detection: CollisionDetection):
+        super().__init__(arms=[Arm.left, Arm.right], dynamic=dynamic, collision_detection=collision_detection,
+                         duration=0.1, previous=None)
+        self.clap_state: ClapState = ClapState.raising_hands
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        # Get the standard initialization commands.
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        # Get the initial position of each hand.
+        commands.extend(self.get_initial_position_commands(static=static, dynamic=dynamic))
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Continue the action, checking for collisions.
+        commands = super().get_ongoing_commands(resp=resp, static=static, dynamic=dynamic)
+        # The motion ended. Decide if we need to do more motions.
+        # It's ok in this case if the motion ends in failed_to_reach because we don't need it to be precise.
+        if self.status == ActionStatus.success or self.status == ActionStatus.failed_to_reach:
+            # We're done raising the hands. Bring the hands together.
+            if self.clap_state == ClapState.raising_hands:
+                # The action is ongoing.
+                self.status = ActionStatus.ongoing
+                # The state is coming together.
+                self.clap_state = ClapState.coming_together
+                # Get a position in front of the Replicant.
+                position = self.get_clap_position(dynamic=dynamic)
+                # Tell both hands to reach for the target position.
+                commands = []
+                for arm in self.arms:
+                    commands.append({"$type": "replicant_reach_for_position",
+                                     "id": static.replicant_id,
+                                     "position": TDWUtils.array_to_vector3(position),
+                                     "duration": self.duration,
+                                     "arm": arm.name})
+            # We're done moving the hands together. Bring the hands apart again.
+            elif self.clap_state == ClapState.coming_together:
+                # The action is ongoing.
+                self.status = ActionStatus.ongoing
+                # The state is pulling apart.
+                self.clap_state = ClapState.pulling_apart
+                # Reach for the initial positions.
+                commands.extend(self.get_initial_position_commands(static=static, dynamic=dynamic))
+            # If the motion is successful and the state is `pulling_apart`, then we're done.
+            elif self.clap_state == ClapState.pulling_apart:
+                self.status = ActionStatus.success
+        return commands
+
+    @staticmethod
+    def get_clap_position(dynamic: ReplicantDynamic) -> np.ndarray:
+        # Get a position in front of the Replicant.
+        position = dynamic.transform.position.copy()
+        # Move the position in front of the Replicant.
+        position += dynamic.transform.forward * 0.4
+        # Set the height of the position.
+        position[1] = 1.5
+        return position
+
+    def get_initial_position_commands(self, static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Get the position.
+        position = Clap.get_clap_position(dynamic=dynamic)
+        origin = dynamic.transform.position.copy()
+        origin[1] = position[1]
+        forward = dynamic.transform.forward * 0.4
+        # Rotate the position to the side.
+        left = TDWUtils.rotate_position_around(position=position,
+                                               origin=origin,
+                                               angle=-90)
+        # Move the position forward.
+        left += forward
+        # Rotate the position to the side.
+        right = TDWUtils.rotate_position_around(position=position,
+                                                origin=origin,
+                                                angle=90)
+        # Move the position forward.
+        right += forward
+        commands = []
+        # Reach for the initial positions.
+        for arm, position in zip(self.arms, [left, right]):
+            commands.append({"$type": "replicant_reach_for_position",
+                             "id": static.replicant_id,
+                             "position": TDWUtils.array_to_vector3(position),
+                             "duration": self.duration,
+                             "arm": arm.name})
+        return commands
+
+
+if __name__ == "__main__":
+    c = Controller()
+    replicant = Replicant()
+    camera = ThirdPersonCamera(position={"x": 0, "y": 1.5, "z": 2.5},
+                               look_at=replicant.replicant_id,
+                               avatar_id="a")
+    path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_clap")
+    print(f"Images will be saved to: {path}")
+    capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+    c.add_ons.extend([replicant, camera, capture])
+    c.communicate(TDWUtils.create_empty_room(12, 12))
+    replicant.action = Clap(dynamic=replicant.dynamic, collision_detection=replicant.collision_detection)
+    while replicant.action.status == ActionStatus.ongoing:
+        c.communicate([])
+    c.communicate([])
+    print(replicant.action.status)
+    c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/custom_actions/clap.gif)
+
+## 3. How to perform actions within actions and create a `Navigate` action
+
+It's often useful to perform existing actions within actions. [`MoveTo`](../../python/replicant/actions/move_to.md), for example, first does a [`TurnTo`](../../python/replicant/actions/turn_to.md)  action followed by a [`MoveBy`](../../python/replicant/actions/move_by.md) action (and it uses a state machine to decide which action to perform). For the sake of clarity, we'll differentiate between the "parent action" and "child action" but these are informal terms. The parent action is the one being assigned to `replicant.action` and the child action is being referenced within the parent action. There can be multiple child actions in a list but only one should be referenced per `communicate()` call.
+
+Referencing a child action within a parent action is simple:
+
+- Within the parent action's `get_initialization_commands()`, function, append the commands from `child.get_initialization_commands()`. 
+- Within the parent action's `get_ongoing_commands()`, function, append the commands from `child.get_ongoing_commands()`.  If needed, reference `child.status` as well.
+- If needed, within the parent action's `get_end_commands()`, function, append the commands from `child.get_end_commands()`.
+
+In this example, we'll define a `Navigate` action. This will be similar to [the previous NavMesh example](navigation.md) but with one big difference: by handling the motion in an action, we can detach it from the standard `do_action()` loop, meaning that the action can be interrupted and that it can be used in a [multi-agent simulation](multiple_replicants.md).
+
+### 3.1 Create the state machine
+
+Once again, we'll define a state machine:
+
+```python
+from enum import Enum
+
+class NavigateState(Enum):
+    getting_path = 1
+    navigating = 2
+```
+
+### 3.2  Create the `Navigate` constructor
+
+The `Navigate` constructor is much more complex than the `Clap` constructor:
+
+- We have two parameters: `target` and `collision_detection`.
+  - The `target` is declared as `target: Union[Dict[str, float], np.ndarray]`, which includes [type hinting](https://docs.python.org/3/library/typing.html). This is strictly optional but it can be useful when working in certain IDEs that will warn you about incorrect types. In this case, `target` can be a dictionary or a numpy array. `self.target` will be a dictionary.
+  - `collision_detection` is a [`CollisionDetection`](../../python/replicant/collision_detection.md) object.
+
+```python
+from enum import Enum
+from typing import Union, Dict
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.replicant.actions.action import Action
+from tdw.replicant.collision_detection import CollisionDetection
+
+
+class NavigateState(Enum):
+    getting_path = 1
+    navigating = 2
+
+
+class Navigate(Action):
+    """
+    Navigate using a NavMeshPath and multiple MoveTo child actions.
+    """
+
+    def __init__(self, target: Union[Dict[str, float], np.ndarray], collision_detection: CollisionDetection):
+        super().__init__()
+        # My collision detection. This will be used to instantiate MoveTo child actions.
+        self.collision_detection: CollisionDetection = collision_detection
+        # Convert the target position to a dictionary if needed.
+        if isinstance(target, np.ndarray):
+            self.target: Dict[str, float] = TDWUtils.array_to_vector3(target)
+        elif isinstance(target, dict):
+            self.target = target
+        else:
+            raise Exception(target)
+```
+
+This sort of type hinting and type checking using `isinstance()` is totally optional. You could simplify this and just assume that the user is always going to set `target` to an x, y, z dictionary.
+
+We have additional fields to declare:
+
+- `self.navigation_state` is our state machine flag.
+- `self.path` is the NavMesh path. Initially, it will be empty.
+- `self.path_index` is the current index in `self.path`. Initially, it will be 1 (because element 0 is always the Replicant's current position).
+- `self.move_to` is the *child action.* It is a [`MoveTo`](../../python/replicant/actions/move_to.md) that will be continuously re-instantiated every time we need to move to a new position.
+- `self.image_frequency` will be set in `get_initialization_commands()`. We need this for later, when we re-instantiate our child object and call `get_initialization_commands(resp, static, dynamic, image_frequency)` within `get_ongoing_commands(resp, static, dynamic)`, which *doesn't* have an `image_frequency` parameter.
+- For the sake of navigating a NavMesh, we want to disable [obstacle detection](movement.md) and re-enable it at the end of `Navigate`. So, we'll store the current obstacle detection rules in `self.original_avoid`
+
+This is the completed constructor:
+
+```python
+from enum import Enum
+from typing import Union, Dict, Optional
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.replicant.actions.action import Action
+from tdw.replicant.actions.move_to import MoveTo
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class NavigateState(Enum):
+    getting_path = 1
+    navigating = 2
+
+
+class Navigate(Action):
+    """
+    Navigate using a NavMeshPath and multiple MoveTo child actions.
+    """
+
+    def __init__(self, target: Union[Dict[str, float], np.ndarray], collision_detection: CollisionDetection):
+        super().__init__()
+        # My collision detection. This will be used to instantiate MoveTo child actions.
+        self.collision_detection: CollisionDetection = collision_detection
+        # Convert the target position to a dictionary if needed.
+        if isinstance(target, np.ndarray):
+            self.target: Dict[str, float] = TDWUtils.array_to_vector3(target)
+        elif isinstance(target, dict):
+            self.target = target
+        else:
+            raise Exception(target)
+        # We are getting the path.
+        self.navigate_state: NavigateState = NavigateState.getting_path
+        # My path.
+        self.path: np.ndarray = np.zeros(shape=0)
+        # The index of the current waypoint in the path. The point at index=0 is the current position.
+        self.path_index: int = 1
+        # My MoveTo action.
+        self.move_to: Optional[MoveTo] = None
+        # The ImageFrequency. This will be used to instantiate MoveTo child actions.
+        # This will be set in get_initialization_commands()
+        self.image_frequency: ImageFrequency = ImageFrequency.once
+        # Don't try to detect obstacles while pathfinding.
+        self.original_avoid: bool = self.collision_detection.avoid
+        self.collision_detection.avoid = False
+```
+
+### 3.3 Define helper functions
+
+We'll define one helper function, which will instantiate a new [`MoveTo`](../../python/replicant/actions/move_to.md), update the path index, and return the child action's initialization commands:
+
+```python
+from enum import Enum
+from typing import List, Union, Dict, Optional
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.replicant.actions.action import Action
+from tdw.replicant.actions.move_to import MoveTo
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class NavigateState(Enum):
+    getting_path = 1
+    navigating = 2
+
+
+class Navigate(Action):
+    """
+    Navigate using a NavMeshPath and multiple MoveTo child actions.
+    """
+
+    def __init__(self, target: Union[Dict[str, float], np.ndarray], collision_detection: CollisionDetection):
+        super().__init__()
+        # My collision detection. This will be used to instantiate MoveTo child actions.
+        self.collision_detection: CollisionDetection = collision_detection
+        # Convert the target position to a dictionary if needed.
+        if isinstance(target, np.ndarray):
+            self.target: Dict[str, float] = TDWUtils.array_to_vector3(target)
+        elif isinstance(target, dict):
+            self.target = target
+        else:
+            raise Exception(target)
+        # We are getting the path.
+        self.navigate_state: NavigateState = NavigateState.getting_path
+        # My path.
+        self.path: np.ndarray = np.zeros(shape=0)
+        # The index of the current waypoint in the path. The point at index=0 is the current position.
+        self.path_index: int = 1
+        # My MoveTo action.
+        self.move_to: Optional[MoveTo] = None
+        # The ImageFrequency. This will be used to instantiate MoveTo child actions.
+        # This will be set in get_initialization_commands()
+        self.image_frequency: ImageFrequency = ImageFrequency.once
+        # Don't try to detect obstacles while pathfinding.
+        self.original_avoid: bool = self.collision_detection.avoid
+        self.collision_detection.avoid = False
+        
+    def set_move_to(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Set the action.
+        self.move_to = MoveTo(target=self.path[self.path_index],
+                              collision_detection=self.collision_detection,
+                              reset_arms=True,
+                              reset_arms_duration=0.25,
+                              arrived_at=0.1,
+                              max_walk_cycles=100,
+                              bounds_position="center",
+                              previous=None)
+        # Update the path index
+        self.path_index += 1
+        # Return the initialization commands.
+        return self.move_to.get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                        image_frequency=self.image_frequency)
+```
+
+### 3.4 Define `get_initialization_commands()`
+
+`Navigate.get_initialization_commands()` *does not* initialize its child action. This is because we need to first get path data from the build. We'll also store the `image_frequency` value for future use:
+
+```python
+from enum import Enum
+from typing import List, Union, Dict, Optional
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.replicant.actions.action import Action
+from tdw.replicant.actions.move_to import MoveTo
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class NavigateState(Enum):
+    getting_path = 1
+    navigating = 2
+
+
+class Navigate(Action):
+    """
+    Navigate using a NavMeshPath and multiple MoveTo child actions.
+    """
+
+    def __init__(self, target: Union[Dict[str, float], np.ndarray], collision_detection: CollisionDetection):
+        super().__init__()
+        # My collision detection. This will be used to instantiate MoveTo child actions.
+        self.collision_detection: CollisionDetection = collision_detection
+        # Convert the target position to a dictionary if needed.
+        if isinstance(target, np.ndarray):
+            self.target: Dict[str, float] = TDWUtils.array_to_vector3(target)
+        elif isinstance(target, dict):
+            self.target = target
+        else:
+            raise Exception(target)
+        # We are getting the path.
+        self.navigate_state: NavigateState = NavigateState.getting_path
+        # My path.
+        self.path: np.ndarray = np.zeros(shape=0)
+        # The index of the current waypoint in the path. The point at index=0 is the current position.
+        self.path_index: int = 1
+        # My MoveTo action.
+        self.move_to: Optional[MoveTo] = None
+        # The ImageFrequency. This will be used to instantiate MoveTo child actions.
+        # This will be set in get_initialization_commands()
+        self.image_frequency: ImageFrequency = ImageFrequency.once
+        # Don't try to detect obstacles while pathfinding.
+        self.original_avoid: bool = self.collision_detection.avoid
+        self.collision_detection.avoid = False
+    
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        # Remember the ImageFrequency because we'll need it to instantiate MoveTo child actions.
+        self.image_frequency = image_frequency
+        # Get the usual initialization commands.
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        # Request a NavMeshPath. Set the path's ID to the Replicant's ID so we know whose path this is.
+        commands.append({"$type": "send_nav_mesh_path",
+                         "origin": TDWUtils.array_to_vector3(dynamic.transform.position),
+                         "destination": self.target,
+                         "id": static.replicant_id})
+        return commands
+
+    def set_move_to(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Set the action.
+        self.move_to = MoveTo(target=self.path[self.path_index],
+                              collision_detection=self.collision_detection,
+                              reset_arms=True,
+                              reset_arms_duration=0.25,
+                              arrived_at=0.1,
+                              max_walk_cycles=100,
+                              bounds_position="center",
+                              previous=None)
+        # Update the path index
+        self.path_index += 1
+        # Return the initialization commands.
+        return self.move_to.get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                        image_frequency=self.image_frequency)
+```
+
+### 2.5 Define `get_ongoing_commands()`
+
+If the state is `getting_path`, cache the NavMeshPath path data and instantiate the first `MoveTo` child action. Set the state to `navigating`:
+
+```
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Get my path.
+        if self.navigate_state == NavigateState.getting_path:
+            # We are now navigating.
+            self.navigate_state = NavigateState.navigating
+            # Parse the output data.
+            for i in range(len(resp) - 1):
+                r_id = OutputData.get_data_type_id(resp[i])
+                # Got NavMeshPath data.
+                if r_id == "path":
+                    path = NavMeshPath(resp[i])
+                    # This is my path.
+                    if path.get_id() == static.replicant_id:
+                        # We failed to pathfind to this destination.
+                        if path.get_state() != "complete":
+                            self.status = ActionStatus.failed_to_move
+                        # This is a valid path.
+                        else:
+                            self.path = path.get_path()
+                            # Start moving.
+                            return self.set_move_to(resp=resp, static=static, dynamic=dynamic)
+```
+
+If the state is `navigating`, call `self.move_to.get_ongoing_commands()`. This will return commands as well as update `self.move_to.status`:
+
+- If `self.move_to.status == ActionStatus.ongoing`, return the commands without updating anything.
+- If `self.move_to.status == ActionStatus.success`, one of two things will happen:
+  - If `self.path_index == self.path.shape[0]`, this was the final waypoint in the path. We're done! Set `self.status = ActionStatus.success`
+  - Otherwise: Instantiate a new `MoveTo` child action and append its initialization commands. Notice that we haven't updated `self.status`, which is still `ActionStatus.ongoing`.
+- Otherwise: The `MoveTo` action failed, which means that this action failed. Set `self.status = self.move_to.status` to get the fail status.
+
+```python
+from enum import Enum
+from typing import List, Union, Dict, Optional
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.output_data import OutputData, NavMeshPath
+from tdw.replicant.actions.action import Action
+from tdw.replicant.actions.move_to import MoveTo
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class NavigateState(Enum):
+    getting_path = 1
+    navigating = 2
+
+
+class Navigate(Action):
+    """
+    Navigate using a NavMeshPath and multiple MoveTo child actions.
+    """
+
+    def __init__(self, target: Union[Dict[str, float], np.ndarray], collision_detection: CollisionDetection):
+        super().__init__()
+        # My collision detection. This will be used to instantiate MoveTo child actions.
+        self.collision_detection: CollisionDetection = collision_detection
+        # Convert the target position to a dictionary if needed.
+        if isinstance(target, np.ndarray):
+            self.target: Dict[str, float] = TDWUtils.array_to_vector3(target)
+        elif isinstance(target, dict):
+            self.target = target
+        else:
+            raise Exception(target)
+        # We are getting the path.
+        self.navigate_state: NavigateState = NavigateState.getting_path
+        # My path.
+        self.path: np.ndarray = np.zeros(shape=0)
+        # The index of the current waypoint in the path. The point at index=0 is the current position.
+        self.path_index: int = 1
+        # My MoveTo action.
+        self.move_to: Optional[MoveTo] = None
+        # The ImageFrequency. This will be used to instantiate MoveTo child actions.
+        # This will be set in get_initialization_commands()
+        self.image_frequency: ImageFrequency = ImageFrequency.once
+        # Don't try to detect obstacles while pathfinding.
+        self.original_avoid: bool = self.collision_detection.avoid
+        self.collision_detection.avoid = False
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        # Remember the ImageFrequency because we'll need it to instantiate MoveTo child actions.
+        self.image_frequency = image_frequency
+        # Get the usual initialization commands.
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        # Request a NavMeshPath. Set the path's ID to the Replicant's ID so we know whose path this is.
+        commands.append({"$type": "send_nav_mesh_path",
+                         "origin": TDWUtils.array_to_vector3(dynamic.transform.position),
+                         "destination": self.target,
+                         "id": static.replicant_id})
+        return commands
+    
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Get my path.
+        if self.navigate_state == NavigateState.getting_path:
+            # We are now navigating.
+            self.navigate_state = NavigateState.navigating
+            # Parse the output data.
+            for i in range(len(resp) - 1):
+                r_id = OutputData.get_data_type_id(resp[i])
+                # Got NavMeshPath data.
+                if r_id == "path":
+                    path = NavMeshPath(resp[i])
+                    # This is my path.
+                    if path.get_id() == static.replicant_id:
+                        # We failed to pathfind to this destination.
+                        if path.get_state() != "complete":
+                            self.status = ActionStatus.failed_to_move
+                        # This is a valid path.
+                        else:
+                            self.path = path.get_path()
+                            # Start moving.
+                            return self.set_move_to(resp=resp, static=static, dynamic=dynamic)
+        # Continue to navigate.
+        else:
+            # Get the child action's ongoing commands. This will also update the child action's status.
+            commands = self.move_to.get_ongoing_commands(resp=resp, static=static, dynamic=dynamic)
+            # The child action ended in success. Check if this is the last waypoint.
+            if self.move_to.status == ActionStatus.success:
+                # We arrived at the destination.
+                if self.path_index >= self.path.shape[0]:
+                    self.status = ActionStatus.success
+                    return commands
+                # Move to the next waypoint.
+                else:
+                    # End the current move_to action.
+                    commands.extend(self.move_to.get_end_commands(resp=resp, static=static, dynamic=dynamic,
+                                                                  image_frequency=self.image_frequency))
+                    # Start a new move_to action. Append the new child action's initialization commands.
+                    commands.extend(self.set_move_to(resp=resp, static=static, dynamic=dynamic))
+            # The action ended in failure.
+            elif self.move_to.status != ActionStatus.ongoing:
+                self.status = self.move_to.status
+            return commands
+
+    def set_move_to(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Set the action.
+        self.move_to = MoveTo(target=self.path[self.path_index],
+                              collision_detection=self.collision_detection,
+                              reset_arms=True,
+                              reset_arms_duration=0.25,
+                              arrived_at=0.1,
+                              max_walk_cycles=100,
+                              bounds_position="center",
+                              previous=None)
+        # Update the path index
+        self.path_index += 1
+        # Return the initialization commands.
+        return self.move_to.get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                        image_frequency=self.image_frequency)
+```
+
+### 3.6 Define `get_end_commands()`
+
+Unlike in `Clap`, we need to define `get_end_commands()` in `Navigate`. This is because the final `MoveTo` child action needs to stop the current walk animation:
+
+```python
+from enum import Enum
+from typing import List, Union, Dict, Optional
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.output_data import OutputData, NavMeshPath
+from tdw.replicant.actions.action import Action
+from tdw.replicant.actions.move_to import MoveTo
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class NavigateState(Enum):
+    getting_path = 1
+    navigating = 2
+
+
+class Navigate(Action):
+    """
+    Navigate using a NavMeshPath and multiple MoveTo child actions.
+    """
+
+    def __init__(self, target: Union[Dict[str, float], np.ndarray], collision_detection: CollisionDetection):
+        super().__init__()
+        # My collision detection. This will be used to instantiate MoveTo child actions.
+        self.collision_detection: CollisionDetection = collision_detection
+        # Convert the target position to a dictionary if needed.
+        if isinstance(target, np.ndarray):
+            self.target: Dict[str, float] = TDWUtils.array_to_vector3(target)
+        elif isinstance(target, dict):
+            self.target = target
+        else:
+            raise Exception(target)
+        # We are getting the path.
+        self.navigate_state: NavigateState = NavigateState.getting_path
+        # My path.
+        self.path: np.ndarray = np.zeros(shape=0)
+        # The index of the current waypoint in the path. The point at index=0 is the current position.
+        self.path_index: int = 1
+        # My MoveTo action.
+        self.move_to: Optional[MoveTo] = None
+        # The ImageFrequency. This will be used to instantiate MoveTo child actions.
+        # This will be set in get_initialization_commands()
+        self.image_frequency: ImageFrequency = ImageFrequency.once
+        # Don't try to detect obstacles while pathfinding.
+        self.original_avoid: bool = self.collision_detection.avoid
+        self.collision_detection.avoid = False
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        # Remember the ImageFrequency because we'll need it to instantiate MoveTo child actions.
+        self.image_frequency = image_frequency
+        # Get the usual initialization commands.
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        # Request a NavMeshPath. Set the path's ID to the Replicant's ID so we know whose path this is.
+        commands.append({"$type": "send_nav_mesh_path",
+                         "origin": TDWUtils.array_to_vector3(dynamic.transform.position),
+                         "destination": self.target,
+                         "id": static.replicant_id})
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Get my path.
+        if self.navigate_state == NavigateState.getting_path:
+            # We are now navigating.
+            self.navigate_state = NavigateState.navigating
+            # Parse the output data.
+            for i in range(len(resp) - 1):
+                r_id = OutputData.get_data_type_id(resp[i])
+                # Got NavMeshPath data.
+                if r_id == "path":
+                    path = NavMeshPath(resp[i])
+                    # This is my path.
+                    if path.get_id() == static.replicant_id:
+                        # We failed to pathfind to this destination.
+                        if path.get_state() != "complete":
+                            self.status = ActionStatus.failed_to_move
+                        # This is a valid path.
+                        else:
+                            self.path = path.get_path()
+                            # Start moving.
+                            return self.set_move_to(resp=resp, static=static, dynamic=dynamic)
+        # Continue to navigate.
+        else:
+            # Get the child action's ongoing commands. This will also update the child action's status.
+            commands = self.move_to.get_ongoing_commands(resp=resp, static=static, dynamic=dynamic)
+            # The child action ended in success. Check if this is the last waypoint.
+            if self.move_to.status == ActionStatus.success:
+                # We arrived at the destination.
+                if self.path_index >= self.path.shape[0]:
+                    self.status = ActionStatus.success
+                    return commands
+                # Move to the next waypoint.
+                else:
+                    # End the current move_to action.
+                    commands.extend(self.move_to.get_end_commands(resp=resp, static=static, dynamic=dynamic,
+                                                                  image_frequency=self.image_frequency))
+                    # Start a new move_to action. Append the new child action's initialization commands.
+                    commands.extend(self.set_move_to(resp=resp, static=static, dynamic=dynamic))
+            # The action ended in failure.
+            elif self.move_to.status != ActionStatus.ongoing:
+                self.status = self.move_to.status
+            return commands
+    
+    def get_end_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                         image_frequency: ImageFrequency) -> List[dict]:
+        # Reset obstacle avoidance.
+        self.collision_detection.avoid = self.original_avoid
+        # Get end commands.
+        if self.move_to is not None:
+            return self.move_to.get_end_commands(resp=resp, static=static, dynamic=dynamic,
+                                                 image_frequency=image_frequency)
+        else:
+            return super().get_end_commands(resp=resp, static=static, dynamic=dynamic,
+                                            image_frequency=image_frequency)
+
+    def set_move_to(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Set the action.
+        self.move_to = MoveTo(target=self.path[self.path_index],
+                              collision_detection=self.collision_detection,
+                              reset_arms=True,
+                              reset_arms_duration=0.25,
+                              arrived_at=0.1,
+                              max_walk_cycles=100,
+                              bounds_position="center",
+                              previous=None)
+        # Update the path index
+        self.path_index += 1
+        # Return the initialization commands.
+        return self.move_to.get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                        image_frequency=self.image_frequency)
+```
+
+### 3.7 Use `Navigate` in a controller
+
+Finally, we can use `Navigate` in a controller. This is similar to `pathfind.py` [in the previous document](navigation.md) but it uses a `Navigate` action instead of embedding the behavior in the controller itself:
+
+```python
+from enum import Enum
+from typing import List, Union, Dict, Optional
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.controller import Controller
+from tdw.output_data import OutputData, NavMeshPath
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.actions.action import Action
+from tdw.replicant.actions.move_to import MoveTo
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class NavigateState(Enum):
+    getting_path = 1
+    navigating = 2
+
+
+class Navigate(Action):
+    """
+    Navigate using a NavMeshPath and multiple MoveTo child actions.
+    """
+
+    def __init__(self, target: Union[Dict[str, float], np.ndarray], collision_detection: CollisionDetection):
+        super().__init__()
+        # My collision detection. This will be used to instantiate MoveTo child actions.
+        self.collision_detection: CollisionDetection = collision_detection
+        # Convert the target position to a dictionary if needed.
+        if isinstance(target, np.ndarray):
+            self.target: Dict[str, float] = TDWUtils.array_to_vector3(target)
+        elif isinstance(target, dict):
+            self.target = target
+        else:
+            raise Exception(target)
+        # We are getting the path.
+        self.navigate_state: NavigateState = NavigateState.getting_path
+        # My path.
+        self.path: np.ndarray = np.zeros(shape=0)
+        # The index of the current waypoint in the path. The point at index=0 is the current position.
+        self.path_index: int = 1
+        # My MoveTo action.
+        self.move_to: Optional[MoveTo] = None
+        # The ImageFrequency. This will be used to instantiate MoveTo child actions.
+        # This will be set in get_initialization_commands()
+        self.image_frequency: ImageFrequency = ImageFrequency.once
+        # Don't try to detect obstacles while pathfinding.
+        self.original_avoid: bool = self.collision_detection.avoid
+        self.collision_detection.avoid = False
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        # Remember the ImageFrequency because we'll need it to instantiate MoveTo child actions.
+        self.image_frequency = image_frequency
+        # Get the usual initialization commands.
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        # Request a NavMeshPath. Set the path's ID to the Replicant's ID so we know whose path this is.
+        commands.append({"$type": "send_nav_mesh_path",
+                         "origin": TDWUtils.array_to_vector3(dynamic.transform.position),
+                         "destination": self.target,
+                         "id": static.replicant_id})
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Get my path.
+        if self.navigate_state == NavigateState.getting_path:
+            # We are now navigating.
+            self.navigate_state = NavigateState.navigating
+            # Parse the output data.
+            for i in range(len(resp) - 1):
+                r_id = OutputData.get_data_type_id(resp[i])
+                # Got NavMeshPath data.
+                if r_id == "path":
+                    path = NavMeshPath(resp[i])
+                    # This is my path.
+                    if path.get_id() == static.replicant_id:
+                        # We failed to pathfind to this destination.
+                        if path.get_state() != "complete":
+                            self.status = ActionStatus.failed_to_move
+                        # This is a valid path.
+                        else:
+                            self.path = path.get_path()
+                            # Start moving.
+                            return self.set_move_to(resp=resp, static=static, dynamic=dynamic)
+        # Continue to navigate.
+        else:
+            # Get the child action's ongoing commands. This will also update the child action's status.
+            commands = self.move_to.get_ongoing_commands(resp=resp, static=static, dynamic=dynamic)
+            # The child action ended in success. Check if this is the last waypoint.
+            if self.move_to.status == ActionStatus.success:
+                # We arrived at the destination.
+                if self.path_index >= self.path.shape[0]:
+                    self.status = ActionStatus.success
+                    return commands
+                # Move to the next waypoint.
+                else:
+                    # End the current move_to action.
+                    commands.extend(self.move_to.get_end_commands(resp=resp, static=static, dynamic=dynamic,
+                                                                  image_frequency=self.image_frequency))
+                    # Start a new move_to action. Append the new child action's initialization commands.
+                    commands.extend(self.set_move_to(resp=resp, static=static, dynamic=dynamic))
+            # The action ended in failure.
+            elif self.move_to.status != ActionStatus.ongoing:
+                self.status = self.move_to.status
+            return commands
+
+    def get_end_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                         image_frequency: ImageFrequency) -> List[dict]:
+        # Reset obstacle avoidance.
+        self.collision_detection.avoid = self.original_avoid
+        # Get end commands.
+        if self.move_to is not None:
+            return self.move_to.get_end_commands(resp=resp, static=static, dynamic=dynamic,
+                                                 image_frequency=image_frequency)
+        else:
+            return super().get_end_commands(resp=resp, static=static, dynamic=dynamic,
+                                            image_frequency=image_frequency)
+
+    def set_move_to(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Set the action.
+        self.move_to = MoveTo(target=self.path[self.path_index],
+                              collision_detection=self.collision_detection,
+                              reset_arms=True,
+                              reset_arms_duration=0.25,
+                              arrived_at=0.1,
+                              max_walk_cycles=100,
+                              bounds_position="center",
+                              previous=None)
+        # Update the path index
+        self.path_index += 1
+        # Return the initialization commands.
+        return self.move_to.get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                        image_frequency=self.image_frequency)
+
+
+class Pathfind(Controller):
+    """
+    An example of how to utilize the NavMesh to pathfind.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        self.replicant = Replicant(position={"x": 0.1, "y": 0, "z": -5})
+        self.camera: ThirdPersonCamera = ThirdPersonCamera(position={"x": 0, "y": 13.8, "z": 0},
+                                                           look_at={"x": 0, "y": 0, "z": 0},
+                                                           avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_navigate")
+        print(f"Images will be saved to: {path}")
+        self.capture: ImageCapture = ImageCapture(avatar_ids=["a"], path=path)
+        self.add_ons.extend([self.replicant, self.camera, self.capture])
+        # Set the object IDs.
+        self.trunk_id = Controller.get_unique_id()
+        self.chair_id = Controller.get_unique_id()
+        self.table_id = Controller.get_unique_id()
+        self.rocking_horse_id = Controller.get_unique_id()
+
+    def init_scene(self):
+        # Load the scene.
+        # Bake the NavMesh.
+        # Add objects to the scene and add NavMesh obstacles.
+        self.communicate([{"$type": "load_scene",
+                           "scene_name": "ProcGenScene"},
+                          TDWUtils.create_empty_room(12, 12),
+                          {"$type": "bake_nav_mesh"},
+                          Controller.get_add_object(model_name="trunck",
+                                                    object_id=self.trunk_id,
+                                                    position={"x": 1.5, "y": 0, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.trunk_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="chair_billiani_doll",
+                                                    object_id=self.chair_id,
+                                                    position={"x": -2.25, "y": 0, "z": 2.5},
+                                                    rotation={"x": 0, "y": 20, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.chair_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="live_edge_coffee_table",
+                                                    object_id=self.table_id,
+                                                    position={"x": 0.2, "y": 0, "z": -2.25},
+                                                    rotation={"x": 0, "y": 20, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.table_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="rh10",
+                                                    object_id=self.rocking_horse_id,
+                                                    position={"x": -1, "y": 0, "z": 1.5}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.rocking_horse_id,
+                           "carve_type": "stationary"}])
+
+    def do_action(self) -> None:
+        while self.replicant.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        self.communicate([])
+
+    def end(self) -> None:
+        self.communicate({"$type": "terminate"})
+
+
+if __name__ == "__main__":
+    c = Pathfind()
+    # Initialize the scene.
+    c.init_scene()
+    # Navigate to a destination.
+    c.replicant.action = Navigate(target={"x": 0, "y": 0, "z": 4},
+                                  collision_detection=c.replicant.collision_detection)
+    c.do_action()
+    print(c.replicant.action.status)
+    # End the simulation.
+    c.end()
+```
+
+Result:
+
+![](images/navigation/pathfind.gif)
+
+### 3.8 Room for improvement?
+
+- The `Navigate` action could be improved. There could be ways to interrupt navigation or to re-path. This won't be covered in this tutorial.
+- The controller is still using a `do_action()` loop that, while simple, makes it impossible to handle multi-agent simulations. This will be revised in the next document.
+
+***
+
+**Next: [Multiple Replicants](multiple_replicants.md)**
+
+[Return to the README](../../../README.md)
+
+***
+
+Example controllers:
+
+- [minimal_custom_action.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/minimal_custom_action.py) A minimal custom action.
+- [clap.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/clap.py) Clap your hands.
+- [navigate.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/navigate.py) Navigate using a NavMeshPath and multiple MoveTo child actions.
+
+Command API:
+
+- [`bake_nav_mesh`](../../api/command_api.md#bake_nav_mesh)
+- [`make_nav_mesh_obstacle`](../../api/command_api.md#make_nav_mesh_obstacle)
+- [`send_nav_mesh_path`](../../api/command_api.md#send_nav_mesh_path)
+
+Output Data API:
+
+- [`NavMeshPath`](../../api/output_data.md#NavMeshPath)
+
+Python API:
+
+- [`Replicant`](../../python/add_ons/replicant.md)
+- [`Action`](../../python/replicant/actions/action.md)
+- [`MoveTo`](../../python/replicant/actions/move_to.md)
\ No newline at end of file
diff --git a/Documentation/lessons/replicants/grasp_drop.md b/Documentation/lessons/replicants/grasp_drop.md
new file mode 100644
index 000000000..92bca0c41
--- /dev/null
+++ b/Documentation/lessons/replicants/grasp_drop.md
@@ -0,0 +1,389 @@
+##### Replicants
+
+# Grasp and drop objects
+
+The Replicant can grasp and drop objects. Each hand can grasp exactly one object at a time.
+
+## The `grasp(target, arm)` action
+
+`grasp(target, arm)` will tell the Replicant to grasp an object. **This is a non-physics, non-motion action. The Replicant WILL NOT bend its arm towards the target.** To properly grasp, first call [`reach_for(target, arm)`](arm_articulation.md), *then* `grasp(target, arm)`.
+
+When a Replicant grasps an object, the object becomes kinematic and continuously tracks the Replicant's hand. If the object [contains other objects](../semantic_states/containment.md), those objects will also become kinematic and will be parented to the root grasped object:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+def do_action():
+    """
+    A helper function to handle the basic Replicant action loop.
+    """
+
+    while replicant.action.status == ActionStatus.ongoing:
+        c.communicate([])
+    c.communicate([])
+
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": -2.4, "y": 2, "z": 3.2},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_grasp_basket_with_object")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+object_id = Controller.get_unique_id()
+commands = [TDWUtils.create_empty_room(12, 12)]
+commands.extend(Controller.get_add_physics_object(model_name="basket_18inx18inx12iin_wicker",
+                                                  object_id=object_id,
+                                                  position={"x": -2, "y": 0, "z": 2}))
+commands.extend(Controller.get_add_physics_object(model_name="vase_02",
+                                                  object_id=Controller.get_unique_id(),
+                                                  position={"x": -2, "y": 0.1, "z": 2}))
+c.communicate(commands)
+replicant.move_to(target=object_id)
+do_action()
+replicant.reach_for(target=object_id, arm=Arm.right)
+do_action()
+replicant.grasp(target=object_id, arm=Arm.right)
+do_action()
+replicant.reach_for(target={"x": 0.1, "y": 1.1, "z": 0.6}, arm=Arm.right, absolute=False)
+do_action()
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/grasp_drop/grasp_basket_with_object.gif)
+
+### The target position
+
+The Replicant will grasp the target at a position defined by the build using affordance points and bounds positions. This is identical to the system used for `reach_for(target, arm)`, which you can read about [here](arm_articulation.md).
+
+### Grasp an object with both hands
+
+The grasped object tracks exactly one hand, as defined by the `arm` parameter. Grasping an object with *both* hands is more of an apparent motion than a real one; you can set the offhand to follow the hand holding the object by setting `reach_for(target, arm, offhand_follows=True)`:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+def do_action():
+    """
+    A helper function to handle the basic Replicant action loop.
+    """
+
+    while replicant.action.status == ActionStatus.ongoing:
+        c.communicate([])
+    c.communicate([])
+
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": -2.4, "y": 2, "z": 3.2},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_grasp_basket_with_object_both_hands")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+object_id = Controller.get_unique_id()
+commands = [TDWUtils.create_empty_room(12, 12)]
+commands.extend(Controller.get_add_physics_object(model_name="basket_18inx18inx12iin_wicker",
+                                                  object_id=object_id,
+                                                  position={"x": -2, "y": 0, "z": 2}))
+commands.extend(Controller.get_add_physics_object(model_name="vase_02",
+                                                  object_id=Controller.get_unique_id(),
+                                                  position={"x": -2, "y": 0.1, "z": 2}))
+c.communicate(commands)
+replicant.move_to(target=object_id)
+do_action()
+# Reach with both hands.
+replicant.reach_for(target=object_id, arm=[Arm.right, Arm.left])
+do_action()
+# Grasp with the right hand.
+replicant.grasp(target=object_id, arm=Arm.right)
+do_action()
+# Reach for a target position with the right hand; the left hand will follow.
+replicant.reach_for(target={"x": 0, "y": 1.1, "z": 0.4}, arm=Arm.right, absolute=False, offhand_follows=True)
+do_action()
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/grasp_drop/grasp_object_both_hands.gif)
+
+### The `angle` and `axis` parameters
+
+A grasped object will continuously rotate relative to the hand grasping it. By default, the rotation is 90 degrees along the relative pitch axis.
+
+The optional `angle` parameter controls the angle and the optional `axis` parameter controls the axis. The `axis` parameter is a string: `"pitch"`, `"yaw"`, or `"roll"`.
+
+This example sets the basket to continuously rotate 45 degrees along the roll axis:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+def do_action():
+    """
+    A helper function to handle the basic Replicant action loop.
+    """
+
+    while replicant.action.status == ActionStatus.ongoing:
+        c.communicate([])
+    c.communicate([])
+
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": -2.4, "y": 2, "z": 3.2},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_grasp_basket_with_object_yaw")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+object_id = Controller.get_unique_id()
+commands = [TDWUtils.create_empty_room(12, 12)]
+commands.extend(Controller.get_add_physics_object(model_name="basket_18inx18inx12iin_wicker",
+                                                  object_id=object_id,
+                                                  position={"x": -2, "y": 0, "z": 2}))
+commands.extend(Controller.get_add_physics_object(model_name="vase_02",
+                                                  object_id=Controller.get_unique_id(),
+                                                  position={"x": -2, "y": 0.1, "z": 2}))
+c.communicate(commands)
+replicant.move_to(target=object_id)
+do_action()
+replicant.reach_for(target=object_id, arm=Arm.right)
+do_action()
+replicant.grasp(target=object_id,
+                arm=Arm.right,
+                angle=45,
+                axis="yaw")
+do_action()
+replicant.reach_for(target={"x": 0.1, "y": 1.1, "z": 0.6}, arm=Arm.right, absolute=False)
+do_action()
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/grasp_drop/grasp_object_yaw.gif)
+
+If you set either `angle` *or* `axis` to None, the object will maintain its initial rotation even as the hand rotates:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+def do_action():
+    """
+    A helper function to handle the basic Replicant action loop.
+    """
+
+    while replicant.action.status == ActionStatus.ongoing:
+        c.communicate([])
+    c.communicate([])
+
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": -2.4, "y": 2, "z": 3.2},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_grasp_basket_with_object_no_rotate")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+object_id = Controller.get_unique_id()
+commands = [TDWUtils.create_empty_room(12, 12)]
+commands.extend(Controller.get_add_physics_object(model_name="basket_18inx18inx12iin_wicker",
+                                                  object_id=object_id,
+                                                  position={"x": -2, "y": 0, "z": 2}))
+commands.extend(Controller.get_add_physics_object(model_name="vase_02",
+                                                  object_id=Controller.get_unique_id(),
+                                                  position={"x": -2, "y": 0.1, "z": 2}))
+c.communicate(commands)
+replicant.move_to(target=object_id)
+do_action()
+replicant.reach_for(target=object_id, arm=Arm.right)
+do_action()
+replicant.grasp(target=object_id,
+                arm=Arm.right,
+                angle=None)
+do_action()
+replicant.reach_for(target={"x": 0.1, "y": 1.1, "z": 0.6}, arm=Arm.right, absolute=False)
+do_action()
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/grasp_drop/grasp_object_no_rotate.gif)
+
+### Low-level description
+
+`replicant.grasp(target, arm)` sets `replicant.action` to a [`Grasp`](../../python/replicant/actions/grasp.md) action. 
+
+In addition to [the usual `Action` initialization commands](actions.md), `Grasp` sends [`replicant_grasp_object`](../../api/command_api.md#replicant_grasp_object). An object can be grasped only if it has a non-kinematic Rigidbody that isn't held by another Replicant. If grasped, the object will become kinematic. The action additionally reads [`Containment`](../../api/output_data.md#Containment) for any objects [contained by the target object](../semantic_states/containment.md). Every contained object is parented to the grasped object via [`parent_object_to_object`](../../api/command_api.md#parent_object_to_object) and made kinematic via [`set_kinematic_state`](../../api/command_api.md#set_kinematic_state). If `angle` is not None and `axis` is not None, the action initializes object rotation via [`replicant_set_grasped_object_rotation`](../../api/command_api.md#replicant_set_grasped_object_rotation).
+
+Assuming that the object can be grasped, the `Grasp` action always succeeds (i.e. there is no physics-related failure state).
+
+A grasped object is *not* parented to its hand or connected to the object in any way. This is due to how the underlying FinalIK system updates per frame vs. how TDW updates per frame. Instead, the grasped object moves and rotates itself to the Replicant's hand per `communicate()` call.
+
+## The `drop(arm)` action
+
+`drop(arm)` will drop any object held by the hand corresponding to `arm`. The action ends in success when the dropped object stops moving or if `communicate()` has been called too many times (see below). The action ends in failure if the Replicant isn't grasping the object.
+
+In this controller, the Replicant will walk to an object, reach for the object, grasp the object, walk backwards, and drop the object. There are few noteworthy aspects about this controller:
+
+- This controller is a subclass of `Controller` and includes its own `do_action()` function. This is more verbose than all of the other examples in this document but it's much more similar to an actual use-case.
+- The Replicant will ignore collisions with the trunk. For more information, [read this](movement.md).
+- When grasping the mug, `angle=None` so that the mug's rotation remains the same.
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+class MoveGraspDrop(Controller):
+    """
+    Walk to an object, grasp it, walk away, and drop it.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        # Set the replicant and the object IDs here because we need to reference them elsewhere.
+        self.replicant = Replicant()
+        self.trunk_id = 1
+        self.mug_id = 2
+
+    def do_action(self) -> None:
+        while self.replicant.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        self.communicate([])
+
+    def run(self) -> None:
+        camera = ThirdPersonCamera(position={"x": -3.5, "y": 1.175, "z": 3},
+                                   avatar_id="a",
+                                   look_at=self.replicant.replicant_id)
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_move_grasp_drop")
+        print(f"Images will be saved to: {path}")
+        capture = ImageCapture(avatar_ids=["a"], path=path)
+        self.add_ons.extend([self.replicant, camera, capture])
+        # Create the room.
+        commands = [TDWUtils.create_empty_room(12, 12)]
+        commands.extend(Controller.get_add_physics_object(model_name="trunck",
+                                                          object_id=self.trunk_id,
+                                                          position={"x": 0, "y": 0, "z": 3},
+                                                          kinematic=True))
+        commands.extend(Controller.get_add_physics_object(model_name="coffeemug",
+                                                          object_id=self.mug_id,
+                                                          position={"x": 0, "y": 0.9888946, "z": 2.7}))
+        self.communicate(commands)
+        self.replicant.move_to(target=self.trunk_id)
+        self.do_action()
+        # Ignore the trunk.
+        self.replicant.collision_detection.exclude_objects.append(self.trunk_id)
+        self.replicant.reach_for(target=self.mug_id, arm=Arm.right)
+        self.do_action()
+        self.replicant.grasp(target=self.mug_id, arm=Arm.right, angle=None)
+        self.do_action()
+        self.replicant.move_by(-2)
+        self.do_action()
+        self.replicant.drop(arm=Arm.right)
+        self.do_action()
+        self.communicate({"$type": "terminate"})
+
+
+if __name__ == "__main__":
+    c = MoveGraspDrop()
+    c.run()
+```
+
+Result:
+
+![](images/grasp_drop/move_to_grasp_drop.gif)
+
+### The `max_num_frames` parameter
+
+Certain objects such as spheres tend to roll for a long time after being dropped. To prevent the `drop(arm)` action from continuing indefinitely, the action includes a `max_num_frames` parameter. The action will always end after this many `communicate()` calls.
+
+### Low-level description
+
+`replicant.drop(arm)` sets `replicant.action` to a [`Drop`](../../python/replicant/actions/drop.md) action. 
+
+In addition to [the usual `Action` initialization commands](actions.md), `Drop` sends [`replicant_drop_object`](../../api/command_api.md#replicant_drop_object).
+
+Per `communicate()` call, the `Drop` action checks if the object has stopped moving using [`Transforms`](../../api/output_data.md#Transforms) output data. If so, the action succeeds.
+
+***
+
+**Next: [Head rotation](head_rotation.md)**
+
+[Return to the README](../../../README.md)
+
+***
+
+Example controllers:
+
+- [grasp_basket_with_object.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/grasp_basket_with_object.py) Grasp a basket containing an object.
+- [grasp_basket_with_object_both_hands.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/grasp_basket_with_object_both_hands.py) Grasp a basket containing an object with both hands.
+- [move_grasp_drop.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/move_grasp_drop.py) Walk to an object, grasp it, walk away, and drop it.
+- [physics_test.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/physics_test.py) Pick up a table and walk around.
+
+Command API:
+
+- [`replicant_grasp_object`](../../api/command_api.md#replicant_grasp_object)
+- [`parent_object_to_object`](../../api/command_api.md#parent_object_to_object)
+- [`set_kinematic_state`](../../api/command_api.md#set_kinematic_state)
+- [`replicant_set_grasped_object_rotation`](../../api/command_api.md#replicant_set_grasped_object_rotation)
+- [`replicant_drop_object`](../../api/command_api.md#replicant_drop_object)
+
+Output Data API:
+
+- [`Containment`](../../api/output_data.md#Containment)
+- [`Transforms`](../../api/output_data.md#Transforms)
+
+Python API:
+
+- [`Replicant`](../../python/add_ons/replicant.md)
+- [`Grasp`](../../python/replicant/actions/grasp.md)
+- [`Drop`](../../python/replicant/actions/drop.md)
\ No newline at end of file
diff --git a/Documentation/lessons/replicants/head_rotation.md b/Documentation/lessons/replicants/head_rotation.md
new file mode 100644
index 000000000..cee0b59c7
--- /dev/null
+++ b/Documentation/lessons/replicants/head_rotation.md
@@ -0,0 +1,185 @@
+##### Replicants
+
+# Head rotation
+
+The Replicant can look at a target object or position, and can reset the rotation of its head. The Replicant's head moves continously over multiple `communicate()` calls. The [camera](../core_concepts/avatars.md) attached to the head always matches the rotation of the head.
+
+## The `look_at(target)` command
+
+The `look_at(target)` command will tell the Replicant to start looking at a target, which can be an x, y, z dictionary position, an x, y, z numpy array position, or an object ID:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.image_frequency import ImageFrequency
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+def do_action():
+    while replicant.action.status == ActionStatus.ongoing:
+        c.communicate([])
+
+
+c = Controller()
+replicant = Replicant(image_frequency=ImageFrequency.always)
+c.add_ons.append(replicant)
+object_id = Controller.get_unique_id()
+commands = [TDWUtils.create_empty_room(12, 12)]
+commands.extend(Controller.get_add_physics_object(model_name="basket_18inx18inx12iin_wicker",
+                                                  object_id=object_id,
+                                                  position={"x": -2, "y": 0, "z": 3}))
+c.communicate(commands)
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_look_at")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[replicant.static.avatar_id], path=path)
+c.add_ons.append(capture)
+c.communicate([])
+replicant.look_at(target=object_id)
+do_action()
+replicant.look_at(target={"x": 2, "y": 0.3, "z": 3})
+do_action()
+c.communicate({"$type": "terminate"})
+```
+
+Result (these images are captured from the Replicant's camera, as opposed to a third-person camera):
+
+![](images/head_rotation/look_at.gif)
+
+### The `duration` parameter
+
+The optional `duration` parameter controls the speed in seconds at which the head turns:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.image_frequency import ImageFrequency
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+def do_action():
+    while replicant.action.status == ActionStatus.ongoing:
+        c.communicate([])
+
+
+c = Controller()
+replicant = Replicant(image_frequency=ImageFrequency.always)
+c.add_ons.append(replicant)
+object_id = Controller.get_unique_id()
+commands = [TDWUtils.create_empty_room(12, 12)]
+commands.extend(Controller.get_add_physics_object(model_name="basket_18inx18inx12iin_wicker",
+                                                  object_id=object_id,
+                                                  position={"x": -2, "y": 0, "z": 3}))
+c.communicate(commands)
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_look_at_slow")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[replicant.static.avatar_id], path=path)
+c.add_ons.append(capture)
+c.communicate([])
+replicant.look_at(target=object_id,
+                  duration=1)
+do_action()
+replicant.look_at(target={"x": 2, "y": 0.3, "z": 3},
+                  duration=1)
+do_action()
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/head_rotation/look_at_slow.gif)
+
+### Low-level description
+
+`replicant.look_at(target)` sets `replicant.action` to a [`LookAt`](../../python/replicant/actions/look_at.md) action. 
+
+In addition to [the usual `Action` initialization commands](actions.md), `LookAt` sends either [`replicant_look_at_position`](../../api/command_api.md#replicant_look_at_position) or [`replicant_look_at_object`](../../api/command_api.md#replicant_look_at_object).
+
+The action ends when the head stops moving (as announced by the build in the output data).
+
+## The `reset_head()` command
+
+Call `reset_head()` to start turning the head back to its neutral position:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.image_frequency import ImageFrequency
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+def do_action():
+    while replicant.action.status == ActionStatus.ongoing:
+        c.communicate([])
+
+
+c = Controller()
+replicant = Replicant(image_frequency=ImageFrequency.always)
+c.add_ons.append(replicant)
+object_id = Controller.get_unique_id()
+commands = [TDWUtils.create_empty_room(12, 12)]
+commands.extend(Controller.get_add_physics_object(model_name="basket_18inx18inx12iin_wicker",
+                                                  object_id=object_id,
+                                                  position={"x": -2, "y": 0, "z": 3}))
+c.communicate(commands)
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_look_at_reset")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[replicant.static.avatar_id], path=path)
+c.add_ons.append(capture)
+c.communicate([])
+replicant.look_at(target=object_id)
+do_action()
+replicant.look_at(target={"x": 2, "y": 0.3, "z": 3})
+do_action()
+replicant.reset_head()
+do_action()
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/head_rotation/look_at_reset.gif)
+
+### The `duration` parameter
+
+The optional `duration` parameter controls the speed in seconds at which the head turns; it works exactly the same was as it does in `look_at(target)`.
+
+### Low-level description
+
+`replicant.reset_head()` sets `replicant.action` to a [`ResetHead`](../../python/replicant/actions/reset_head.md) action. 
+
+In addition to [the usual `Action` initialization commands](actions.md), `ResetHead` sends [`replicant_reset_head`](../../api/command_api.md#replicant_reset_head).
+
+The action ends when the head stops moving (as announced by the build in the output data).
+
+***
+
+**Next: [Navigation](navigation.md)**
+
+[Return to the README](../../../README.md)
+
+***
+
+Example controllers:
+
+- [look_at.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/look_at.py) Look at an object and a position, then reset the head.
+
+Command API:
+
+- [`replicant_look_at_position`](../../api/command_api.md#replicant_look_at_position)
+- [`replicant_look_at_object`](../../api/command_api.md#replicant_look_at_object)
+- [`replicant_reset_head`](../../api/command_api.md#replicant_reset_head)
+
+Python API:
+
+- [`Replicant`](../../python/add_ons/replicant.md)
+- [`LookAt`](../../python/replicant/actions/look_at.md)
+- [`ResetHead`](../../python/replicant/actions/reset_head.md)
\ No newline at end of file
diff --git a/Documentation/lessons/replicants/images/animations/animate.gif b/Documentation/lessons/replicants/images/animations/animate.gif
new file mode 100644
index 000000000..bffeea78b
Binary files /dev/null and b/Documentation/lessons/replicants/images/animations/animate.gif differ
diff --git a/Documentation/lessons/replicants/images/animations/animate_collision_detection.gif b/Documentation/lessons/replicants/images/animations/animate_collision_detection.gif
new file mode 100644
index 000000000..403b9a74d
Binary files /dev/null and b/Documentation/lessons/replicants/images/animations/animate_collision_detection.gif differ
diff --git a/Documentation/lessons/replicants/images/animations/idle_to_crouch.gif b/Documentation/lessons/replicants/images/animations/idle_to_crouch.gif
new file mode 100644
index 000000000..5cd008c82
Binary files /dev/null and b/Documentation/lessons/replicants/images/animations/idle_to_crouch.gif differ
diff --git a/Documentation/lessons/replicants/images/arm_articulation/go_to_reach_for.gif b/Documentation/lessons/replicants/images/arm_articulation/go_to_reach_for.gif
new file mode 100644
index 000000000..3eff680e7
Binary files /dev/null and b/Documentation/lessons/replicants/images/arm_articulation/go_to_reach_for.gif differ
diff --git a/Documentation/lessons/replicants/images/arm_articulation/reach_for_follow.gif b/Documentation/lessons/replicants/images/arm_articulation/reach_for_follow.gif
new file mode 100644
index 000000000..3b14991e0
Binary files /dev/null and b/Documentation/lessons/replicants/images/arm_articulation/reach_for_follow.gif differ
diff --git a/Documentation/lessons/replicants/images/arm_articulation/reach_for_move.gif b/Documentation/lessons/replicants/images/arm_articulation/reach_for_move.gif
new file mode 100644
index 000000000..83ca69db4
Binary files /dev/null and b/Documentation/lessons/replicants/images/arm_articulation/reach_for_move.gif differ
diff --git a/Documentation/lessons/replicants/images/arm_articulation/reach_for_move_no_reset.gif b/Documentation/lessons/replicants/images/arm_articulation/reach_for_move_no_reset.gif
new file mode 100644
index 000000000..6e857009d
Binary files /dev/null and b/Documentation/lessons/replicants/images/arm_articulation/reach_for_move_no_reset.gif differ
diff --git a/Documentation/lessons/replicants/images/arm_articulation/reach_for_object.gif b/Documentation/lessons/replicants/images/arm_articulation/reach_for_object.gif
new file mode 100644
index 000000000..26aa9fafd
Binary files /dev/null and b/Documentation/lessons/replicants/images/arm_articulation/reach_for_object.gif differ
diff --git a/Documentation/lessons/replicants/images/arm_articulation/reach_for_object_both_hands.gif b/Documentation/lessons/replicants/images/arm_articulation/reach_for_object_both_hands.gif
new file mode 100644
index 000000000..5447ab75e
Binary files /dev/null and b/Documentation/lessons/replicants/images/arm_articulation/reach_for_object_both_hands.gif differ
diff --git a/Documentation/lessons/replicants/images/arm_articulation/reach_for_object_slow.gif b/Documentation/lessons/replicants/images/arm_articulation/reach_for_object_slow.gif
new file mode 100644
index 000000000..43cc30381
Binary files /dev/null and b/Documentation/lessons/replicants/images/arm_articulation/reach_for_object_slow.gif differ
diff --git a/Documentation/lessons/replicants/images/arm_articulation/reach_for_position.gif b/Documentation/lessons/replicants/images/arm_articulation/reach_for_position.gif
new file mode 100644
index 000000000..dce7e528f
Binary files /dev/null and b/Documentation/lessons/replicants/images/arm_articulation/reach_for_position.gif differ
diff --git a/Documentation/lessons/replicants/images/arm_articulation/reach_for_position_both_hands.gif b/Documentation/lessons/replicants/images/arm_articulation/reach_for_position_both_hands.gif
new file mode 100644
index 000000000..0ff177e1f
Binary files /dev/null and b/Documentation/lessons/replicants/images/arm_articulation/reach_for_position_both_hands.gif differ
diff --git a/Documentation/lessons/replicants/images/arm_articulation/reach_too_far.gif b/Documentation/lessons/replicants/images/arm_articulation/reach_too_far.gif
new file mode 100644
index 000000000..60808513f
Binary files /dev/null and b/Documentation/lessons/replicants/images/arm_articulation/reach_too_far.gif differ
diff --git a/Documentation/lessons/replicants/images/arm_articulation/reset_arm.gif b/Documentation/lessons/replicants/images/arm_articulation/reset_arm.gif
new file mode 100644
index 000000000..26eb51ba9
Binary files /dev/null and b/Documentation/lessons/replicants/images/arm_articulation/reset_arm.gif differ
diff --git a/Documentation/lessons/replicants/images/crash.gif b/Documentation/lessons/replicants/images/crash.gif
new file mode 100644
index 000000000..1ed8ef9ff
Binary files /dev/null and b/Documentation/lessons/replicants/images/crash.gif differ
diff --git a/Documentation/lessons/replicants/images/custom_actions/clap.gif b/Documentation/lessons/replicants/images/custom_actions/clap.gif
new file mode 100644
index 000000000..6018d644a
Binary files /dev/null and b/Documentation/lessons/replicants/images/custom_actions/clap.gif differ
diff --git a/Documentation/lessons/replicants/images/grasp_drop/grasp_basket_with_object.gif b/Documentation/lessons/replicants/images/grasp_drop/grasp_basket_with_object.gif
new file mode 100644
index 000000000..2ffbfc0ac
Binary files /dev/null and b/Documentation/lessons/replicants/images/grasp_drop/grasp_basket_with_object.gif differ
diff --git a/Documentation/lessons/replicants/images/grasp_drop/grasp_object_both_hands.gif b/Documentation/lessons/replicants/images/grasp_drop/grasp_object_both_hands.gif
new file mode 100644
index 000000000..623b36fcb
Binary files /dev/null and b/Documentation/lessons/replicants/images/grasp_drop/grasp_object_both_hands.gif differ
diff --git a/Documentation/lessons/replicants/images/grasp_drop/grasp_object_no_rotate.gif b/Documentation/lessons/replicants/images/grasp_drop/grasp_object_no_rotate.gif
new file mode 100644
index 000000000..b998b4b6f
Binary files /dev/null and b/Documentation/lessons/replicants/images/grasp_drop/grasp_object_no_rotate.gif differ
diff --git a/Documentation/lessons/replicants/images/grasp_drop/grasp_object_yaw.gif b/Documentation/lessons/replicants/images/grasp_drop/grasp_object_yaw.gif
new file mode 100644
index 000000000..eac59883d
Binary files /dev/null and b/Documentation/lessons/replicants/images/grasp_drop/grasp_object_yaw.gif differ
diff --git a/Documentation/lessons/replicants/images/grasp_drop/move_to_grasp_drop.gif b/Documentation/lessons/replicants/images/grasp_drop/move_to_grasp_drop.gif
new file mode 100644
index 000000000..5c4512d27
Binary files /dev/null and b/Documentation/lessons/replicants/images/grasp_drop/move_to_grasp_drop.gif differ
diff --git a/Documentation/lessons/replicants/images/head_rotation/look_at.gif b/Documentation/lessons/replicants/images/head_rotation/look_at.gif
new file mode 100644
index 000000000..df6f13681
Binary files /dev/null and b/Documentation/lessons/replicants/images/head_rotation/look_at.gif differ
diff --git a/Documentation/lessons/replicants/images/head_rotation/look_at_reset.gif b/Documentation/lessons/replicants/images/head_rotation/look_at_reset.gif
new file mode 100644
index 000000000..92cac8f41
Binary files /dev/null and b/Documentation/lessons/replicants/images/head_rotation/look_at_reset.gif differ
diff --git a/Documentation/lessons/replicants/images/head_rotation/look_at_slow.gif b/Documentation/lessons/replicants/images/head_rotation/look_at_slow.gif
new file mode 100644
index 000000000..66c34f64c
Binary files /dev/null and b/Documentation/lessons/replicants/images/head_rotation/look_at_slow.gif differ
diff --git a/Documentation/lessons/replicants/images/move_by.gif b/Documentation/lessons/replicants/images/move_by.gif
new file mode 100644
index 000000000..cbe08de40
Binary files /dev/null and b/Documentation/lessons/replicants/images/move_by.gif differ
diff --git a/Documentation/lessons/replicants/images/movement/avoid_object.gif b/Documentation/lessons/replicants/images/movement/avoid_object.gif
new file mode 100644
index 000000000..99aed7055
Binary files /dev/null and b/Documentation/lessons/replicants/images/movement/avoid_object.gif differ
diff --git a/Documentation/lessons/replicants/images/movement/avoid_wall.gif b/Documentation/lessons/replicants/images/movement/avoid_wall.gif
new file mode 100644
index 000000000..cc8fc9800
Binary files /dev/null and b/Documentation/lessons/replicants/images/movement/avoid_wall.gif differ
diff --git a/Documentation/lessons/replicants/images/movement/move_to.gif b/Documentation/lessons/replicants/images/movement/move_to.gif
new file mode 100644
index 000000000..1509132b6
Binary files /dev/null and b/Documentation/lessons/replicants/images/movement/move_to.gif differ
diff --git a/Documentation/lessons/replicants/images/movement/non_kinematic.gif b/Documentation/lessons/replicants/images/movement/non_kinematic.gif
new file mode 100644
index 000000000..40567fe05
Binary files /dev/null and b/Documentation/lessons/replicants/images/movement/non_kinematic.gif differ
diff --git a/Documentation/lessons/replicants/images/multiple_replicants/carry_couch.gif b/Documentation/lessons/replicants/images/multiple_replicants/carry_couch.gif
new file mode 100644
index 000000000..de56aee61
Binary files /dev/null and b/Documentation/lessons/replicants/images/multiple_replicants/carry_couch.gif differ
diff --git a/Documentation/lessons/replicants/images/multiple_replicants/move_by.gif b/Documentation/lessons/replicants/images/multiple_replicants/move_by.gif
new file mode 100644
index 000000000..e3f82d784
Binary files /dev/null and b/Documentation/lessons/replicants/images/multiple_replicants/move_by.gif differ
diff --git a/Documentation/lessons/replicants/images/multiple_replicants/move_by_stop.gif b/Documentation/lessons/replicants/images/multiple_replicants/move_by_stop.gif
new file mode 100644
index 000000000..2d613c5f9
Binary files /dev/null and b/Documentation/lessons/replicants/images/multiple_replicants/move_by_stop.gif differ
diff --git a/Documentation/lessons/replicants/images/multiple_replicants/navigate.gif b/Documentation/lessons/replicants/images/multiple_replicants/navigate.gif
new file mode 100644
index 000000000..386f73aa6
Binary files /dev/null and b/Documentation/lessons/replicants/images/multiple_replicants/navigate.gif differ
diff --git a/Documentation/lessons/replicants/images/multiple_replicants/state_machine.gif b/Documentation/lessons/replicants/images/multiple_replicants/state_machine.gif
new file mode 100644
index 000000000..3d0a18818
Binary files /dev/null and b/Documentation/lessons/replicants/images/multiple_replicants/state_machine.gif differ
diff --git a/Documentation/lessons/replicants/images/navigation/obstacle_avoidance.gif b/Documentation/lessons/replicants/images/navigation/obstacle_avoidance.gif
new file mode 100644
index 000000000..bc97e26b0
Binary files /dev/null and b/Documentation/lessons/replicants/images/navigation/obstacle_avoidance.gif differ
diff --git a/Documentation/lessons/replicants/images/navigation/pathfind.gif b/Documentation/lessons/replicants/images/navigation/pathfind.gif
new file mode 100644
index 000000000..3d515e615
Binary files /dev/null and b/Documentation/lessons/replicants/images/navigation/pathfind.gif differ
diff --git a/Documentation/lessons/replicants/images/output_data/move_by_egocentric.gif b/Documentation/lessons/replicants/images/output_data/move_by_egocentric.gif
new file mode 100644
index 000000000..6a680902d
Binary files /dev/null and b/Documentation/lessons/replicants/images/output_data/move_by_egocentric.gif differ
diff --git a/Documentation/lessons/replicants/images/reset/reset.gif b/Documentation/lessons/replicants/images/reset/reset.gif
new file mode 100644
index 000000000..200bb3e08
Binary files /dev/null and b/Documentation/lessons/replicants/images/reset/reset.gif differ
diff --git a/Documentation/lessons/replicants/movement.md b/Documentation/lessons/replicants/movement.md
new file mode 100644
index 000000000..4648d7a8e
--- /dev/null
+++ b/Documentation/lessons/replicants/movement.md
@@ -0,0 +1,419 @@
+##### Replicants
+
+# Movement
+
+The Replicant can walk a distance, turn by an angle, walk to a target, or turn to face a target.
+
+## The `move_by(distance)` action
+
+Call [`replicant.move_by(distance)`](../../python/add_ons/replicant.md) to start to walk a target distance:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.action_status import ActionStatus
+
+c = Controller()
+replicant = Replicant()
+c.add_ons.append(replicant)
+c.communicate(TDWUtils.create_empty_room(12, 12))
+replicant.move_by(distance=2)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+If the target distance is < 0, the Replicant will walk backwards.
+
+In addition to `distance` there are other optional parameters; [read the API documentation for more information](../../python/add_ons/replicant.md).
+
+### Success and failure
+
+The `move_by(distance)` action succeeds (i.e. `replicant.action.status == ActionStatus.success`) if the Replicant moves the target distance minus a small offset (see the `arrived_at` optional parameter).
+
+The action can end in failure due to a [collision](collision_detection.md) or because the Replicant took too long to arrive (see the `max_walk_cycles` optional parameter).
+
+### Why there is a pause at the start of movement
+
+The Replicant's walk animation is stored as a [humanoid animation asset bundle](../non_physics_humanoids/overview.md). The first time you call `move_by()` or `move_to()`, the animation needs to be downloaded and loaded into memory, hence the delay at the start of the action. Like all asset bundles, this is a one-time requirement. If you call `move_by()` and `move_to()` more than once, subsequent calls will begin immediately.
+
+## The `turn_by(angle)` action
+
+Call [`replicant.turn_by(angle)`](../../python/add_ons/replicant.md) to turn the Replicant by an angle in degrees.
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.action_status import ActionStatus
+
+c = Controller()
+replicant = Replicant()
+c.add_ons.append(replicant)
+c.communicate(TDWUtils.create_empty_room(12, 12))
+replicant.turn_by(angle=40)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+Unlike `move_by(distance)`, `turn_by(angle)` is not an animated motion, meaning that the Replicant will immediately rotate by the angle. There are no optional parameters and the action always ends in success.
+
+## The `turn_to(target)` action
+
+Call [`replicant.turn_to(target)`](../../python/add_ons/replicant.md) to turn the Replicant to face a target position or object. `target` can be an object ID, a position as an x, y, z dictionary, or a position as a 3-element numpy array:
+
+```python
+import numpy as np
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.action_status import ActionStatus
+
+c = Controller()
+replicant = Replicant()
+c.add_ons.append(replicant)
+object_id = Controller.get_unique_id()
+c.communicate([TDWUtils.create_empty_room(12, 12),
+               Controller.get_add_object(model_name="rh10",
+                                         object_id=object_id,
+                                         position={"x": 2, "y": 0, "z": 2.3})])
+# Turn to a target position.
+replicant.turn_to(target={"x": -1, "y": 0, "z": 0})
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+# Turn to a target position.
+replicant.turn_to(target=np.array([1, 0, 0]))
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+# Turn to a target object.
+replicant.turn_to(target=object_id)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+Like `turn_by(angle)`, this is not an animated motion, there are no optional parameters, and the action always succeeds.
+
+## The `move_to(target)` action
+
+Call [`replicant.move_to(target)`](../../python/add_ons/replicant.md) to tell the Replicant to walk to a target position or object.
+
+`move_to(target)` combines a `turn_to(target)` action with a `move_by(distance)` action. It has all of the same optional parameters and collision detection rules as `move_by(distance)` plus a few more; [read the API documentation for more information](../../python/add_ons/replicant.md).
+
+Like `turn_to(target)`, the `target` can be an object ID, a position as an x, y, z dictionary, or a position as a 3-element numpy array.
+
+This example tells the Replicant to move to a target position behind it:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+from tdw.replicant.action_status import ActionStatus
+
+c = Controller()
+replicant = Replicant(position={"x": 0, "y": 0, "z": 2})
+camera = ThirdPersonCamera(position={"x": 2, "y": 1.6, "z": 1},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_move_to")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=["a"],
+                       path=path)
+c.add_ons.extend([replicant, camera, capture])
+# Create the scene.
+c.communicate(TDWUtils.create_empty_room(12, 12))
+# Start walking.
+replicant.move_to(target={"x": 0, "y": 0, "z": -3})
+# Continue walking until the action ends.
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/movement/move_to.gif)
+
+## Physics and collision detection
+
+*For more information regarding collision detection, [read this.](collision_detection.md)*
+
+### Turn actions
+
+The `turn_by(angle)` and `turn_to(angle)` actions always succeed and ignore `replicant.collision_detection`.
+
+### Obstacle avoidance
+
+By default, `move_by(distance)` and `move_to(target)` will try to *avoid* obstacles *before* colliding with them.
+
+In this example, the Replicant will start to walk to a wall and stop before it hits it:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+from tdw.replicant.action_status import ActionStatus
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 2, "y": 1.6, "z": 1},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_avoid_wall")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=["a"], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate(TDWUtils.create_empty_room(8, 8))
+replicant.move_by(10)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+print(replicant.action.status)
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/movement/avoid_wall.gif)
+
+Output:
+
+```
+ActionStatus.detected_obstacle
+```
+
+This also works with any objects in the Replicant's path:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+from tdw.replicant.action_status import ActionStatus
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 2, "y": 1.6, "z": 1},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_avoid_object")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=["a"], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate([TDWUtils.create_empty_room(8, 8),
+               Controller.get_add_object(model_name="rh10",
+                                         object_id=Controller.get_unique_id(),
+                                         position={"x": 0, "y": 0, "z": 2})])
+replicant.move_by(10)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+print(replicant.action.status)
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/movement/avoid_object.gif)
+
+Output:
+
+```
+ActionStatus.detected_obstacle
+```
+
+**To disable obstacle avoidance, set `replicant.collision_detection.avoid = False`.** If you do this:
+
+- The Replicant will walk until it collides with non-kinematic objects.
+- The Replicant will walk through kinematic objects.
+- The Replicant will walk through walls.
+
+### Collisions with non-kinematic objects
+
+Even if you set `replicant.collision_detection.avoid = False`, a Replicant will stop walking when it *collides* with a non-kinematic object. There are two ways to toggle this off:
+
+1. **Set `replicant.collision_detection.objects = False` to ignore *all* collisions with objects.** The Replicant will still physically interact with non-kinematic objects.
+2. **Append an object ID to the `replicant.collision_detection.exclude_objects` list.** The Replicant will ignore collisions with this specific object (and will still physically interact with it).
+
+In this example, we've disabled obstacle avoidance and object avoidance; notice how the Replicant collides with the sofa and pushes it out of the way:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+c = Controller()
+camera = ThirdPersonCamera(position={"x": -0.5, "y": 1.175, "z": 6},
+                           look_at={"x": 0.5, "y": 1, "z": 0},
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_crash")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=["a"], path=path)
+replicant = Replicant(position={"x": 0, "y": 0, "z": 4})
+# Don't try to avoid obstacles.
+replicant.collision_detection.avoid = False
+replicant.collision_detection.objects = False
+# Append the add-ons.
+c.add_ons.extend([replicant, camera, capture])
+# Create a scene. Add a sofa. The sofa will be implausibly light.
+commands = [TDWUtils.create_empty_room(12, 20),
+            c.get_add_object(model_name="arflex_strips_sofa",
+                             position={"x": 0, "y": 0, "z": 0},
+                             object_id=Controller.get_unique_id())]
+# Add the target object.
+ball_id = Controller.get_unique_id()
+commands.extend(c.get_add_physics_object(model_name="prim_sphere",
+                                         object_id=ball_id,
+                                         position={"x": 0, "y": 0, "z": -4.0},
+                                         scale_factor={"x": 0.2, "y": 0.2, "z": 0.2},
+                                         kinematic=True,
+                                         gravity=False,
+                                         library="models_special.json"))
+# Send the commands.
+c.communicate(commands)
+replicant.move_to(target=ball_id)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+![](images/crash.gif)
+
+### Collisions with kinematic objects and walls
+
+Because the Replicant's Rigidbody is kinematic (non-physics), **the Replicant won't collide with other kinematic objects.** Assuming that `collision_detection.avoid = False` and `collision_detection.objects = False`, the Replicant will walk through kinematic objects without interacting with them.
+
+Walls are environment objects that don't have Rigidbodies. If `collision_detection.avoid = False`, the Replicant will walk through walls without interacting with them.
+
+In this example, the Replicant will walk through an object and a wall:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+from tdw.replicant.action_status import ActionStatus
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 2, "y": 1.6, "z": 1},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_non_kinematic")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=["a"],
+                       path=path)
+c.add_ons.extend([replicant, camera, capture])
+# Create the scene.
+commands = [TDWUtils.create_empty_room(12, 12)]
+# Add a kinematic object to the scene.
+commands.extend(Controller.get_add_physics_object(model_name="trunck",
+                                                  object_id=Controller.get_unique_id(),
+                                                  position={"x": 0, "y": 0, "z": 3},
+                                                  kinematic=True))
+c.communicate(commands)
+# Look at the Replicant.
+camera.look_at(replicant.replicant_id)
+# Disable obstacle avoidance.
+replicant.collision_detection.avoid = False
+# Disable checks for object collisions.
+replicant.collision_detection.objects = False
+# Start walking.
+replicant.move_by(20)
+# Continue walking until the action ends.
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/movement/non_kinematic.gif)
+
+### Collision detection and previous actions
+
+By default, the Replicant will fail a move action depending on the result of the *previous* action. If the previous action was a move action, and it was in the same direction, and it ended in a collision, the current move action will immediately fail (because it's assumed that it would also end in a collision).
+
+To override this behavior, set `replicant.collision_detection.previous_was_same = False`.
+
+## Low-level description
+
+### Turning
+
+`replicant.turn_by(angle)` sets `replicant.action` to a [`TurnBy`](../../python/replicant/actions/turn_by.md) action. `replicant.turn_to(target)` sets `replicant.action` to a [`TurnTo`](../../python/replicant/actions/turn_to.md).
+
+In addition to [the usual `Action` initialization commands](actions.md), `TurnBy` sends [`rotate_object_by`](../../api/command_api.md#rotate_object_by), while `TurnTo` sends [`object_look_at_position`](../../api/command_api.md#object_look_at_position).
+
+### Moving
+
+`replicant.move_by(distance)` sets `replicant.action` to a [`MoveBy`](../../python/replicant/actions/move_by.md) action. `replicant.move_to(target)` sets `replicant.action` to a [`MoveTo`](../../python/replicant/actions/move_to.md) action.
+
+In addition to [the usual `Action` initialization commands](actions.md),  `MoveBy` and `MoveTo` send [`add_humanoid_animation`](../../api/command_api.md#add_humanoid_animation) and [`play_humanoid_animation`](../../api/command_api.md#play_humanoid_animation) for the walk animation. Every `communicate()` call, these actions check for collisions and the distance traversed to determine if the action has ended. The action checks if the walk animation is done by checking `replicant.dynamic.output_action_status` (which will indicate that the build has signaled that an animation ended), and replay the walk animation as needed.
+
+Due to certain low-level features in the Replicant's implementation, collision detection can be glitchy while the Replicant is walking. On every `communicate()` call, the `MoveBy` and `MoveTo` actions will send [`replicant_resolve_collider_intersections`](../../api/command_api.md#replicant_resolve_collider_intersections); this command will attempt to push forward any objects intersecting with the Replicant.
+
+Obstacle avoidance is achieved via overlap shapes. Per `communicate()` call, the action sends [`send_overlap_box`](../../api/command_api.md#send_overlap_box), which returns [`Overlap`](../../api/output_data.md#Overlap) data. The overlap box is cast in front or behind the Replicant, depending on whether it is walking forwards or backwards. If the `Overlap` data includes walls or object IDs, the Replicant stops walking.
+
+In addition to [the usual `Action` end commands](actions.md), `MoveBy` and `MoveTo` send [`stop_humanoid_animation`](../../api/command_api.md#stop_humanoid_animation).
+
+***
+
+**Next: [Animations](animations.md)**
+
+[Return to the README](../../../README.md)
+
+***
+
+Example controllers:
+
+- [move_by.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/move_by.py) Tell the Replicant to walk a target distance.
+- [move_to.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/move_to.py) Tell the Replicant to walk to a target position.
+- [crash.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/crash.py) Collide with an obstacle.
+- [collision_detection_tests.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/collision_detection_tests.py) Tests for collision detection while walking.
+- [non_kinematic.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/non_kinematic.py) Walk through an object and a wall.
+
+Command API:
+
+- [`rotate_object_by`](../../api/command_api.md#rotate_object_by)
+- [`object_look_at_position`](../../api/command_api.md#object_look_at_position)
+- [`add_humanoid_animation`](../../api/command_api.md#add_humanoid_animation)
+- [`play_humanoid_animation`](../../api/command_api.md#play_humanoid_animation)
+- [`replicant_resolve_collider_intersections`](../../api/command_api.md#replicant_resolve_collider_intersections)
+- [`send_overlap_box`](../../api/command_api.md#send_overlap_box)
+- [`stop_humanoid_animation`](../../api/command_api.md#stop_humanoid_animation)
+
+Output Data API:
+
+- [`Overlap`](../../api/output_data.md#Overlap)
+
+Python API:
+
+- [`Replicant`](../../python/add_ons/replicant.md)
+- [`TurnBy`](../../python/replicant/actions/turn_by.md)
+- [`TurnTo`](../../python/replicant/actions/turn_to.md)
+- [`MoveBy`](../../python/replicant/actions/move_by.md)
+- [`MoveTo`](../../python/replicant/actions/move_to.md)
\ No newline at end of file
diff --git a/Documentation/lessons/replicants/multiple_replicants.md b/Documentation/lessons/replicants/multiple_replicants.md
new file mode 100644
index 000000000..958141550
--- /dev/null
+++ b/Documentation/lessons/replicants/multiple_replicants.md
@@ -0,0 +1,806 @@
+##### Replicants
+
+# Multi-Replicant simulations
+
+*For more information regarding multi-agent simulations, [read this](../multi_agent/overview.md).*
+
+So far, the "simple action loop" of calling `c.communicate([])` until an action ends has been sufficient. This is because all of our controllers have assumed that there is only one agent in the scene, that the agent is a Replicant, and that we don't need to interrupt an action.
+
+Actions and Replicants are designed for multi-agent simulations in which behavior can be interrupted. The "simple action loop" is useful when showcasing *other* aspects of the Replicant but it's *not necessary*.
+
+## Example A: End when action(s) end
+
+In this example, two replicants will walk forward. We've edited the "simple action loop" to check the states of *both* Replicants' actions. The loop ends when either of the Replicants end. In this case, we know that, because one Replicant needs to walk further than the other, that only the Replicant with the shorter distance will finish its action:
+
+```python
+from typing import Dict
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+class MultiReplicant(Controller):
+    def __init__(self, replicants: Dict[int, Dict[str, float]],
+                 port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        # Remember the Replicants. Key = ID. Value = Replicant.
+        self.replicants: Dict[int, Replicant] = dict()
+        for replicant_id in replicants:
+            # Create a Replicant add-on. Set its ID and position.
+            replicant = Replicant(replicant_id=replicant_id,
+                                  position=replicants[replicant_id])
+            # Append the add-on.
+            self.add_ons.append(replicant)
+            self.replicants[replicant_id] = replicant
+        # Add a camera and enable image capture.
+        # These aren't field (they don't start with self. ) because we don't need to reference them again.
+        camera = ThirdPersonCamera(position={"x": -2.4, "y": 6, "z": 3.2},
+                                   look_at={"x": 0, "y": 1, "z": 0},
+                                   avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("multi_replicant")
+        print(f"Images will be saved to: {path}")
+        capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+        self.add_ons.extend([camera, capture])
+        # Create an empty scene.
+        self.communicate(TDWUtils.create_empty_room(12, 12))
+
+    def do_actions(self) -> None:
+        # Loop.
+        done = False
+        while not done:
+            for replicant_id in self.replicants:
+                # One of the actions ended. Stop.
+                if self.replicants[replicant_id].action.status != ActionStatus.ongoing:
+                    done = True
+            # Continue the loop.
+            if not done:
+                self.communicate([])
+        self.communicate([])
+
+
+if __name__ == "__main__":
+    c = MultiReplicant(replicants={0: {"x": 1.5, "y": 0, "z": -1},
+                                   1: {"x": -1.5, "y": 0, "z": -1}})
+    c.replicants[0].move_by(distance=2)
+    c.replicants[1].move_by(distance=4)
+    c.do_actions()
+    print(c.replicants[0].action.status)
+    print(c.replicants[1].action.status)
+    c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/multiple_replicants/move_by_stop.gif)
+
+Output:
+
+```
+ActionStatus.success
+ActionStatus.ongoing
+```
+
+We can easily revise the loop to end when *both* Replicants end their actions:
+
+```python
+from typing import Dict
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+class MultiReplicant(Controller):
+    def __init__(self, replicants: Dict[int, Dict[str, float]],
+                 port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        # Remember the Replicants. Key = ID. Value = Replicant.
+        self.replicants: Dict[int, Replicant] = dict()
+        for replicant_id in replicants:
+            # Create a Replicant add-on. Set its ID and position.
+            replicant = Replicant(replicant_id=replicant_id,
+                                  position=replicants[replicant_id])
+            # Append the add-on.
+            self.add_ons.append(replicant)
+            self.replicants[replicant_id] = replicant
+        # Add a camera and enable image capture.
+        # These aren't field (they don't start with self. ) because we don't need to reference them again.
+        camera = ThirdPersonCamera(position={"x": -2.4, "y": 6, "z": 3.2},
+                                   look_at={"x": 0, "y": 1, "z": 0},
+                                   avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("multi_replicant")
+        print(f"Images will be saved to: {path}")
+        capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+        self.add_ons.extend([camera, capture])
+        # Create an empty scene.
+        self.communicate(TDWUtils.create_empty_room(12, 12))
+
+    def do_actions(self) -> None:
+        # Loop.
+        done = False
+        while not done:
+            done = True
+            for replicant_id in self.replicants:
+                # One of the actions is ongoing. We're not done.
+                if self.replicants[replicant_id].action.status == ActionStatus.ongoing:
+                    done = False
+            # Continue the loop.
+            if not done:
+                self.communicate([])
+        self.communicate([])
+
+
+if __name__ == "__main__":
+    c = MultiReplicant(replicants={0: {"x": 1.5, "y": 0, "z": -1},
+                                   1: {"x": -1.5, "y": 0, "z": -1}})
+    c.replicants[0].move_by(distance=2)
+    c.replicants[1].move_by(distance=4)
+    c.do_actions()
+    print(c.replicants[0].action.status)
+    print(c.replicants[1].action.status)
+    c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/multiple_replicants/move_by.gif)
+
+Output:
+
+```
+ActionStatus.success
+ActionStatus.success
+```
+
+## Example B: State machines
+
+In this example,  there are two replicants:
+
+1. `replicant_0` will [play a dance animation](animations.md) in a loop. We'll control the behavior of `replicant_0` by simply checking if the animation is done.
+2. `replicant_1` will try to walk to the other side of the room. If it gets too close to `replicant_0`, it will back up and try again. We'll control the behavior of `replicant_1` with a state machine, similar to how we handled [custom actions in the previous document](custom_actions.md). Before calling `communicate([])` we'll evaluate the Replicant's state and its action status and decide what it should do next:
+
+```python
+from enum import Enum
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+class Replicant1State(Enum):
+    moving_to = 1
+    moving_backward = 2
+    turning = 3
+    moving_forward = 4
+
+
+class MultiReplicantStateMachine(Controller):
+    """
+    Control the actions of multiple Replicants using a state machine.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        self.replicant_0 = Replicant(replicant_id=0,
+                                     position={"x": 0, "y": 0, "z": 0})
+        self.replicant_1 = Replicant(replicant_id=1,
+                                     position={"x": 0, "y": 0, "z": -2})
+        camera = ThirdPersonCamera(position={"x": -4, "y": 3, "z": 3.2},
+                                   look_at=self.replicant_1.replicant_id,
+                                   avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("multi_replicant_state_machine")
+        print(f"Images will be saved to: {path}")
+        capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+        self.add_ons.extend([self.replicant_0, self.replicant_1, camera, capture])
+        # Create an empty scene.
+        self.communicate(TDWUtils.create_empty_room(12, 12))
+        # Start animating.
+        self.replicant_0_animation: str = "dancing_3"
+        self.replicant_0.animate(animation=self.replicant_0_animation)
+        # Set Replicant 1's target to be 6 meters in front of it.
+        self.replicant_1_target = {"x": self.replicant_1.initial_position["x"],
+                                   "y": self.replicant_1.initial_position["y"],
+                                   "z": self.replicant_1.initial_position["z"] + 6}
+        # Start moving.
+        self.replicant_1.move_to(target=self.replicant_1_target)
+        self.replicant_1_state = Replicant1State.moving_to
+
+    def run(self) -> None:
+        done = False
+        while not done:
+            # The animation ended. Play it again.
+            if self.replicant_0.action.status != ActionStatus.ongoing:
+                self.replicant_0.animate(animation=self.replicant_0_animation)
+
+            # Handle Replicant 1's state machine.
+            # Check if the action succeeded.
+            if self.replicant_1.action.status == ActionStatus.success:
+                # The Replicant finished moving forward. We're done.
+                if self.replicant_1_state == Replicant1State.moving_to:
+                    done = True
+                # The Replicant finished moving backwards. Start turning.
+                elif self.replicant_1_state == Replicant1State.moving_backward:
+                    self.replicant_1_state = Replicant1State.turning
+                    self.replicant_1.turn_by(35)
+                # The Replicant finished turning. Start moving forward.
+                elif self.replicant_1_state == Replicant1State.turning:
+                    self.replicant_1.move_by(distance=0.75)
+                    self.replicant_1_state = Replicant1State.moving_forward
+                # The Replicant finished moving forward. Start moving to the target again.
+                elif self.replicant_1_state == Replicant1State.moving_forward:
+                    self.replicant_1.move_to(target=self.replicant_1_target)
+                    self.replicant_1_state = Replicant1State.moving_to
+            # The action ended in failure.
+            elif self.replicant_1.action.status != ActionStatus.ongoing:
+                # Start backing up.
+                self.replicant_1_state = Replicant1State.moving_backward
+                self.replicant_1.move_by(distance=-0.5)
+
+            # Continue the loop.
+            self.communicate([])
+        self.communicate({"$type": "terminate"})
+
+
+if __name__ == "__main__":
+    c = MultiReplicantStateMachine()
+    c.run()
+```
+
+Result:
+
+![](images/multiple_replicants/state_machine.gif)
+
+## Example C: Multi-Replicant navigation
+
+In this example, we're going to use the `Navigate` custom action that we wrote in the [previous document](navigation.md), but for multiple Replicants. We'll also take the simple multi-state system in Example B and apply it here.
+
+If we handled all of the backing-up logic in the controller like we did in Example B, this example's code could get very complicated, especially if there were more than two Replicants. To solve this, we've moved a lot of code into a custom `Navigator` class, which is a subclass of `Replicant`. We've also introduced *two* state machines: One of them is handling the `Navigate` action and one of them is handling the `Navigator` Replicant. The `Navigate` class will evaluate its own `NavigatorState` within `on_send(resp)` to decide what to do next:
+
+```python
+from enum import Enum
+from typing import List, Union, Dict, Optional
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.controller import Controller
+from tdw.output_data import OutputData, NavMeshPath
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.actions.action import Action
+from tdw.replicant.actions.move_to import MoveTo
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class NavigateState(Enum):
+    getting_path = 1
+    navigating = 2
+
+
+class Navigate(Action):
+    """
+    Navigate using a NavMeshPath and multiple MoveTo child actions.
+    """
+
+    def __init__(self, target: Union[Dict[str, float], np.ndarray], collision_detection: CollisionDetection):
+        super().__init__()
+        # My collision detection. This will be used to instantiate MoveTo child actions.
+        self.collision_detection: CollisionDetection = collision_detection
+        # Convert the target position to a dictionary if needed.
+        if isinstance(target, np.ndarray):
+            self.target: Dict[str, float] = TDWUtils.array_to_vector3(target)
+        elif isinstance(target, dict):
+            self.target = target
+        else:
+            raise Exception(target)
+        # We are getting the path.
+        self.navigate_state: NavigateState = NavigateState.getting_path
+        # My path.
+        self.path: np.ndarray = np.zeros(shape=0)
+        # The index of the current waypoint in the path. The point at index=0 is the current position.
+        self.path_index: int = 1
+        # My MoveTo action.
+        self.move_to: Optional[MoveTo] = None
+        # The ImageFrequency. This will be used to instantiate MoveTo child actions.
+        # This will be set in get_initialization_commands()
+        self.image_frequency: ImageFrequency = ImageFrequency.once
+        # Don't try to detect obstacles while pathfinding.
+        self.original_avoid: bool = self.collision_detection.avoid
+        self.collision_detection.avoid = False
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        # Remember the ImageFrequency because we'll need it to instantiate MoveTo child actions.
+        self.image_frequency = image_frequency
+        # Get the usual initialization commands.
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        # Request a NavMeshPath. Set the path's ID to the Replicant's ID so we know whose path this is.
+        commands.append({"$type": "send_nav_mesh_path",
+                         "origin": TDWUtils.array_to_vector3(dynamic.transform.position),
+                         "destination": self.target,
+                         "id": static.replicant_id})
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Get my path.
+        if self.navigate_state == NavigateState.getting_path:
+            # We are now navigating.
+            self.navigate_state = NavigateState.navigating
+            # Parse the output data.
+            for i in range(len(resp) - 1):
+                r_id = OutputData.get_data_type_id(resp[i])
+                # Got NavMeshPath data.
+                if r_id == "path":
+                    path = NavMeshPath(resp[i])
+                    # This is my path.
+                    if path.get_id() == static.replicant_id:
+                        # We failed to pathfind to this destination.
+                        if path.get_state() != "complete":
+                            self.status = ActionStatus.failed_to_move
+                            return []
+                        # This is a valid path.
+                        else:
+                            self.path = path.get_path()
+                            # Start moving.
+                            return self.set_move_to(resp=resp, static=static, dynamic=dynamic)
+        # Continue to navigate.
+        else:
+            # Get the child action's ongoing commands. This will also update the child action's status.
+            commands = self.move_to.get_ongoing_commands(resp=resp, static=static, dynamic=dynamic)
+            # The child action ended in success. Check if this is the last waypoint.
+            if self.move_to.status == ActionStatus.success:
+                # We arrived at the destination.
+                if self.path_index >= self.path.shape[0]:
+                    self.status = ActionStatus.success
+                    return commands
+                # Move to the next waypoint.
+                else:
+                    # End the current move_to action.
+                    commands.extend(self.move_to.get_end_commands(resp=resp, static=static, dynamic=dynamic,
+                                                                  image_frequency=self.image_frequency))
+                    # Start a new move_to action. Append the new child action's initialization commands.
+                    commands.extend(self.set_move_to(resp=resp, static=static, dynamic=dynamic))
+            # The action ended in failure.
+            elif self.move_to.status != ActionStatus.ongoing:
+                self.status = self.move_to.status
+            return commands
+
+    def get_end_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                         image_frequency: ImageFrequency) -> List[dict]:
+        # Reset obstacle avoidance.
+        self.collision_detection.avoid = self.original_avoid
+        # Get end commands.
+        if self.move_to is not None:
+            return self.move_to.get_end_commands(resp=resp, static=static, dynamic=dynamic,
+                                                 image_frequency=image_frequency)
+        else:
+            return super().get_end_commands(resp=resp, static=static, dynamic=dynamic,
+                                            image_frequency=image_frequency)
+
+    def set_move_to(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Set the action.
+        self.move_to = MoveTo(target=self.path[self.path_index],
+                              collision_detection=self.collision_detection,
+                              reset_arms=True,
+                              reset_arms_duration=0.25,
+                              arrived_at=0.1,
+                              max_walk_cycles=100,
+                              bounds_position="center",
+                              previous=None)
+        # Update the path index
+        self.path_index += 1
+        # Return the initialization commands.
+        return self.move_to.get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                        image_frequency=self.image_frequency)
+
+
+class NavigatorState(Enum):
+    navigating = 1
+    moving_backward = 2
+    turning = 3
+    moving_forward = 4
+    done = 5
+
+
+class Navigator(Replicant):
+    """
+    A Replicant trying to navigate to a target position.
+    """
+
+    def __init__(self, target: Dict[str, float], replicant_id: int = 0, position: Dict[str, float] = None, rotation: Dict[str, float] = None):
+        super().__init__(replicant_id=replicant_id, position=position, rotation=rotation)
+        # Set the initial target, the initial state, and the action.
+        self.target = target
+        self.navigator_state: NavigatorState = NavigatorState.navigating
+        self.navigate(target=self.target)
+        # We don't care if the previous action ended in a collision.
+        self.collision_detection.previous_was_same = False
+
+    def navigate(self, target: Union[Dict[str, float], np.ndarray]) -> None:
+        self.action = Navigate(target=target, collision_detection=self.collision_detection)
+
+    def on_send(self, resp: List[bytes]) -> None:
+        # Process the Replicant's action, commands, etc. as normal.
+        super().on_send(resp=resp)
+        # The action is ongoing. Do nothing extra.
+        if self.action.status == ActionStatus.ongoing:
+            return
+        # The action succeeded. Check if we're done navigating.
+        elif self.action.status == ActionStatus.success:
+            # We're done!
+            if self.navigator_state == NavigatorState.navigating:
+                self.navigator_state = NavigatorState.done
+            # The Replicant finished moving backwards. Start turning.
+            elif self.navigator_state == NavigatorState.moving_backward:
+                self.navigator_state = NavigatorState.turning
+                self.turn_by(35)
+            # The Replicant finished turning. Start moving forward.
+            elif self.navigator_state == NavigatorState.turning:
+                self.move_by(distance=0.75)
+                self.navigator_state = NavigatorState.moving_forward
+            # The Replicant finished moving forward. Start navigating to the target again.
+            elif self.navigator_state == NavigatorState.moving_forward:
+                self.navigate(target=self.target)
+                self.navigator_state = NavigatorState.navigating
+        # The action ended in failure. Start backing up.
+        else:
+            self.navigator_state = NavigatorState.moving_backward
+            self.move_by(distance=-0.5)
+
+
+class MultiNavigate(Controller):
+    """
+    An example of how to utilize the NavMesh to pathfind in a multi-Replicant simulation.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        self.replicant_0 = Navigator(replicant_id=0,
+                                     position={"x": 0, "y": 0, "z": -5},
+                                     target={"x": 0, "y": 0, "z": 4})
+        self.replicant_1 = Navigator(replicant_id=1,
+                                     position={"x": 0, "y": 0, "z": 5},
+                                     target={"x": 0, "y": 0, "z": -4})
+        self.camera: ThirdPersonCamera = ThirdPersonCamera(position={"x": 0, "y": 13.8, "z": 0},
+                                                           look_at={"x": 0, "y": 0, "z": 0},
+                                                           avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("multi_replicant_navigate")
+        print(f"Images will be saved to: {path}")
+        self.capture: ImageCapture = ImageCapture(avatar_ids=["a"], path=path)
+        self.add_ons.extend([self.replicant_0, self.replicant_1, self.camera, self.capture])
+        # Set the object IDs.
+        self.trunk_id = Controller.get_unique_id()
+        self.chair_id = Controller.get_unique_id()
+        self.table_id = Controller.get_unique_id()
+        self.rocking_horse_id = Controller.get_unique_id()
+
+    def init_scene(self):
+        self.communicate([{"$type": "load_scene",
+                           "scene_name": "ProcGenScene"},
+                          TDWUtils.create_empty_room(12, 12),
+                          {"$type": "bake_nav_mesh"},
+                          Controller.get_add_object(model_name="trunck",
+                                                    object_id=self.trunk_id,
+                                                    position={"x": 1.5, "y": 0, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.trunk_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="chair_billiani_doll",
+                                                    object_id=self.chair_id,
+                                                    position={"x": -2.25, "y": 0, "z": 2.5},
+                                                    rotation={"x": 0, "y": 20, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.chair_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="live_edge_coffee_table",
+                                                    object_id=self.table_id,
+                                                    position={"x": 0.2, "y": 0, "z": -2.25},
+                                                    rotation={"x": 0, "y": 20, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.table_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="rh10",
+                                                    object_id=self.rocking_horse_id,
+                                                    position={"x": -1, "y": 0, "z": 1.5}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.rocking_horse_id,
+                           "carve_type": "stationary"}])
+
+    def run(self) -> None:
+        self.init_scene()
+        # Wait until both Replicants are done navigating.
+        while self.replicant_0.navigator_state != NavigatorState.done or self.replicant_1.navigator_state != NavigatorState.done:
+            self.communicate([])
+        self.communicate({"$type": "terminate"})
+
+
+if __name__ == "__main__":
+    c = MultiNavigate()
+    c.run()
+```
+
+Result:
+
+![](images/multiple_replicants/navigate.gif)
+
+**Remember: the *specific* the way this behavior has been implemented is optional.** You could do the exact same thing in any of the following ways:
+
+- Handle everything in the controller, without custom actions or Replicant subclasses.
+- Handle everything in the controller without a `Replicant` add-on, using only low-level commands.
+- Handle `Navigate` as a custom action but don't use a custom Replicant subclass.
+- Handle everything in a Replicant subclass within `on_send`, without a custom `Navigate` action.
+- Create both a `Navigate` action and a `Yield` action (the latter of which encapsulates the `moving_backward`, `turning`, and `moving_forward` states).
+
+## Example D: Carry a Couch
+
+In this example, two Replicants will move to opposite ends of a couch, lift it, carry it, and drop it. As we've already learned, [only one Replicant can grasp an object at a time](grasp_drop.md), so technically the Replicants are moving synchronously rather than carrying the object together.
+
+Because we want to coordinate most of the movements of these Replicants, we're going to use a NavMesh but we don't need or want a `Navigate` action; we'll handle the multiple `move_to(target)` actions manually.
+
+While navigating, the controller checks the Replicants' action statuses asynchronously and waits for both to finish. For the rest of the actions, we want the Replicants to appear to work in tandem so we'll wait for both of their actions to finish before proceeding:
+
+```python
+import numpy as np
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.output_data import OutputData, NavMeshPath, Bounds
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.arm import Arm
+from tdw.replicant.action_status import ActionStatus
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+class CarryCouch(Controller):
+    """
+    Both Replicants will approach opposite sides of the sofa.
+    They will reach to lift up the sofa (although only one Replicant is grasp), carry the sofa, and put it down again.
+
+    In some cases, the Replicants will act asynchronously.
+    In some cases, the Replicants will wait for each other to finish their respective actions.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        # Set the object ID.
+        self.object_id: int = Controller.get_unique_id()
+        # Set the Replicants.
+        self.replicant_0: Replicant = Replicant(replicant_id=0, position={"x": -1, "y": 0, "z": -1})
+        self.replicant_1: Replicant = Replicant(replicant_id=1, position={"x": 1, "y": 0, "z": -1})
+        # Add a camera and enable image capture.
+        camera = ThirdPersonCamera(position={"x": 0, "y": 2.2, "z": -2.61},
+                                   avatar_id="a",
+                                   look_at=self.object_id)
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_carry_couch")
+        print(f"Images will be saved to: {path}")
+        capture = ImageCapture(avatar_ids=["a"], path=path)
+        self.add_ons.extend([self.replicant_0, self.replicant_1, camera, capture])
+        # Initialize the scene.
+        commands = [TDWUtils.create_empty_room(12, 12),
+                    {"$type": "bake_nav_mesh"}]
+        commands.extend(Controller.get_add_physics_object(model_name="arflex_strips_sofa",
+                                                          object_id=self.object_id,
+                                                          position={"x": 0, "y": 0, "z": 1}))
+        commands.append({"$type": "make_nav_mesh_obstacle",
+                         "id": self.object_id,
+                         "carve_type": "stationary"})
+        self.communicate(commands)
+
+    def carry_sofa(self) -> None:
+        """
+        A "meta-action" directing both Replicants to "carry" a sofa.
+        """
+
+        # Get a response from the build.
+        resp = self.communicate([])
+        # Get the left and right sides of the sofa.
+        left = np.zeros(shape=3)
+        right = np.zeros(shape=3)
+        center = np.zeros(shape=3)
+        for i in range(len(resp) - 1):
+            r_id = OutputData.get_data_type_id(resp[i])
+            if r_id == "boun":
+                bounds = Bounds(resp[i])
+                for j in range(bounds.get_num()):
+                    if bounds.get_id(j) == self.object_id:
+                        left = bounds.get_left(j)
+                        right = bounds.get_right(j)
+                        center = bounds.get_center(j)
+                        break
+                break
+        # Set the height of each bounds point to 0.
+        left[1] = 0
+        right[1] = 0
+        center[1] = 0
+        # Offset the bounds positions.
+        v = left - center
+        v = v / np.linalg.norm(v)
+        left += v * 0.25
+        v = right - center
+        v = v / np.linalg.norm(v)
+        right += v * 0.25
+        # Set target positions for each.
+        distance_left = np.linalg.norm(self.replicant_0.dynamic.transform.position - left)
+        distance_right = np.linalg.norm(self.replicant_0.dynamic.transform.position - right)
+        if distance_left < distance_right:
+            replicant_0_target = left
+            replicant_1_target = right
+        else:
+            replicant_0_target = right
+            replicant_1_target = left
+        # Get paths to each target.
+        resp = self.communicate([{"$type": "send_nav_mesh_path",
+                                  "origin": TDWUtils.array_to_vector3(self.replicant_0.dynamic.transform.position),
+                                  "destination": TDWUtils.array_to_vector3(replicant_0_target),
+                                  "id": self.replicant_0.replicant_id},
+                                 {"$type": "send_nav_mesh_path",
+                                  "origin": TDWUtils.array_to_vector3(self.replicant_1.dynamic.transform.position),
+                                  "destination": TDWUtils.array_to_vector3(replicant_1_target),
+                                  "id": self.replicant_1.replicant_id}])
+        # Determine which Replicant requested this path.
+        replicant_0_path = np.zeros(shape=0)
+        replicant_1_path = np.zeros(shape=0)
+        for i in range(len(resp) - 1):
+            r_id = OutputData.get_data_type_id(resp[i])
+            if r_id == "path":
+                nav_mesh_path = NavMeshPath(resp[i])
+                path_id = nav_mesh_path.get_id()
+                if path_id == self.replicant_0.replicant_id:
+                    replicant_0_path = nav_mesh_path.get_path()
+                elif path_id == self.replicant_1.replicant_id:
+                    replicant_1_path = nav_mesh_path.get_path()
+        # These are the indices of the current waypoint in each path.
+        replicant_0_path_index: int = 1
+        replicant_1_path_index: int = 1
+        # Start moving to each waypoint.
+        self.replicant_0.collision_detection.avoid = False
+        self.replicant_1.collision_detection.avoid = False
+        self.replicant_0.collision_detection.objects = False
+        self.replicant_1.collision_detection.objects = False
+        self.replicant_0.move_to(target=replicant_0_path[replicant_0_path_index])
+        self.replicant_1.move_to(target=replicant_1_path[replicant_1_path_index])
+        # Wait until the Replicants stop pathfinding.
+        done = False
+        while not done:
+            # Check if both Replicants are done navigating.
+            replicant_0_done = False
+            if self.replicant_0.action.status != ActionStatus.ongoing:
+                replicant_0_path_index += 1
+                if replicant_0_path_index >= replicant_0_path.shape[0]:
+                    replicant_0_done = True
+                else:
+                    self.replicant_0.move_to(replicant_0_path[replicant_0_path_index])
+            replicant_1_done = False
+            if self.replicant_1.action.status != ActionStatus.ongoing:
+                replicant_1_path_index += 1
+                if replicant_1_path_index >= replicant_1_path.shape[0]:
+                    replicant_1_done = True
+                else:
+                    self.replicant_1.move_to(replicant_1_path[replicant_1_path_index])
+            done = replicant_0_done and replicant_1_done
+            # Continue the loop.
+            self.communicate([])
+        # Look at the sofa's center.
+        self.replicant_0.turn_to(target=center)
+        self.replicant_1.turn_to(target=center)
+        self.do_actions()
+        # Reach for the couch.
+        self.replicant_0.reach_for(target=self.object_id, arm=[Arm.left, Arm.right])
+        self.replicant_1.reach_for(target=self.object_id, arm=[Arm.left, Arm.right])
+        self.do_actions()
+        # Only the first Replicant grasps the couch.
+        self.replicant_0.grasp(target=self.object_id, arm=Arm.left, angle=None)
+        self.do_replicant_0_action()
+        # Everyone raises their hands.
+        self.reach_for_height(y=0.8)
+        # Do the actions.
+        self.do_actions()
+        # The first Replicant walks backwards. The second Replicant walks forwards.
+        self.replicant_0.move_by(distance=-2,
+                                 reset_arms=False)
+        self.replicant_1.move_by(distance=2,
+                                 reset_arms=False)
+        self.do_actions()
+        # Lower the sofa.
+        self.reach_for_height(y=0.1)
+        # Drop the sofa.
+        self.replicant_0.drop(arm=Arm.left)
+        self.do_replicant_0_action()
+        # Reset the arms.
+        self.replicant_0.reset_arm(arm=[Arm.left, Arm.right])
+        self.replicant_1.reset_arm(arm=[Arm.left, Arm.right])
+        self.do_actions()
+
+    def do_actions(self) -> None:
+        """
+        Wait for both Replicants' actions to end.
+        """
+
+        while self.replicant_0.action.status == ActionStatus.ongoing or self.replicant_1.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        self.communicate([])
+
+    def do_replicant_0_action(self) -> None:
+        """
+        Wait for only the first Replicant's action to end.
+        """
+
+        while self.replicant_0.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        self.communicate([])
+
+    def reach_for_height(self, y: float) -> None:
+        """
+        Tell both Replicants to reach for positions relative to their left hands.
+
+        :param y: Add this y value to the target position.
+        """
+
+        for replicant in [self.replicant_0, self.replicant_1]:
+            # Get the position of the left hand.
+            target = TDWUtils.array_to_vector3(replicant.dynamic.body_parts[replicant.static.hands[Arm.left]].position)
+            # Raise the target's height.
+            target["y"] = y
+            # The left hand reaches for the target and the right hand follows.
+            replicant.reach_for(target=target,
+                                arm=Arm.left,
+                                offhand_follows=True)
+        # Both Replicants do their actions.
+        self.do_actions()
+
+
+if __name__ == "__main__":
+    c = CarryCouch()
+    c.carry_sofa()
+    c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/multiple_replicants/carry_couch.gif)
+
+***
+
+**Next: [Reset](reset.md)**
+
+[Return to the README](../../../README.md)
+
+***
+
+Example controllers:
+
+- [multi_replicant.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/multi_replicant.py) A minimal multi-Replicant simulation.
+- [multi_replicant_state_machine.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/multi_replicant_state_machine.py) Control the actions of multiple Replicants using a state machine.
+- [multi_navigate.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/multi_navigate.py) Multiple Replicants navigate using custom actions and state machines.
+- [carry_couch.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/carry_couch.py) Two Replicants appear to carry a couch together.
+
+Command API:
+
+- [`bake_nav_mesh`](../../api/command_api.md#bake_nav_mesh)
+- [`make_nav_mesh_obstacle`](../../api/command_api.md#make_nav_mesh_obstacle)
+- [`send_nav_mesh_path`](../../api/command_api.md#send_nav_mesh_path)
+
+Output Data API:
+
+- [`NavMeshPath`](../../api/output_data.md#NavMeshPath)
+
+Python API:
+
+- [`Replicant`](../../python/add_ons/replicant.md)
+- [`Action`](../../python/replicant/actions/action.md)
\ No newline at end of file
diff --git a/Documentation/lessons/replicants/navigation.md b/Documentation/lessons/replicants/navigation.md
new file mode 100644
index 000000000..a3b6f4c88
--- /dev/null
+++ b/Documentation/lessons/replicants/navigation.md
@@ -0,0 +1,287 @@
+##### Replicants
+
+# Navigation
+
+*For more information regarding navigation in TDW, [read this.](../navigation/overview.md)*
+
+Like all other [agents](../agents/overview.md), the Replicant doesn't have a built-in navigation system. In most cases, the best way to handle navigation is to train a model for navigation planning using the Replicant's [images, depth maps, and other output data](output_data.md).
+
+This document explains two simple ways to handle basic navigation.
+
+### Basic obstacle avoidance
+
+This controller has a very basic system for handling obstacles. If the Replicant [detects an obstacle while moving](movement.md), the controller tells the Replicant to back up, turn by 45 degrees, and try moving again:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.image_frequency import ImageFrequency
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+class ObstacleAvoidance(Controller):
+    """
+    A very simple method for navigating around obstacles.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        # Set the target object's ID here so it can be referenced from more than one function.
+        self.target_id = Controller.get_unique_id()
+        # Set the replicant here so that it can be referenced from more than one function.
+        self.replicant = Replicant(position={"x": 0, "y": 0, "z": -8},
+                                   image_frequency=ImageFrequency.never)
+
+    def run(self) -> None:
+        # Enable image capture from a third-person camera.
+        camera = ThirdPersonCamera(position={"x": -0.5, "y": 1.175, "z": 8.45},
+                                   look_at={"x": 0.5, "y": 1, "z": 0},
+                                   avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_obstacle_avoidance")
+        print(f"Images will be saved to: {path}")
+        capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+        self.add_ons.extend([self.replicant, camera, capture])
+        # Create an empty room.
+        commands = [TDWUtils.create_empty_room(12, 20)]
+        # Add some objects.
+        commands.extend(Controller.get_add_physics_object(model_name="chair_billiani_doll",
+                                                          object_id=Controller.get_unique_id(),
+                                                          position={"x": 1.35, "y": 0, "z": 2.75},
+                                                          rotation={"x": 0, "y": 20, "z": 0},
+                                                          library="models_core.json"))
+        commands.extend(Controller.get_add_physics_object(model_name="live_edge_coffee_table",
+                                                          object_id=Controller.get_unique_id(),
+                                                          position={"x": 0, "y": 0, "z": 2},
+                                                          rotation={"x": 0, "y": 20, "z": 0},
+                                                          library="models_core.json"))
+        # Add a ball.
+        ball_id = Controller.get_unique_id()
+        commands.extend(Controller.get_add_physics_object(model_name="prim_sphere",
+                                                          object_id=ball_id,
+                                                          position={"x": 5, "y": 0, "z": 3.5},
+                                                          rotation={"x": 0, "y": 0, "z": 0},
+                                                          scale_factor={"x": 0.2, "y": 0.2, "z": 0.2},
+                                                          kinematic=True,
+                                                          gravity=False,
+                                                          library="models_special.json"))
+        # Add the target object.
+        commands.extend(Controller.get_add_physics_object(model_name="prim_sphere",
+                                                          object_id=self.target_id,
+                                                          position={"x": 0, "y": 0, "z": 4.0},
+                                                          rotation={"x": 0, "y": 0, "z": 0},
+                                                          scale_factor={"x": 0.2, "y": 0.2, "z": 0.2},
+                                                          kinematic=True,
+                                                          gravity=False,
+                                                          library="models_special.json"))
+        # Send the commands.
+        self.communicate(commands)
+        # Exclude the balls so they do not act as obstacles and trigger the avoidance mechanisms.
+        self.replicant.collision_detection.exclude_objects = [self.target_id, ball_id]
+        # Start to navigate.
+        done = False
+        while not done:
+            # Move to the target.
+            self.replicant.move_to(target=self.target_id)
+            self.do_action()
+            # We arrived at the target.
+            if self.replicant.action.status == ActionStatus.success:
+                done = True
+            # Take evasive action.
+            else:
+                self.replicant.move_by(distance=-0.5)
+                self.do_action()
+                self.replicant.turn_by(angle=45.0)
+                self.do_action()
+                self.replicant.move_by(distance=3.0)
+                self.do_action()
+        self.communicate({"$type": "terminate"})
+
+    def do_action(self) -> None:
+        """
+        Call `communicate([])` until the Replicant's action is done.
+        """
+
+        while self.replicant.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        self.communicate([])
+
+
+if __name__ == "__main__":
+    ObstacleAvoidance().run()
+```
+
+Result:
+
+![](images/navigation/obstacle_avoidance.gif)
+
+### NavMesh pathfinding
+
+A Replicant can follow a [NavMesh path](../navigation/nav_mesh.md). Send [`bake_nav_mesh`](../../api/command_api.md#bake_nav_mesh) and [`make_nav_mesh_obstacle`](../../api/command_api.md#make_nav_mesh_obstacle) to initialize the NavMesh. Send [`send_nav_mesh_path`](../../api/command_api.md#send_nav_mesh_path) to received [`NavMeshPath`](../../api/output_data.md#NavMeshPath) output data. Then, call [`self.replicant.move_to(target)`](movement.md) for every waypoint on the path. 
+
+In this example controller, navigation is handled in the `navigate(destination)` function. You can repeatedly call this function to set a new destination. The function returns True if the Replicant arrived at the destination. **This isn't the best way to handle NavMesh pathfinding.** The main problem is that it won't be possible to have multiple Replicants navigating around the scene. **The next two documents in this tutorial will show increasingly better implementations of this controller.**
+
+```python
+from typing import Dict
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.controller import Controller
+from tdw.output_data import OutputData, NavMeshPath
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.action_status import ActionStatus
+
+
+class Pathfind(Controller):
+    """
+    An example of how to utilize the NavMesh to pathfind.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        self.replicant = Replicant(position={"x": 0.1, "y": 0, "z": -5})
+        self.camera: ThirdPersonCamera = ThirdPersonCamera(position={"x": 0, "y": 13.8, "z": 0},
+                                                           look_at={"x": 0, "y": 0, "z": 0},
+                                                           avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_pathfind")
+        print(f"Images will be saved to: {path}")
+        self.capture: ImageCapture = ImageCapture(avatar_ids=["a"], path=path)
+        self.add_ons.extend([self.replicant, self.camera, self.capture])
+        # Set the object IDs.
+        self.trunk_id = Controller.get_unique_id()
+        self.chair_id = Controller.get_unique_id()
+        self.table_id = Controller.get_unique_id()
+        self.rocking_horse_id = Controller.get_unique_id()
+
+    def init_scene(self):
+        # Load the scene.
+        # Bake the NavMesh.
+        # Add objects to the scene and add NavMesh obstacles.
+        self.communicate([{"$type": "load_scene",
+                           "scene_name": "ProcGenScene"},
+                          TDWUtils.create_empty_room(12, 12),
+                          {"$type": "bake_nav_mesh"},
+                          Controller.get_add_object(model_name="trunck",
+                                                    object_id=self.trunk_id,
+                                                    position={"x": 1.5, "y": 0, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.trunk_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="chair_billiani_doll",
+                                                    object_id=self.chair_id,
+                                                    position={"x": -2.25, "y": 0, "z": 2.5},
+                                                    rotation={"x": 0, "y": 20, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.chair_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="live_edge_coffee_table",
+                                                    object_id=self.table_id,
+                                                    position={"x": 0.2, "y": 0, "z": -2.25},
+                                                    rotation={"x": 0, "y": 20, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.table_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="rh10",
+                                                    object_id=self.rocking_horse_id,
+                                                    position={"x": -1, "y": 0, "z": 1.5}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.rocking_horse_id,
+                           "carve_type": "stationary"}])
+
+    def navigate(self, destination: Dict[str, float]) -> bool:
+        # Don't handle collision detection (it will interfere with NavMesh pathfinding).
+        self.replicant.collision_detection.avoid = False
+        # The origin of the path is the current position of the Replicant.
+        origin = TDWUtils.array_to_vector3(self.replicant.dynamic.transform.position)
+        # Request a NavMeshPath.
+        resp = self.communicate({"$type": "send_nav_mesh_path",
+                                 "origin": origin,
+                                 "destination": destination,
+                                 "id": self.replicant.replicant_id})
+        # Parse the output data to get the path.
+        path: np.ndarray = np.zeros(shape=1)
+        for i in range(len(resp) - 1):
+            r_id = OutputData.get_data_type_id(resp[i])
+            if r_id == "path":
+                nav_mesh_path = NavMeshPath(resp[i])
+                # This path belongs to the Replicant (this is useful when there is more than one agent).
+                if nav_mesh_path.get_id() == self.replicant.replicant_id:
+                    # We failed to get a valid path.
+                    if nav_mesh_path.get_state() != "complete":
+                        self.replicant.collision_detection.avoid = True
+                        return False
+                    # Get the path.
+                    path = nav_mesh_path.get_path()
+                    # Break because we have the data we need.
+                    break
+        # Move to each waypoint. Ignore the first waypoint because it's the position of the Replicant.
+        for i in range(1, path.shape[0]):
+            target = TDWUtils.array_to_vector3(path[i])
+            target["y"] = 0
+            # Move to the waypoint.
+            self.replicant.move_to(target=target)
+            # Do the action.
+            self.do_action()
+            # If the Replicant failed to reach the waypoint, end here.
+            if self.replicant.action.status != ActionStatus.success:
+                self.replicant.collision_detection.avoid = True
+                return False
+        # We arrived at the destination. Re-enable obstacle detection and return True.
+        self.replicant.collision_detection.avoid = True
+        return True
+
+    def do_action(self) -> None:
+        while self.replicant.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        self.communicate([])
+
+    def end(self) -> None:
+        self.communicate({"$type": "terminate"})
+
+
+if __name__ == "__main__":
+    c = Pathfind()
+    # Initialize the scene.
+    c.init_scene()
+    # Navigate to a destination.
+    success = c.navigate(destination={"x": 0, "y": 0, "z": 4})
+    print(success)
+    # End the simulation.
+    c.end()
+```
+
+Result:
+
+![](images/navigation/pathfind.gif)
+
+***
+
+**Next: [Custom actions](custom_actions.md)**
+
+[Return to the README](../../../README.md)
+
+***
+
+Example controllers:
+
+- [obstacle_avoidance.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/obstacle_avoidance.py) A very simple method for navigating around obstacles.
+- [pathfind.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/pathfind.py) An example of how to utilize the NavMesh to pathfind.
+
+Command API:
+
+- [`bake_nav_mesh`](../../api/command_api.md#bake_nav_mesh)
+- [`make_nav_mesh_obstacle`](../../api/command_api.md#make_nav_mesh_obstacle)
+- [`send_nav_mesh_path`](../../api/command_api.md#send_nav_mesh_path)
+
+Output Data API:
+
+- [`NavMeshPath`](../../api/output_data.md#NavMeshPath)
+
+Python API:
+
+- [`Replicant`](../../python/add_ons/replicant.md)
\ No newline at end of file
diff --git a/Documentation/lessons/replicants/output_data.md b/Documentation/lessons/replicants/output_data.md
new file mode 100644
index 000000000..b907e55a7
--- /dev/null
+++ b/Documentation/lessons/replicants/output_data.md
@@ -0,0 +1,326 @@
+##### Replicants
+
+# Output Data
+
+*For more information regarding TDW output data, [read this](../core_concepts/output_data.md).*
+
+*For more information regarding TDW image output data, [read this](../core_concepts/images.md).*
+
+The Replicant  includes static data about the agent (such as body part IDs) and dynamic data (such as body part positions). 
+
+## Static Replicant Data
+
+Static Replicant data is stored in `replicant.static`. This is a [`ReplicantStatic` object](../../python/replicant/replicant_static.md).
+
+In addition to the replicant's ID (`static.replicant_id`), each body part has a separate ID.
+
+- `static.body_parts` is a dictionary. The key is a [`ReplicantBodyPart`](../../python/replicant/replicant_body_part.md) enum value. The value is the body part ID:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.replicant_body_part import ReplicantBodyPart
+
+c = Controller()
+replicant = Replicant()
+c.add_ons.append(replicant)
+c.communicate(TDWUtils.create_empty_room(12, 12))
+# Print the ID of the Replicant's left hand.
+print(replicant.static.body_parts[ReplicantBodyPart.hand_l])
+c.communicate({"$type": "terminate"})
+```
+
+- `static.body_parts_by_id` is the reciprocal dictionary: the key is the body part ID and the value is a [`ReplicantBodyPart`](../../python/replicant/replicant_body_part.md) enum value.
+
+- Internally, the Replicant often needs to refer to each hand quickly. For ease of use, the static data includes `static.hands`, a dictionary with the key is an [`Arm`](../../python/replicant/arm.md) enum value and the value is an ID of a hand:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.arm import Arm
+
+c = Controller()
+replicant = Replicant()
+c.add_ons.append(replicant)
+c.communicate(TDWUtils.create_empty_room(12, 12))
+# Print the ID of the Replicant's left hand.
+print(replicant.static.hands[Arm.left])
+c.communicate({"$type": "terminate"})
+```
+
+-  `static.avatar_id` is the ID of the Replicant's avatar (camera). This is always the string-ified version of `replicant_id`. If `replicant_id` is `0` then `avatar_id` is `"0"`.
+
+## Dynamic Replicant Data
+
+Dynamic Replicant data is stored in `self.replicant.dynamic`. This is a [`ReplicantDynamic` object](../../python/replicant/replicant_dynamic.md). It is updated every `communicate()` call.
+
+- `dynamic.transform` is the [`Transform`](../../python/object_data/transform.md) (position, rotation, and forward) of the Replicant. This prints the position of the Replicant:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+
+c = Controller()
+replicant = Replicant()
+c.add_ons.append(replicant)
+c.communicate(TDWUtils.create_empty_room(12, 12))
+print(replicant.dynamic.transform.position)
+c.communicate({"$type": "terminate"})
+```
+
+Output:
+
+```
+[0. 0. 0.]
+```
+
+- `dynamic.body_parts` is a dictionary. The key is a body part ID. The value is a [`Transform`](../../python/object_data/transform.md). This example prints the position of the Replicant's left hand. Note that we use the `static` data to get the hand's ID.
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.arm import Arm
+
+c = Controller()
+replicant = Replicant()
+c.add_ons.append(replicant)
+c.communicate(TDWUtils.create_empty_room(12, 12))
+hand_id = replicant.static.hands[Arm.left]
+print(replicant.dynamic.body_parts[hand_id].position)
+c.communicate({"$type": "terminate"})
+```
+
+- `dynamic.held_objects` records whether an object is held in each hand and if so what the object's ID is. The key is an [`Arm`](../../python/replicant/arm.md) enum value and the value is an ID of an object. If the hand isn't holding an object, the key won't be in the dictionary:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.arm import Arm
+
+c = Controller()
+replicant = Replicant()
+c.add_ons.append(replicant)
+c.communicate(TDWUtils.create_empty_room(12, 12))
+hand = Arm.left
+# Is the left hand holding an object?
+if hand in replicant.dynamic.held_objects:
+    # This is the object being held.
+    object_id = replicant.dynamic.held_objects[hand]
+    print(object_id)
+```
+
+- `dynamic.projection_matrix` and `dynamic.camera_matrix` are numpy arrays of the camera matrices.
+- `dynamic.collisions` is a dictionary. The key is a body part ID. The value is a list of object IDs that the body part collided with. This example checks whether the lower left arm is colliding with any objects:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.replicant_body_part import ReplicantBodyPart
+
+c = Controller()
+replicant = Replicant()
+c.add_ons.append(replicant)
+c.communicate(TDWUtils.create_empty_room(12, 12))
+body_part_id = replicant.static.body_parts[ReplicantBodyPart.lowerarm_l]
+# Is the lower left arm colliding with any objects?
+object_ids = replicant.dynamic.collisions[body_part_id]
+print(len(object_ids) > 0)
+c.communicate({"$type": "terminate"})
+```
+
+Output:
+
+```
+False
+```
+
+- `dynamic.got_images` is a boolean that indicates whether there is image data.
+- `dynamic.images` is a dictionary of image data. The key is the [pass mask as a string](../core_concepts/images.md). See below for more information:
+
+## Images
+
+Images, as noted above, are stored in `dynamic.images`. 
+
+### Image frequency
+
+**By default, images are captured at the end of every [action](actions.md).** If there is no image data, `dynamic.images` is an empty dictionary:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.action_status import ActionStatus
+
+c = Controller()
+replicant = Replicant()
+c.add_ons.append(replicant)
+c.communicate(TDWUtils.create_empty_room(12, 12))
+# Walk forward 0.5 meters.
+replicant.move_by(0.5)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+    # This will be an empty dictionary.
+    print(replicant.dynamic.images.keys())
+c.communicate([])
+# This will print the image pass mask labels.
+print(replicant.dynamic.images.keys())
+c.communicate({"$type": "terminate"})
+```
+
+Output:
+
+```
+dict_keys([])
+dict_keys([])
+dict_keys([])
+dict_keys([])
+dict_keys([])
+dict_keys([])
+dict_keys([])
+dict_keys([])
+dict_keys([])
+dict_keys([])
+dict_keys([])
+dict_keys(['img', 'id', 'depth'])
+```
+
+If you want to capture images on every `communicate()` call, or never capture images, set the `image_frequency` parameter in the Replicant constructor, which accepts an [`ImageFrequency`](../../python/replicant/image_frequency.md) value:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.image_frequency import ImageFrequency
+
+c = Controller()
+# Request image data on every `communicate()` call.
+replicant = Replicant(image_frequency=ImageFrequency.always)
+c.add_ons.append(replicant)
+c.communicate(TDWUtils.create_empty_room(12, 12))
+replicant.move_by(0.5)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+    print(replicant.dynamic.images.keys())
+c.communicate([])
+print(replicant.dynamic.images.keys())
+c.communicate({"$type": "terminate"})
+```
+
+Output:
+
+```
+dict_keys([])
+dict_keys(['img', 'id', 'depth'])
+dict_keys(['img', 'id', 'depth'])
+dict_keys(['img', 'id', 'depth'])
+dict_keys(['img', 'id', 'depth'])
+dict_keys(['img', 'id', 'depth'])
+dict_keys(['img', 'id', 'depth'])
+dict_keys(['img', 'id', 'depth'])
+dict_keys(['img', 'id', 'depth'])
+dict_keys(['img', 'id', 'depth'])
+dict_keys(['img', 'id', 'depth'])
+dict_keys(['img', 'id', 'depth'])
+```
+
+### Save images
+
+To save images, call `replicant.dynamic.save_images` after a `communicate()` call:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.image_frequency import ImageFrequency
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_egocentric")
+print(f"Images will be saved to: {path}")
+c = Controller()
+# Request image data on every `communicate()` call.
+replicant = Replicant(image_frequency=ImageFrequency.always)
+c.add_ons.append(replicant)
+c.communicate(TDWUtils.create_empty_room(12, 12))
+replicant.move_by(2)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+    # Save the images.
+    replicant.dynamic.save_images(output_directory=path)
+c.communicate([])
+# Save the images.
+replicant.dynamic.save_images(output_directory=path)
+c.communicate({"$type": "terminate"})
+```
+
+Result:
+
+![](images/output_data/move_by_egocentric.gif)
+
+### `_depth` and `_id` passes
+
+In addition to the the `_img` pass, the Replicant will capture `_id` and `_depth` passes. The `_id` pass is `dynamic.images["id"]` and the `_depth` pass is `dynamic.images["depth"]` (assuming that `dynamic.images` isn't empty).
+
+- [To learn more about the `_id` pass, read  this.](../visual_perception/id.md)
+- [To learn more about the `_depth` pass, including how to interpret it as a point cloud, read this.](../visual_perception/depth.md)
+
+## Low-level description
+
+When the `Replicant` add-on initializes, it sends [`send_replicants`](../../api/command_api.md#send_replicants) in order to receive [`Replicants`](../../api/output_data.md#Replicants) output data per `communicate()` call. This data has been optimized for speed, not human usage; this one of many reasons that we recommend using the `Replicant` add-on instead of low-level TDW commands and output data. Both `ReplicantStatic` and `ReplicantDynamic` parse`Replicants` output data. 
+
+Some [actions](actions.md) require additional output data. When the `Replicant` add-on initializes, it also sends [`send_transforms`](../../api/command_api.md#send_transforms), [`send_bounds`](../../api/command_api.md#send_bounds), and [`send_containment`](../../api/command_api.md#send_containment) to receive  [`Transforms`](../../api/output_data.md#Transforms),  [`Bounds`](../../api/output_data.md#Bounds), and  [`Containment`](../../api/output_data.md#Containment) respectively per `communicate()` call.
+
+The `Replicant`'s `static` and `dynamic` data are initially `None`. Both are set *after* the first `communicate()` call (because the Replicant needs one `communicate()` call to start requesting output data).
+
+On the *second* `communicate()` call (i.e. one call after initialization), the `Replicant` add-on sends [`create_avatar`](../../api/command_api.md#create_avatar) and [`parent_avatar_to_replicant`](../../api/command_api.md#parent_avatar_to_replicant) to attach an [avatar (camera)](../core_concepts/avatars.md) to its head. This is how it receives image data.
+
+The `Replicant` sends [`send_images`](../../api/command_api.md#send_images) and [`send_camera_matrices`](../../api/command_api.md#send_camera_matrices) to receive [`Images`](../../api/output_data.md#Images) and [`CameraMatrices`](../../api/output_data.md#CameraMatrices) output data, respectively. The frequency at which this data is sent depends on the value of the `image_frequency` value in the constructor. By default, these commands are only sent when an action ends; accordingly, they are actually passed from the `action`, to the `Replicant`, to the controller.
+
+***
+
+**Next: [Collision detection](collision_detection.md)**
+
+[Return to the README](../../../README.md)
+
+***
+
+Example controllers:
+
+- [egocentric_images.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/egocentric_images.py) Capture image data from the Replicant per `communicate()` call and save it to disk.
+
+Command API:
+
+- [`send_replicants`](../../api/command_api.md#send_replicants)
+- [`send_transforms`](../../api/command_api.md#send_transforms)
+- [`send_bounds`](../../api/command_api.md#send_bounds)
+- [`send_containment`](../../api/command_api.md#send_containment)
+- [`create_avatar`](../../api/command_api.md#create_avatar)
+- [`parent_avatar_to_replicant`](../../api/command_api.md#parent_avatar_to_replicant)
+- [`send_images`](../../api/command_api.md#send_images)
+- [`send_camera_matrices`](../../api/command_api.md#send_camera_matrices)
+
+Output Data API:
+
+- [`Replicants`](../../api/output_data.md#Replicants)
+- [`Transforms`](../../api/output_data.md#Transforms)
+- [`Bounds`](../../api/output_data.md#Bounds)
+- [`Containment`](../../api/output_data.md#Containment)
+- [`Images`](../../api/output_data.md#Images)
+- [`CameraMatrices`](../../api/output_data.md#CameraMatrices)
+
+Python API:
+
+- [`Replicant`](../../python/add_ons/replicant.md)
+- [`ReplicantStatic`](../../python/replicant/replicant_static.md)
+- [`ReplicantDynamic`](../../python/replicant/replicant_dynamic.md)
+- [`Arm`](../../python/replicant/arm.md)
+- [`ReplicantBodyPart`](../../python/replicant/replicant_body_part.md)
+- [`ImageFrequency`](../../python/replicant/image_frequency.md)
+- [`Transform`](../../python/object_data/transform.md)
diff --git a/Documentation/lessons/replicants/overview.md b/Documentation/lessons/replicants/overview.md
new file mode 100644
index 000000000..0b8df8309
--- /dev/null
+++ b/Documentation/lessons/replicants/overview.md
@@ -0,0 +1,76 @@
+##### Replicants
+
+# Overview
+
+A **Replicant** is a human-like [agent](../agents/overview.md).
+
+Replicants move via a combination of inverse kinematics (IK) for actions such as reaching for an object; the combined joint movement is fluid and plausible. Replicants can also perform arbitrary pre-recorded animations, meaning that specific complex actions are far easier to achieve than in a robotics simulation.
+
+Replicants are visually similar to [non-physics humanoids](../non_physics_humanoids/overview.md) but *do* exist in a physics context. Replicants can *cause* physics events but won't *respond* to physics events. For example, if a Replicant walks into an object, it will push the object aside. Non-physics humanoids, on the other hand, will move through objects without interacting with them.
+
+![](images/crash.gif)
+
+Replicants differ from [robots](../robots/overview.md) in that they aren't *entirely* physics-driven. In addition  to being massless, Replicant joints aren't defined as motorized drives. Joint velocity, momentum, etc. aren't concepts that exist in the context of a Replicant.
+
+Replicants are thus a compromise on realism: It is far easier to achieve complex behavior with a Replicant than with a robot. On the other hand, the Replicant's motion itself isn't driven by physics to the extent that a robot's would be.
+
+Like robots, Replicants are agents but are *not* [avatars](../core_concepts/avatars.md) in TDW. Avatar commands won't work with robots.
+
+## The `Replicant` add-on
+
+Like robots, Replicants are best controlled via the [`Replicant`](../../python/add_ons/replicant.md) add-on. Like all other add-ons, the `Replicant` add-on decomposes into low-level TDW commands but, given the complexity of the agent, we recommend that you *always* use the `Replicant` add-on rather than directly manipulating the agent with low-level commands.
+
+Adding a Replicant to a scene is simple:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+
+c = Controller()
+replicant = Replicant()
+c.add_ons.append(replicant)
+c.communicate(TDWUtils.create_empty_room(12, 12))
+```
+
+You can set the initial position and rotation of the Replicant in the constructor:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+
+c = Controller()
+replicant = Replicant(position={"x": 1, "y": 0, "z": -2},
+                      rotation={"x": 0, "y": 30, "z": 0})
+c.add_ons.append(replicant)
+c.communicate(TDWUtils.create_empty_room(12, 12))
+```
+
+There are additional constructor parameters that will be covered in subsequent documents.
+
+## Replicant asset bundles
+
+Like [objects](../core_concepts/objects.md) and [scenes](../core_concepts/scenes.md), Replicants are **asset bundles** stored on a remote S3 server that must be downloaded before they can be added to the scene. This means that when you first add a Replicant to the scene, there will be a brief pause while it is downloaded and loaded into memory. For subsequent scene resets, the Replicant will already in memory and will appear immediately.
+
+## Future development
+
+**The Replicant is usable but unfinished.** This version of the Replicant can be thought of as "Phase 1" and there are known limitations to it. These include:
+
+- The Replicant's pose won't blend between [animations](animations.md), meaning that it will always snap back to a neutral pose at the start of an animation. 
+- The Replicant's [turn action](movement.md) doesn't have an animation.
+- The Replicant can't bend its fingers to achieve a realistic grasp action.
+
+**All of these limitations will be resolved in future phases of Replicant development.** Additionally, more features will be added to the Replicant over time.
+
+***
+
+**Next: [Actions](actions.md)**
+
+[Return to the README](../../../README.md)
+
+***
+
+Python API:
+
+- [`Replicant`](../../python/add_ons/replicant.md)
\ No newline at end of file
diff --git a/Documentation/lessons/replicants/reset.md b/Documentation/lessons/replicants/reset.md
new file mode 100644
index 000000000..40394b293
--- /dev/null
+++ b/Documentation/lessons/replicants/reset.md
@@ -0,0 +1,146 @@
+##### Replicants
+
+# Reset
+
+Call `replicant.reset()` to reset a Replicant when you start a new scene. This will destroy the current Replicant and reset the add-on's state (e.g. static and dynamic data, collision detection rules, etc.). To properly reset a Replicant, you must also destroy and recreate the scene; otherwise, a copy of this Replicant will be created without destroying the current one.
+
+In this controller, we'll run several "trials" and reset the scene each time. To end the scene, we'll clear the add-ons to prevent them from resetting and then send `{"$type": "terminate"}`:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+class Reset(Controller):
+    """
+    Run a series of trials and reset the scene each time.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        self.replicant = Replicant()
+        self.camera = ThirdPersonCamera(position={"x": 2, "y": 3, "z": 2.53},
+                                        look_at=self.replicant.replicant_id,
+                                        avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reset")
+        print(f"Images will be saved to: {path}")
+        self.capture = ImageCapture(avatar_ids=[self.camera.avatar_id], path=path)
+        self.add_ons.extend([self.replicant, self.camera, self.capture])
+
+    def trial(self, distance: float):
+        # Load a new scene.
+        self.communicate([{"$type": "load_scene",
+                           "scene_name": "ProcGenScene"},
+                          TDWUtils.create_empty_room(12, 12)])
+        self.replicant.move_by(distance=distance)
+        while self.replicant.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        self.communicate([])
+        # End the trial by resetting the add-ons.
+        self.replicant.reset()
+        self.camera.initialized = False
+        # Remember the current frame count.
+        f = self.capture.frame
+        # Reset image capture.
+        self.capture.initialized = False
+        # Set the current frame count.
+        self.capture.frame = f
+
+    def end(self) -> None:
+        # Clear the add-ons so that nothing tries to initialize when we terminate.
+        self.add_ons.clear()
+        self.communicate({"$type": "terminate"})
+
+
+if __name__ == "__main__":
+    c = Reset()
+    for d in [1, 2, 3]:
+        c.trial(distance=d)
+    c.end()
+```
+
+Result:
+
+![](images/reset/reset.gif)
+
+You can optionally set the `position` and `rotation` parameters of `reset()`:
+
+```python
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+class Reset(Controller):
+    """
+    Run a series of trials and reset the scene each time.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        self.replicant = Replicant()
+        self.camera = ThirdPersonCamera(position={"x": 2, "y": 3, "z": 2.53},
+                                        look_at=self.replicant.replicant_id,
+                                        avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reset")
+        print(f"Images will be saved to: {path}")
+        self.capture = ImageCapture(avatar_ids=[self.camera.avatar_id], path=path)
+        self.add_ons.extend([self.replicant, self.camera, self.capture])
+
+    def trial(self, distance: float):
+        # Load a new scene.
+        self.communicate([{"$type": "load_scene",
+                           "scene_name": "ProcGenScene"},
+                          TDWUtils.create_empty_room(12, 12)])
+        self.replicant.move_by(distance=distance)
+        while self.replicant.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        self.communicate([])
+        # End the trial by resetting the add-ons.
+        self.replicant.reset(position={"x": 0, "y": 0, "z": 0},
+                             rotation={"x": 0, "y": 0, "z": 0})
+        self.camera.initialized = False
+        # Remember the current frame count.
+        f = self.capture.frame
+        # Reset image capture.
+        self.capture.initialized = False
+        # Set the current frame count.
+        self.capture.frame = f
+
+    def end(self) -> None:
+        # Clear the add-ons so that nothing tries to initialize when we terminate.
+        self.add_ons.clear()
+        self.communicate({"$type": "terminate"})
+
+
+if __name__ == "__main__":
+    c = Reset()
+    for d in [1, 2, 3]:
+        c.trial(distance=d)
+    c.end()
+```
+
+***
+
+**This is the last document in the "Replicants" tutorial.**
+
+[Return to the README](../../../README.md)
+
+***
+
+Example controllers:
+
+- [reset.py](https://github.com/threedworld-mit/tdw/blob/master/Python/example_controllers/replicant/reset.py) Run a series of trials and reset the scene each time.
+
+Python API:
+
+- [`Replicant`](../../python/add_ons/replicant.md)
\ No newline at end of file
diff --git a/Documentation/lessons/semantic_states/containment.md b/Documentation/lessons/semantic_states/containment.md
index e03a8049c..99627c5ed 100644
--- a/Documentation/lessons/semantic_states/containment.md
+++ b/Documentation/lessons/semantic_states/containment.md
@@ -10,9 +10,9 @@
 | `inside`         | An object in a basket    |
 | `enclosed`       | An object in a microwave |
 
-## The `ContainerManager` add-on
+## Container shapes
 
-[`ContainerManager`](../../python/add_ons/container_manager.md) is an add-on that adds semantic containment listeners to objects. Every [model record](../../python/librarian/model_librarian.md) has a `container_shapes` field. Each `ContainerShape` defines a 3D space and includes a semantic tag.
+Every [model record](../../python/librarian/model_librarian.md) has a `container_shapes` field, a list of JSON data that deserializes to [`ContainerShape`](../../python/container_data/container_shape.md). Each `ContainerShape` defines a 3D space and includes a semantic tag.
 
 Only a subset of objects in TDW that could be containers have these predefined container shapes; more will be added over time. To print the current full list:
 
@@ -25,6 +25,14 @@ for record in librarian.records:
         print(record.name)
 ```
 
+Container shapes need to be added to the object's metadata in the build in order to function; in other words, they are defined both in the JSON metadata and in the build's C# code.
+
+**Container shapes are only added if you call `Controller.get_add_physics_object()`, *not* `Controller.get_add_object()`.** This is because container shapes are each added in a separate command. If  you call `Controller.get_add_physics_object()`, you will receive a list of commands, including commands to add container shapes. Conversely, `Controller.get_add_object()` returns a single command.
+
+## The `ContainerManager` add-on
+
+[`ContainerManager`](../../python/add_ons/container_manager.md) is an add-on that adds semantic containment listeners to objects. 
+
 To use the `ContainerManager` you must add it to `c.add_ons`:
 
 ```python
@@ -305,7 +313,7 @@ You may wish to record the raw containment data and then further filter it for c
 
 ## Low-level API
 
-Per object in the scene, `ContainerManager` tries to find a corresponding `ModelRecord` and then checks its `container_shapes` list. For each container shape, `ContainerManager` sends the following:
+Calling `Controller.get_add_physics_object()` will automatically check the object's metadata record and add container shape commands as needed:
 
 | Container Shape                                              | Command                                                      |
 | ------------------------------------------------------------ | ------------------------------------------------------------ |
@@ -313,7 +321,7 @@ Per object in the scene, `ContainerManager` tries to find a corresponding `Model
 | [`CylinderContainer`](../../python/container_data/cylinder_container.md) | [`add_cylinder_container`](../../api/command_api.md#add_cylinder_container) |
 | [`SphereContainer`](../../python/container_data/sphere_container.md) | [`add_sphere_container`](../../api/command_api.md#add_sphere_container) |
 
-`ContainerManager` then sends [`send_containment`](../../api/command_api.md#send_containment) which, per `communicate()` call, sends [`Overlap`](../../api/output_data.md#Overlap) data for all container shapes that have overlapping objects (other than the parent object). `Overlap` data is *not* sent if there are no overlapping objects.
+`ContainerManager` then sends [`send_containment`](../../api/command_api.md#send_containment) which, per `communicate()` call, sends [`Containment`](../../api/output_data.md#Containment) data for all container shapes that have overlapping objects (other than the parent object).
 
 ***
 
@@ -346,4 +354,4 @@ Command API:
 
 Output Data:
 
-- [`Overlap`](../../api/output_data.md#Overlap)
\ No newline at end of file
+- [`Containment`](../../api/output_data.md#Containment)
\ No newline at end of file
diff --git a/Documentation/python/add_ons/container_manager.md b/Documentation/python/add_ons/container_manager.md
index e20614310..71bbaee85 100644
--- a/Documentation/python/add_ons/container_manager.md
+++ b/Documentation/python/add_ons/container_manager.md
@@ -63,57 +63,6 @@ This is called within `Controller.communicate(commands)` before sending commands
 | --- | --- | --- | --- |
 | commands |  List[dict] |  | The commands that are about to be sent to the build. |
 
-#### add_box
-
-**`self.add_box(object_id, position, tag, half_extents, rotation)`**
-
-Add a box container shape to an object.
-
-
-| Parameter | Type | Default | Description |
-| --- | --- | --- | --- |
-| object_id |  int |  | The ID of the object. |
-| position |  Dict[str, float] |  | The position of the box relative to the parent object. |
-| tag |  ContainerTag |  | The box's semantic [`ContainerTag`](../container_data/container_tag.md). |
-| half_extents |  Dict[str, float] |  | The half-extents (half the scale) of the box. |
-| rotation |  Dict[str, float] |  | The rotation of the box in Euler angles relative to the parent object. |
-
-_Returns:_  The ID of the container shape.
-
-#### add_cylinder
-
-**`self.add_cylinder(object_id, position, tag, radius, height, rotation)`**
-
-Add a cylinder container shape to an object.
-
-
-| Parameter | Type | Default | Description |
-| --- | --- | --- | --- |
-| object_id |  int |  | The ID of the object. |
-| position |  Dict[str, float] |  | The position of the cylinder relative to the parent object. |
-| tag |  ContainerTag |  | The cylinder's semantic [`ContainerTag`](../container_data/container_tag.md). |
-| radius |  float |  | The radius of the cylinder. |
-| height |  float |  | The height of the cylinder. |
-| rotation |  Dict[str, float] |  | The rotation of the cylinder in Euler angles relative to the parent object. |
-
-_Returns:_  The ID of the container shape.
-
-#### add_sphere
-
-**`self.add_sphere(object_id, position, tag, radius)`**
-
-Add a sphere container shape to an object.
-
-
-| Parameter | Type | Default | Description |
-| --- | --- | --- | --- |
-| object_id |  int |  | The ID of the object. |
-| position |  Dict[str, float] |  | The position of the sphere relative to the parent object. |
-| tag |  ContainerTag |  | The sphere's semantic [`ContainerTag`](../container_data/container_tag.md). |
-| radius |  float |  | The radius of the sphere. |
-
-_Returns:_  The ID of the container shape.
-
 #### reset
 
 **`self.reset()`**
diff --git a/Documentation/python/add_ons/replicant.md b/Documentation/python/add_ons/replicant.md
new file mode 100644
index 000000000..f079ae873
--- /dev/null
+++ b/Documentation/python/add_ons/replicant.md
@@ -0,0 +1,341 @@
+# Replicant
+
+`from tdw.add_ons.replicant import Replicant`
+
+A Replicant is a human-like agent that can interact with the scene with pseudo-physics behavior.
+
+When a Replicant collides with objects, it initiates a physics-driven collision. The Replicant's own movements are driven by non-physics animation.
+
+A Replicant can walk, turn, reach for positions or objects, grasp and drop objects, and turn its head to look around.
+
+***
+
+## Class Variables
+
+| Variable | Type | Description | Value |
+| --- | --- | --- | --- |
+| `LIBRARY_NAME` | str | The Replicants library file. You can override this to use a custom library (e.g. a local library). | `"replicants.json"` |
+
+***
+
+## Fields
+
+- `initial_position` The initial position of the Replicant.
+
+- `initial_rotation` The initial rotation of the Replicant.
+
+- `static` The [`ReplicantStatic`](../replicant/replicant_static.md) data.
+
+- `dynamic` The [`ReplicantDynamic`](../replicant/replicant_dynamic.md) data.
+
+- `replicant_id` The ID of this replicant.
+
+- `action` The Replicant's current [action](../replicant/actions/action.md). Can be None (no ongoing action).
+
+- `image_frequency` An [`ImageFrequency`](../replicant/image_frequency.md) value that sets how often images are captured.
+
+- `collision_detection` [The collision detection rules.](../replicant/collision_detection.md) This determines whether the Replicant will immediately stop moving or turning when it collides with something.
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+**`Replicant(position, rotation)`**
+
+**`Replicant(replicant_id=0, position, rotation, image_frequency=ImageFrequency.once, name="replicant_0")`**
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| replicant_id |  int  | 0 | The ID of the Replicant. |
+| position |  Union[Dict[str, float] |  | The position of the Replicant as an x, y, z dictionary or numpy array. If None, defaults to `{"x": 0, "y": 0, "z": 0}`. |
+| rotation |  Union[Dict[str, float] |  | The rotation of the Replicant in Euler angles (degrees) as an x, y, z dictionary or numpy array. If None, defaults to `{"x": 0, "y": 0, "z": 0}`. |
+| image_frequency |  ImageFrequency  | ImageFrequency.once | An [`ImageFrequency`](../replicant/image_frequency.md) value that sets how often images are captured. |
+| name |  str  | "replicant_0" | The name of the Replicant model. |
+
+***
+
+### Movement
+
+These actions move or turn the Replicant.
+
+#### turn_by
+
+**`self.turn_by(angle)`**
+
+Turn the Replicant by an angle.
+
+This is a non-animated action, meaning that the Replicant will immediately snap to the angle.
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| angle |  float |  | The target angle in degrees. Positive value = clockwise turn. |
+
+#### turn_to
+
+**`self.turn_to(target)`**
+
+Turn the Replicant to face a target object or position.
+
+This is a non-animated action, meaning that the Replicant will immediately snap to the angle.
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| target |  Union[int, Dict[str, float] |  | The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array. |
+
+#### move_by
+
+**`self.move_by(distance)`**
+
+**`self.move_by(distance, reset_arms=True, reset_arms_duration=0.25, arrived_at=0.1, max_walk_cycles=100)`**
+
+Walk a given distance.
+
+The Replicant will continuously play a walk cycle animation until the action ends.
+
+The action can end for several reasons depending on the collision detection rules (see [`self.collision_detection`](../replicant/collision_detection.md).
+
+- If the Replicant walks the target distance, the action succeeds.
+- If `collision_detection.previous_was_same == True`, and the previous action was `move_by()` or `move_to()`, and it was in the same direction (forwards/backwards), and the previous action ended in failure, this action ends immediately.
+- If `self.collision_detection.avoid_obstacles == True` and the Replicant encounters a wall or object in its path:
+- If the object is in `self.collision_detection.exclude_objects`, the Replicant ignores it.
+- Otherwise, the action ends in failure.
+- If the Replicant collides with an object or a wall and `self.collision_detection.objects == True` and/or `self.collision_detection.walls == True` respectively:
+- If the object is in `self.collision_detection.exclude_objects`, the Replicant ignores it.
+- Otherwise, the action ends in failure.
+- If the Replicant takes too long to reach the target distance, the action ends in failure (see `self.max_walk_cycles`).
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| distance |  float |  | The target distance. If less than 0, the Replicant will walk backwards. |
+| reset_arms |  bool  | True | If True, reset the arms to their neutral positions while beginning the walk cycle. |
+| reset_arms_duration |  float  | 0.25 | The speed at which the arms are reset in seconds. |
+| arrived_at |  float  | 0.1 | If at any point during the action the difference between the target distance and distance traversed is less than this, then the action is successful. |
+| max_walk_cycles |  int  | 100 | The walk animation will loop this many times maximum. If by that point the Replicant hasn't reached its destination, the action fails. |
+
+#### move_to
+
+**`self.move_to(target)`**
+
+**`self.move_to(target, reset_arms=True, reset_arms_duration=0.25, arrived_at=0.1, max_walk_cycles=100, bounds_position="center")`**
+
+Turn the Replicant to a target position or object and then walk to it.
+
+While walking, the Replicant will continuously play a walk cycle animation until the action ends.
+
+The action can end for several reasons depending on the collision detection rules (see [`self.collision_detection`](../replicant/collision_detection.md).
+
+- If the Replicant walks the target distance, the action succeeds.
+- If `collision_detection.previous_was_same == True`, and the previous action was `move_by()` or `move_to()`, and it was in the same direction (forwards/backwards), and the previous action ended in failure, this action ends immediately.
+- If `self.collision_detection.avoid_obstacles == True` and the Replicant encounters a wall or object in its path:
+- If the object is in `self.collision_detection.exclude_objects`, the Replicant ignores it.
+- Otherwise, the action ends in failure.
+- If the Replicant collides with an object or a wall and `self.collision_detection.objects == True` and/or `self.collision_detection.walls == True` respectively:
+- If the object is in `self.collision_detection.exclude_objects`, the Replicant ignores it.
+- Otherwise, the action ends in failure.
+- If the Replicant takes too long to reach the target distance, the action ends in failure (see `self.max_walk_cycles`).
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| target |  Union[int, Dict[str, float] |  | The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array. |
+| reset_arms |  bool  | True | If True, reset the arms to their neutral positions while beginning the walk cycle. |
+| reset_arms_duration |  float  | 0.25 | The speed at which the arms are reset in seconds. |
+| arrived_at |  float  | 0.1 | If at any point during the action the difference between the target distance and distance traversed is less than this, then the action is successful. |
+| max_walk_cycles |  int  | 100 | The walk animation will loop this many times maximum. If by that point the Replicant hasn't reached its destination, the action fails. |
+| bounds_position |  str  | "center" | If `target` is an integer object ID, move towards this bounds point of the object. Options: `"center"`, `"top`", `"bottom"`, `"left"`, `"right"`, `"front"`, `"back"`. |
+
+***
+
+### Arm Articulation
+
+These actions move and bend the joints of the Replicant's arms.
+
+#### reach_for
+
+**`self.reach_for(target, arm)`**
+
+**`self.reach_for(target, arm, absolute=True, offhand_follows=False, arrived_at=0.09, max_distance=1.5, duration=0.25)`**
+
+Reach for a target object or position. One or both hands can reach for the target at the same time.
+
+If target is an object, the target position is a point on the object.
+If the object has affordance points, the target position is the affordance point closest to the hand.
+Otherwise, the target position is the bounds position closest to the hand.
+
+The Replicant's arm(s) will continuously over multiple `communicate()` calls move until either the motion is complete or the arm collides with something (see `self.collision_detection`).
+
+- If the hand is near the target at the end of the action, the action succeeds.
+- If the target is too far away at the start of the action, the action fails.
+- The collision detection will respond normally to walls, objects, obstacle avoidance, etc.
+- If `self.collision_detection.previous_was_same == True`, and if the previous action was a subclass of `ArmMotion`, and it ended in a collision, this action ends immediately.
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| target |  Union[int, Dict[str, float] |  | The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array. |
+| arm |  Union[Arm, List[Arm] |  | The [`Arm`](../replicant/arm.md) value(s) that will reach for the `target` as a single value or a list. Example: `Arm.left` or `[Arm.left, Arm.right]`. |
+| absolute |  bool  | True | If True, the target position is in world space coordinates. If False, the target position is relative to the Replicant. Ignored if `target` is an int. |
+| offhand_follows |  bool  | False | If True, the offhand will follow the primary hand, meaning that it will maintain the same relative position. Ignored if `arm` is a list or `target` is an int. |
+| arrived_at |  float  | 0.09 | If at the end of the action the hand(s) is this distance or less from the target position, the action succeeds. |
+| max_distance |  float  | 1.5 | The maximum distance from the hand to the target position. |
+| duration |  float  | 0.25 | The duration of the motion in seconds. |
+
+#### reset_arm
+
+**`self.reset_arm(arm)`**
+
+**`self.reset_arm(arm, duration=0.25)`**
+
+Move arm(s) back to rest position(s). One or both arms can be reset at the same time.
+
+The Replicant's arm(s) will continuously over multiple `communicate()` calls move until either the motion is complete or the arm collides with something (see `self.collision_detection`).
+
+- The collision detection will respond normally to walls, objects, obstacle avoidance, etc.
+- If `self.collision_detection.previous_was_same == True`, and if the previous action was an arm motion, and it ended in a collision, this action ends immediately.
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| arm |  Union[Arm, List[Arm] |  | The [`Arm`](../replicant/arm.md) value(s) that will reach for the `target` as a single value or a list. Example: `Arm.left` or `[Arm.left, Arm.right]`. |
+| duration |  float  | 0.25 | The duration of the motion in seconds. |
+
+***
+
+### Object Interaction
+
+These actions involve interaction with other objects, e.g. grasping or dropping.
+
+#### grasp
+
+**`self.grasp(target, arm)`**
+
+**`self.grasp(target, arm, angle=90, axis="pitch")`**
+
+Grasp a target object.
+
+The action fails if the hand is already holding an object. Otherwise, the action succeeds.
+
+When an object is grasped, it is made kinematic. Any objects contained by the object are parented to it and also made kinematic. For more information regarding containment in TDW, [read this](../../lessons/semantic_states/containment.md).
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| target |  int |  | The target object ID. |
+| arm |  Arm |  | The [`Arm`](../replicant/arm.md) value for the hand that will grasp the target object. |
+| angle |  Optional[float] | 90 | Continuously (per `communicate()` call, including after this action ends), rotate the the grasped object by this many degrees relative to the hand. If None, the grasped object will maintain its initial rotation. |
+| axis |  Optional[str] | "pitch" | Continuously (per `communicate()` call, including after this action ends) rotate the grasped object around this axis relative to the hand. Options: `"pitch"`, `"yaw"`, `"roll"`. If None, the grasped object will maintain its initial rotation. |
+
+#### drop
+
+**`self.drop(arm)`**
+
+**`self.drop(arm, max_num_frames=100)`**
+
+Drop a held target object.
+
+The action ends when the object stops moving or the number of consecutive `communicate()` calls since dropping the object exceeds `self.max_num_frames`.
+
+When an object is dropped, it is made non-kinematic. Any objects contained by the object are parented to it and also made non-kinematic. For more information regarding containment in TDW, [read this](../../lessons/semantic_states/containment.md).
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| arm |  Arm |  | The [`Arm`](../replicant/arm.md) holding the object. |
+| max_num_frames |  int  | 100 | Wait this number of `communicate()` calls maximum for the object to stop moving before ending the action. |
+
+***
+
+### Head
+
+These actions rotate the Replicant's head.
+
+#### look_at
+
+**`self.look_at()`**
+
+**`self.look_at(target=target, duration=0.1)`**
+
+Look at a target object or position.
+
+The head will continuously move over multiple `communicate()` calls until it is looking at the target.
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| target |  Union[int, np.ndarray, Dict[str, float] | target | The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array. |
+| duration |  float  | 0.1 | The duration of the motion in seconds. |
+
+#### reset_head
+
+**`self.reset_head()`**
+
+**`self.reset_head(duration=0.1)`**
+
+Reset the head to its neutral rotation.
+
+The head will continuously move over multiple `communicate()` calls until it is at its neutral rotation.
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| duration |  float  | 0.1 | The duration of the motion in seconds. |
+
+***
+
+### Animation
+
+These actions play arbitrary humanoid animations.
+
+#### animate
+
+**`self.animate(animation)`**
+
+**`self.animate(animation, library="humanoid_animations.json")`**
+
+Play an animation.
+
+The animation will end either when the animation clip is finished or if the Replicant collides with something (see [`self.collision_detection`](../replicant/collision_detection.md)).
+
+- The collision detection will respond normally to walls, objects, obstacle avoidance, etc.
+- If `self.collision_detection.previous_was_same == True`, and it was the same animation, and it ended in a collision, this action ends immediately.
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| animation |  str |  | The name of the animation. |
+| library |  str  | "humanoid_animations.json" | The animation library. |
+
+***
+
+### Misc.
+
+Misc. non-action functions.
+
+#### get_initialization_commands
+
+**`self.get_initialization_commands()`**
+
+This function gets called exactly once per add-on. To re-initialize, set `self.initialized = False`.
+
+_Returns:_  A list of commands that will initialize this add-on.
+
+#### on_send
+
+**`self.on_send(resp)`**
+
+This is called within `Controller.communicate(commands)` after commands are sent to the build and a response is received.
+
+Use this function to send commands to the build on the next `Controller.communicate(commands)` call, given the `resp` response.
+Any commands in the `self.commands` list will be sent on the *next* `Controller.communicate(commands)` call.
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+
+#### reset
+
+**`self.reset(position, rotation)`**
+
+Reset the Replicant. Call this when you reset the scene.
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| position |  Union[Dict[str, float] |  | The position of the Replicant as an x, y, z dictionary or numpy array. If None, defaults to `{"x": 0, "y": 0, "z": 0}`. |
+| rotation |  Union[Dict[str, float] |  | The rotation of the Replicant in Euler angles (degrees) as an x, y, z dictionary or numpy array. If None, defaults to `{"x": 0, "y": 0, "z": 0}`. |
+
+***
+
diff --git a/Documentation/python/container_data/containment_event.md b/Documentation/python/container_data/containment_event.md
index 6a9b85064..1437a8ef7 100644
--- a/Documentation/python/container_data/containment_event.md
+++ b/Documentation/python/container_data/containment_event.md
@@ -25,6 +25,6 @@ Data describing a containment event i.e. when a container shape overlaps with on
 | Parameter | Type | Default | Description |
 | --- | --- | --- | --- |
 | container_id |  int |  | The ID of the container. |
-| object_ids |  np.array |  | The IDs of the contained objects as a numpy array. |
+| object_ids |  np.ndarray |  | The IDs of the contained objects as a numpy array. |
 | tag |  ContainerTag |  | A semantic [`ContainerTag`](container_tag.md) describing the semantic nature of the event. |
 
diff --git a/Documentation/python/replicant/action_status.md b/Documentation/python/replicant/action_status.md
new file mode 100644
index 000000000..8c265fb75
--- /dev/null
+++ b/Documentation/python/replicant/action_status.md
@@ -0,0 +1,21 @@
+# ActionStatus
+
+`from tdw.replicant.action_status import ActionStatus`
+
+The status of the Replicant after doing an action.
+
+| Value | Description |
+| --- | --- |
+| `ongoing` | The action is ongoing. |
+| `failure` | Generic failure code (useful for custom APIs). |
+| `success` | The action was successful. |
+| `failed_to_move` | Tried to move to a target position or object but failed. |
+| `failed_to_turn` | Tried to turn but failed to align with the target angle, position, or object. |
+| `cannot_reach` | Didn't try to reach for the target position because it can't. |
+| `failed_to_reach` | Tried to reach for the target but failed; the magnet isn't close to the target. |
+| `not_holding` | Didn't try to drop the object(s) because it isn't holding them. |
+| `collision` | Tried to move or turn but failed because it collided with something. |
+| `detected_obstacle` | Detected an obstacle in its path. |
+| `already_holding` | Already holding the object. |
+| `still_dropping` | Dropped an object but, after many `communicate()` calls, the object is still moving. |
+| `cannot_grasp` | Didn't try to grasp the object because it's of an invalid type (e.g. a kinematic object). |
\ No newline at end of file
diff --git a/Documentation/python/replicant/actions/action.md b/Documentation/python/replicant/actions/action.md
new file mode 100644
index 000000000..301764748
--- /dev/null
+++ b/Documentation/python/replicant/actions/action.md
@@ -0,0 +1,69 @@
+# Action
+
+`from tdw.replicant.actions.action import Action`
+
+An action that the Replicant can do. An action is first initialized, has an ongoing state, and an end state.
+An action also has a status indicating whether it's ongoing, succeeded, or failed; and if it failed, why.
+
+***
+
+## Fields
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+**`Action()`**
+
+(no parameters)
+
+#### get_initialization_commands
+
+**`self.get_initialization_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands to initialize this action.
+
+#### get_ongoing_commands
+
+**`self.get_ongoing_commands(resp, static, dynamic)`**
+
+Evaluate an action per-frame to determine whether it's done.
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+
+_Returns:_  A list of commands to send to the build to continue the action.
+
+#### get_end_commands
+
+**`self.get_end_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands that must be sent to end any action.
\ No newline at end of file
diff --git a/Documentation/python/replicant/actions/animate.md b/Documentation/python/replicant/actions/animate.md
new file mode 100644
index 000000000..2525ffd94
--- /dev/null
+++ b/Documentation/python/replicant/actions/animate.md
@@ -0,0 +1,85 @@
+# Animate
+
+`from tdw.replicant.actions.animate import Animate`
+
+Play an animation.
+
+The animation will end either when the animation clip is finished or if the Replicant collides with something (see `self.collision_detection`).
+
+- The collision detection will respond normally to walls, objects, obstacle avoidance, etc.
+- If `self.collision_detection.previous_was_same == True`, and it was the same animation, and it ended in a collision, this action ends immediately.
+
+***
+
+## Fields
+
+- `record` The `HumanoidAnimationRecord` of the animation.
+
+- `collision_detection` The [`CollisionDetection`](../collision_detection.md) rules.
+
+- `forward` If True, play the animation forwards. If False, play the animation backwards.
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+**`Animate(animation, collision_detection, forward, library, previous)`**
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| animation |  str |  | The name of the animation. |
+| collision_detection |  CollisionDetection |  | The [`CollisionDetection`](../collision_detection.md) rules. |
+| forward |  bool |  | If True, play the animation forwards. If False, play the animation backwards. |
+| library |  str |  | The name animation library. |
+| previous |  Optional[Action] |  | The previous action. Can be None. |
+
+#### get_initialization_commands
+
+**`self.get_initialization_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands to initialize this action.
+
+#### get_ongoing_commands
+
+**`self.get_ongoing_commands(resp, static, dynamic)`**
+
+Evaluate an action per-frame to determine whether it's done.
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+
+_Returns:_  A list of commands to send to the build to continue the action.
+
+#### get_end_commands
+
+**`self.get_end_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands that must be sent to end any action.
\ No newline at end of file
diff --git a/Documentation/python/replicant/actions/arm_motion.md b/Documentation/python/replicant/actions/arm_motion.md
new file mode 100644
index 000000000..5323cb70b
--- /dev/null
+++ b/Documentation/python/replicant/actions/arm_motion.md
@@ -0,0 +1,87 @@
+# ArmMotion
+
+`from tdw.replicant.actions.arm_motion import ArmMotion`
+
+Abstract base class for actions related to Replicant arm motion.
+
+Duration an arm motion, the Replicant's arm(s) will continuously over multiple `communicate()` calls move until either the motion is complete or the arm collides with something (see `self.collision_detection`).
+
+- The collision detection will respond normally to walls, objects, obstacle avoidance, etc.
+- If `self.collision_detection.previous_was_same == True`, and if the previous action was a subclass of `ArmMotion`, and it ended in a collision, this action ends immediately.
+
+***
+
+## Fields
+
+- `arms` A list of [`Arm`](../arm.md) values that will reach for the `target`. Example: `[Arm.left, Arm.right]`.
+
+- `collision_detection` The [`CollisionDetection`](../collision_detection.md) rules.
+
+- `duration` The duration of the motion in seconds.
+
+- `collisions` If the action fails in a collision, this is a list of arms that collided with something.
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+**`ArmMotion(arms, dynamic, duration, collision_detection, previous)`**
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| arms |  List[Arm] |  | A list of [`Arm`](../arm.md) values that will reach for the `target`. Example: `[Arm.left, Arm.right]`. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| duration |  float |  | The duration of the motion in seconds. |
+| collision_detection |  CollisionDetection |  | The [`CollisionDetection`](../collision_detection.md) rules. |
+| previous |  Optional[Action] |  | The previous action. Can be None. |
+
+#### get_initialization_commands
+
+**`self.get_initialization_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands to initialize this action.
+
+#### get_ongoing_commands
+
+**`self.get_ongoing_commands(resp, static, dynamic)`**
+
+Evaluate an action per-frame to determine whether it's done.
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+
+_Returns:_  A list of commands to send to the build to continue the action.
+
+#### get_end_commands
+
+**`self.get_end_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands that must be sent to end any action.
\ No newline at end of file
diff --git a/Documentation/python/replicant/actions/drop.md b/Documentation/python/replicant/actions/drop.md
new file mode 100644
index 000000000..092a788ef
--- /dev/null
+++ b/Documentation/python/replicant/actions/drop.md
@@ -0,0 +1,86 @@
+# Drop
+
+`from tdw.replicant.actions.drop import Drop`
+
+Drop a held object.
+
+The action ends when the object stops moving or the number of consecutive `communicate()` calls since dropping the object exceeds `self.max_num_frames`.
+
+When an object is dropped, it is made non-kinematic. Any objects contained by the object are parented to it and also made non-kinematic. For more information regarding containment in TDW, [read this](../../../lessons/semantic_states/containment.md).
+
+***
+
+## Fields
+
+- `arm` The [`Arm`](../arm.md) holding the object.
+
+- `object_id` The ID of the held object.
+
+- `object_position` The current position of the object.
+
+- `max_num_frames` Wait this number of `communicate()` calls maximum for the object to stop moving before ending the action.
+
+- `frame_count` The current frame.
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+**`Drop(arm, dynamic, max_num_frames)`**
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| arm |  Arm |  | The [`Arm`](../arm.md) holding the object. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| max_num_frames |  int |  | Wait this number of `communicate()` calls maximum for the object to stop moving before ending the action. |
+
+#### get_initialization_commands
+
+**`self.get_initialization_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands to initialize this action.
+
+#### get_ongoing_commands
+
+**`self.get_ongoing_commands(resp, static, dynamic)`**
+
+Evaluate an action per-frame to determine whether it's done.
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+
+_Returns:_  A list of commands to send to the build to continue the action.
+
+#### get_end_commands
+
+**`self.get_end_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands that must be sent to end any action.
\ No newline at end of file
diff --git a/Documentation/python/replicant/actions/grasp.md b/Documentation/python/replicant/actions/grasp.md
new file mode 100644
index 000000000..33350a88c
--- /dev/null
+++ b/Documentation/python/replicant/actions/grasp.md
@@ -0,0 +1,86 @@
+# Grasp
+
+`from tdw.replicant.actions.grasp import Grasp`
+
+Grasp a target object.
+
+The action fails if the hand is already holding an object. Otherwise, the action succeeds.
+
+When an object is grasped, it is made kinematic. Any objects contained by the object are parented to it and also made kinematic. For more information regarding containment in TDW, [read this](../../../lessons/semantic_states/containment.md).
+
+***
+
+## Fields
+
+- `target` The target object ID.
+
+- `arm` The [`Arm`](../arm.md) value for the hand that will grasp the target object.
+
+- `angle` Continuously (per `communicate()` call, including after this action ends), rotate the grasped object by this many degrees. If None, the grasped object will maintain its initial rotation.
+
+- `axis` Continuously (per `communicate()` call, including after this action ends), rotate the grasped object around this axis. Options: `"pitch"`, `"yaw"`, `"roll"`. If None, the grasped object will maintain its initial rotation.
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+**`Grasp(target, arm, dynamic, angle, axis)`**
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| target |  int |  | The target object ID. |
+| arm |  Arm |  | The [`Arm`](../arm.md) value for the hand that will grasp the target object. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| angle |  Optional[float] |  | Continuously (per `communicate()` call, including after this action ends), rotate the the grasped object by this many degrees relative to the hand. If None, the grasped object will maintain its initial rotation. |
+| axis |  Optional[str] |  | Continuously (per `communicate()` call, including after this action ends) rotate the grasped object around this axis relative to the hand. Options: `"pitch"`, `"yaw"`, `"roll"`. If None, the grasped object will maintain its initial rotation. |
+
+#### get_initialization_commands
+
+**`self.get_initialization_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands to initialize this action.
+
+#### get_ongoing_commands
+
+**`self.get_ongoing_commands(resp, static, dynamic)`**
+
+Evaluate an action per-frame to determine whether it's done.
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+
+_Returns:_  A list of commands to send to the build to continue the action.
+
+#### get_end_commands
+
+**`self.get_end_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands that must be sent to end any action.
\ No newline at end of file
diff --git a/Documentation/python/replicant/actions/head_motion.md b/Documentation/python/replicant/actions/head_motion.md
new file mode 100644
index 000000000..87cfb791b
--- /dev/null
+++ b/Documentation/python/replicant/actions/head_motion.md
@@ -0,0 +1,74 @@
+# HeadMotion
+
+`from tdw.replicant.actions.head_motion import HeadMotion`
+
+Abstract base class for actions that rotate the Replicant's head.
+
+Duration an head motion, the Replicant's head will continuously move over multiple `communicate()` calls until either the motion is complete.
+
+***
+
+## Fields
+
+- `duration` The duration of the motion in seconds.
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+**`HeadMotion(duration)`**
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| duration |  float |  | The duration of the motion in seconds. |
+
+#### get_initialization_commands
+
+**`self.get_initialization_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands to initialize this action.
+
+#### get_ongoing_commands
+
+**`self.get_ongoing_commands(resp, static, dynamic)`**
+
+Evaluate an action per-frame to determine whether it's done.
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+
+_Returns:_  A list of commands to send to the build to continue the action.
+
+#### get_end_commands
+
+**`self.get_end_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands that must be sent to end any action.
\ No newline at end of file
diff --git a/Documentation/python/replicant/actions/look_at.md b/Documentation/python/replicant/actions/look_at.md
new file mode 100644
index 000000000..04261fda7
--- /dev/null
+++ b/Documentation/python/replicant/actions/look_at.md
@@ -0,0 +1,83 @@
+# LookAt
+
+`from tdw.replicant.actions.look_at import LookAt`
+
+Look at a target object or position.
+
+The head will continuously move over multiple `communicate()` calls until it is looking at the target.
+
+***
+
+## Fields
+
+- `target` The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array.
+
+- `duration` The duration of the motion in seconds.
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+**`LookAt(target, duration)`**
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| target |  Union[int, np.ndarray, Dict[str, float] |  | The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array. |
+| duration |  float |  | The duration of the motion in seconds. |
+
+#### get_initialization_commands
+
+**`self.get_initialization_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands to initialize this action.
+
+#### get_ongoing_commands
+
+**`self.get_ongoing_commands(resp, static, dynamic)`**
+
+Evaluate an action per-frame to determine whether it's done.
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+
+_Returns:_  A list of commands to send to the build to continue the action.
+
+#### get_end_commands
+
+**`self.get_end_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands that must be sent to end any action.
\ No newline at end of file
diff --git a/Documentation/python/replicant/actions/move_by.md b/Documentation/python/replicant/actions/move_by.md
new file mode 100644
index 000000000..4b57ec247
--- /dev/null
+++ b/Documentation/python/replicant/actions/move_by.md
@@ -0,0 +1,123 @@
+# MoveBy
+
+`from tdw.replicant.actions.move_by import MoveBy`
+
+Walk a given distance.
+
+The Replicant will continuously play a walk cycle animation until the action ends.
+
+The action can end for several reasons depending on the collision detection rules (see [`self.collision_detection`](../collision_detection.md).
+
+- If the Replicant walks the target distance, the action succeeds.
+- If `self.collision_detection.previous_was_same == True`, and the previous action was `MoveBy` or `MoveTo`, and it was in the same direction (forwards/backwards), and the previous action ended in failure, this action ends immediately.
+- If `self.collision_detection.avoid_obstacles == True` and the Replicant encounters a wall or object in its path:
+  - If the object is in `self.collision_detection.exclude_objects`, the Replicant ignores it.
+  - Otherwise, the action ends in failure.
+- If the Replicant collides with an object or a wall and `self.collision_detection.objects == True` and/or `self.collision_detection.walls == True` respectively:
+  - If the object is in `self.collision_detection.exclude_objects`, the Replicant ignores it.
+  - Otherwise, the action ends in failure.
+- If the Replicant takes too long to reach the target distance, the action ends in failure (see `self.max_walk_cycles`).
+
+***
+
+## Class Variables
+
+| Variable | Type | Description | Value |
+| --- | --- | --- | --- |
+| `OVERLAP_HALF_EXTENTS` | Dict[str, float] | While walking, the Replicant will cast an overlap shape in front of or behind it, depending on whether it is walking forwards or backwards. The overlap is used to detect object prior to collision (see `self.collision_detection.avoid_obstacles`). These are the half-extents of the overlap shape. | `{"x": 0.31875, "y": 0.8814, "z": 0.0875}` |
+
+***
+
+## Fields
+
+- `distance` The target distance. If less than 0, the Replicant will walk backwards.
+
+- `reset_arms` If True, reset the arms to their neutral positions while beginning the walk cycle.
+
+- `reset_arms_duration` The speed at which the arms are reset in seconds.
+
+- `arrived_at` If at any point during the action the difference between the target distance and distance traversed is less than this, then the action is successful.
+
+- `max_walk_cycles` The walk animation will loop this many times maximum. If by that point the Replicant hasn't reached its destination, the action fails.
+
+- `walk_cycle` The current walk cycle.
+
+- `record` The `HumanoidAnimationRecord` of the animation.
+
+- `collision_detection` The [`CollisionDetection`](../collision_detection.md) rules.
+
+- `forward` If True, play the animation forwards. If False, play the animation backwards.
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+**`MoveBy(distance, dynamic, collision_detection, previous, reset_arms, reset_arms_duration, arrived_at, max_walk_cycles)`**
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| distance |  float |  | The target distance. If less than 0, the Replicant will walk backwards. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| collision_detection |  CollisionDetection |  | The [`CollisionDetection`](../collision_detection.md) rules. |
+| previous |  Optional[Action] |  | The previous action, if any. |
+| reset_arms |  bool |  | If True, reset the arms to their neutral positions while beginning the walk cycle. |
+| reset_arms_duration |  float |  | The speed at which the arms are reset in seconds. |
+| arrived_at |  float |  | If at any point during the action the difference between the target distance and distance traversed is less than this, then the action is successful. |
+| max_walk_cycles |  int |  | The walk animation will loop this many times maximum. If by that point the Replicant hasn't reached its destination, the action fails. |
+
+#### get_initialization_commands
+
+**`self.get_initialization_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands to initialize this action.
+
+#### get_ongoing_commands
+
+**`self.get_ongoing_commands(resp, static, dynamic)`**
+
+Evaluate an action per-frame to determine whether it's done.
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+
+_Returns:_  A list of commands to send to the build to continue the action.
+
+#### get_end_commands
+
+**`self.get_end_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands that must be sent to end any action.
\ No newline at end of file
diff --git a/Documentation/python/replicant/actions/move_to.md b/Documentation/python/replicant/actions/move_to.md
new file mode 100644
index 000000000..3620a93cf
--- /dev/null
+++ b/Documentation/python/replicant/actions/move_to.md
@@ -0,0 +1,105 @@
+# MoveTo
+
+`from tdw.replicant.actions.move_to import MoveTo`
+
+Turn the Replicant to a target position or object and then walk to it.
+
+While walking, the Replicant will continuously play a walk cycle animation until the action ends.
+
+The action can end for several reasons depending on the collision detection rules (see [`self.collision_detection`](../collision_detection.md).
+
+- If the Replicant walks the target distance (i.e. it reaches its target), the action succeeds.
+- If `self.collision_detection.previous_was_same == True`, and the previous action was `MoveBy` or `MoveTo`, and it was in the same direction (forwards/backwards), and the previous action ended in failure, this action ends immediately.
+- If `self.collision_detection.avoid_obstacles == True` and the Replicant encounters a wall or object in its path:
+  - If the object is in `self.collision_detection.exclude_objects`, the Replicant ignores it.
+  - Otherwise, the action ends in failure.
+- If the Replicant collides with an object or a wall and `self.collision_detection.objects == True` and/or `self.collision_detection.walls == True` respectively:
+  - If the object is in `self.collision_detection.exclude_objects`, the Replicant ignores it.
+  - Otherwise, the action ends in failure.
+- If the Replicant takes too long to reach the target distance, the action ends in failure (see `self.max_walk_cycles`).
+
+***
+
+## Fields
+
+- `target` The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array.
+
+- `collision_detection` The [`CollisionDetection`](../collision_detection.md) rules.
+
+- `reset_arms` If True, reset the arms to their neutral positions while beginning the walk cycle.
+
+- `reset_arms_duration` The speed at which the arms are reset in seconds.
+
+- `arrived_at` If at any point during the action the difference between the target distance and distance traversed is less than this, then the action is successful.
+
+- `max_walk_cycles` The walk animation will loop this many times maximum. If by that point the Replicant hasn't reached its destination, the action fails.
+
+- `bounds_position` If `target` is an integer object ID, move towards this bounds point of the object. Options: `"center"`, `"top`", `"bottom"`, `"left"`, `"right"`, `"front"`, `"back"`.
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+**`MoveTo(target, collision_detection, previous, reset_arms, reset_arms_duration, arrived_at, max_walk_cycles, bounds_position)`**
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| target |  Union[int, Dict[str, float] |  | The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array. |
+| collision_detection |  CollisionDetection |  | The [`CollisionDetection`](../collision_detection.md) rules. |
+| previous |  Optional[Action] |  | The previous action, if any. |
+| reset_arms |  bool |  | If True, reset the arms to their neutral positions while beginning the walk cycle. |
+| reset_arms_duration |  float |  | The speed at which the arms are reset in seconds. |
+| arrived_at |  float |  | If at any point during the action the difference between the target distance and distance traversed is less than this, then the action is successful. |
+| max_walk_cycles |  int |  | The walk animation will loop this many times maximum. If by that point the Replicant hasn't reached its destination, the action fails. |
+| bounds_position |  str |  | If `target` is an integer object ID, move towards this bounds point of the object. Options: `"center"`, `"top`", `"bottom"`, `"left"`, `"right"`, `"front"`, `"back"`. |
+
+#### get_initialization_commands
+
+**`self.get_initialization_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands to initialize this action.
+
+#### get_ongoing_commands
+
+**`self.get_ongoing_commands(resp, static, dynamic)`**
+
+Evaluate an action per-frame to determine whether it's done.
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+
+_Returns:_  A list of commands to send to the build to continue the action.
+
+#### get_end_commands
+
+**`self.get_end_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands that must be sent to end any action.
\ No newline at end of file
diff --git a/Documentation/python/replicant/actions/reach_for.md b/Documentation/python/replicant/actions/reach_for.md
new file mode 100644
index 000000000..b9ee1531f
--- /dev/null
+++ b/Documentation/python/replicant/actions/reach_for.md
@@ -0,0 +1,111 @@
+# ReachFor
+
+`from tdw.replicant.actions.reach_for import ReachFor`
+
+Reach for a target object or position. One or both hands can reach for the target at the same time.
+
+If target is an object, the target position is a point on the object.
+If the object has affordance points, the target position is the affordance point closest to the hand.
+Otherwise, the target position is the bounds position closest to the hand.
+
+The Replicant's arm(s) will continuously over multiple `communicate()` calls move until either the motion is complete or the arm collides with something (see `self.collision_detection`).
+
+- If the hand is near the target at the end of the action, the action succeeds.
+- If the target is too far away at the start of the action, the action fails.
+- The collision detection will respond normally to walls, objects, obstacle avoidance, etc.
+- If `self.collision_detection.previous_was_same == True`, and if the previous action was a subclass of `ArmMotion`, and it ended in a collision, this action ends immediately.
+
+***
+
+## Fields
+
+- `target` The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array.
+
+- `arrived_at` If the motion ends and the hand is this distance or less from the target, the action succeeds.
+
+- `max_distance` If the target is further away from this distance at the start of the action, the action fails.
+
+- `offhand_follows` If True, the offhand will follow the primary hand, meaning that it will maintain the same relative position. Ignored if `len(arms) > 1` or if `target` is an object ID.
+
+- `arms` A list of [`Arm`](../arm.md) values that will reach for the `target`. Example: `[Arm.left, Arm.right]`.
+
+- `collision_detection` The [`CollisionDetection`](../collision_detection.md) rules.
+
+- `duration` The duration of the motion in seconds.
+
+- `collisions` If the action fails in a collision, this is a list of arms that collided with something.
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+**`ReachFor(target, offhand_follows, arrived_at, max_distance, arms, dynamic, collision_detection, duration, previous)`**
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| target |  Union[int, np.ndarray, Dict[str, float] |  | The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array. |
+| offhand_follows |  bool |  | If True, the offhand will follow the primary hand, meaning that it will maintain the same relative position. Ignored if `len(arms) > 1` or if `target` is an object ID. |
+| arrived_at |  float |  | If the motion ends and the hand is this distance or less from the target, the action succeeds. |
+| max_distance |  float |  | If the target is further away from this distance at the start of the action, the action fails. |
+| arms |  List[Arm] |  | A list of [`Arm`](../arm.md) values that will reach for the `target`. Example: `[Arm.left, Arm.right]`. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| collision_detection |  CollisionDetection |  | The [`CollisionDetection`](../collision_detection.md) rules. |
+| duration |  float |  | The duration of the motion in seconds. |
+| previous |  Optional[Action] |  | The previous action. Can be None. |
+
+#### get_initialization_commands
+
+**`self.get_initialization_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands to initialize this action.
+
+#### get_ongoing_commands
+
+**`self.get_ongoing_commands(resp, static, dynamic)`**
+
+Evaluate an action per-frame to determine whether it's done.
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+
+_Returns:_  A list of commands to send to the build to continue the action.
+
+#### get_end_commands
+
+**`self.get_end_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands that must be sent to end any action.
\ No newline at end of file
diff --git a/Documentation/python/replicant/actions/reset_arm.md b/Documentation/python/replicant/actions/reset_arm.md
new file mode 100644
index 000000000..5262775e9
--- /dev/null
+++ b/Documentation/python/replicant/actions/reset_arm.md
@@ -0,0 +1,95 @@
+# ResetArm
+
+`from tdw.replicant.actions.reset_arm import ResetArm`
+
+Move arm(s) back to rest position(s). One or both arms can be reset at the same time.
+
+The Replicant's arm(s) will continuously over multiple `communicate()` calls move until either the motion is complete or the arm collides with something (see `self.collision_detection`).
+
+- The collision detection will respond normally to walls, objects, obstacle avoidance, etc.
+- If `self.collision_detection.previous_was_same == True`, and if the previous action was a subclass of `ArmMotion`, and it ended in a collision, this action ends immediately.
+
+***
+
+## Fields
+
+- `arms` A list of [`Arm`](../arm.md) values that will reach for the `target`. Example: `[Arm.left, Arm.right]`.
+
+- `collision_detection` The [`CollisionDetection`](../collision_detection.md) rules.
+
+- `duration` The duration of the motion in seconds.
+
+- `collisions` If the action fails in a collision, this is a list of arms that collided with something.
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+\_\_init\_\_
+
+**`ResetArm(arms, dynamic, duration, collision_detection, previous)`**
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| arms |  List[Arm] |  | A list of [`Arm`](../arm.md) values that will reach for the `target`. Example: `[Arm.left, Arm.right]`. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| duration |  float |  | The duration of the motion in seconds. |
+| collision_detection |  CollisionDetection |  | The [`CollisionDetection`](../collision_detection.md) rules. |
+| previous |  Optional[Action] |  | The previous action. Can be None. |
+
+#### get_initialization_commands
+
+**`self.get_initialization_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands to initialize this action.
+
+#### get_ongoing_commands
+
+**`self.get_ongoing_commands(resp, static, dynamic)`**
+
+Evaluate an action per-frame to determine whether it's done.
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+
+_Returns:_  A list of commands to send to the build to continue the action.
+
+#### get_end_commands
+
+**`self.get_end_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands that must be sent to end any action.
\ No newline at end of file
diff --git a/Documentation/python/replicant/actions/reset_head.md b/Documentation/python/replicant/actions/reset_head.md
new file mode 100644
index 000000000..52f492541
--- /dev/null
+++ b/Documentation/python/replicant/actions/reset_head.md
@@ -0,0 +1,80 @@
+# ResetHead
+
+`from tdw.replicant.actions.reset_head import ResetHead`
+
+Reset the head to its neutral rotation.
+
+The head will continuously move over multiple `communicate()` calls until it is at its neutral rotation.
+
+***
+
+## Fields
+
+- `duration` The duration of the motion in seconds.
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+**`ResetHead(duration)`**
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| duration |  float |  | The duration of the motion in seconds. |
+
+#### get_initialization_commands
+
+**`self.get_initialization_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands to initialize this action.
+
+#### get_ongoing_commands
+
+**`self.get_ongoing_commands(resp, static, dynamic)`**
+
+Evaluate an action per-frame to determine whether it's done.
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+
+_Returns:_  A list of commands to send to the build to continue the action.
+
+#### get_end_commands
+
+**`self.get_end_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands that must be sent to end any action.
\ No newline at end of file
diff --git a/Documentation/python/replicant/actions/turn_by.md b/Documentation/python/replicant/actions/turn_by.md
new file mode 100644
index 000000000..dd2937074
--- /dev/null
+++ b/Documentation/python/replicant/actions/turn_by.md
@@ -0,0 +1,72 @@
+# TurnBy
+
+`from tdw.replicant.actions.turn_by import TurnBy`
+
+Turn by an angle.
+
+This is a non-animated action, meaning that the Replicant will immediately snap to the angle.
+
+***
+
+## Fields
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+**`TurnBy( The angle to turn by, in degrees.)`**
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+|  The angle to turn by, in degrees. |  |  | :param: The angle to turn by, in degrees. |
+
+#### get_initialization_commands
+
+**`self.get_initialization_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands to initialize this action.
+
+#### get_ongoing_commands
+
+**`self.get_ongoing_commands(resp, static, dynamic)`**
+
+Evaluate an action per-frame to determine whether it's done.
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+
+_Returns:_  A list of commands to send to the build to continue the action.
+
+#### get_end_commands
+
+**`self.get_end_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands that must be sent to end any action.
\ No newline at end of file
diff --git a/Documentation/python/replicant/actions/turn_to.md b/Documentation/python/replicant/actions/turn_to.md
new file mode 100644
index 000000000..9152e8383
--- /dev/null
+++ b/Documentation/python/replicant/actions/turn_to.md
@@ -0,0 +1,72 @@
+# TurnTo
+
+`from tdw.replicant.actions.turn_to import TurnTo`
+
+Turn to a target object or position.
+
+This is a non-animated action, meaning that the Replicant will immediately snap to the angle.
+
+***
+
+## Fields
+
+- `status` [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+
+- `initialized` If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+
+- `done` If True, this action is done and won't send any more commands.
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+**`TurnTo(target)`**
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| target |  Union[int, Dict[str, float] |  | The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array. |
+
+#### get_initialization_commands
+
+**`self.get_initialization_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands to initialize this action.
+
+#### get_ongoing_commands
+
+**`self.get_ongoing_commands(resp, static, dynamic)`**
+
+Evaluate an action per-frame to determine whether it's done.
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+
+_Returns:_  A list of commands to send to the build to continue the action.
+
+#### get_end_commands
+
+**`self.get_end_commands(resp, static, dynamic, image_frequency)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build. |
+| static |  ReplicantStatic |  | The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized. |
+| dynamic |  ReplicantDynamic |  | The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call. |
+| image_frequency |  ImageFrequency |  | An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured. |
+
+_Returns:_  A list of commands that must be sent to end any action.
\ No newline at end of file
diff --git a/Documentation/python/replicant/arm.md b/Documentation/python/replicant/arm.md
new file mode 100644
index 000000000..b434fd13a
--- /dev/null
+++ b/Documentation/python/replicant/arm.md
@@ -0,0 +1,10 @@
+# Arm
+
+`from tdw.replicant.arm import Arm`
+
+An enum that defines the side that an arm is on.
+
+| Value | Description |
+| --- | --- |
+| `left` | The left arm. |
+| `right` | The right arm. |
\ No newline at end of file
diff --git a/Documentation/python/replicant/collision_detection.md b/Documentation/python/replicant/collision_detection.md
new file mode 100644
index 000000000..83aa92e1f
--- /dev/null
+++ b/Documentation/python/replicant/collision_detection.md
@@ -0,0 +1,38 @@
+# CollisionDetection
+
+`from tdw.replicant.collision_detection import CollisionDetection`
+
+Parameters for how a Replicant handles collision detection.
+
+***
+
+## Fields
+
+- `objects` If True, the Replicant will stop when it collides with an object unless the is in the `exclude_objects`.
+
+- `avoid` If True, while walking, the Replicant will try to stop *before* colliding with objects.
+
+- `held` If True, ignore collisions between a held object and hand + lower arm holding the object.
+
+- `exclude_objects` The Replicant will ignore a collision with any object in this list, *regardless* of whether or not `objects == True` or the mass of the object. Can be None.
+
+- `previous_was_same` If True, the Replicant will stop if the previous action resulted in a collision and was the same sort of action as the current one.
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+**`CollisionDetection()`**
+
+**`CollisionDetection(objects=True, avoid=True, held=True, exclude_objects=None, previous_was_same=True)`**
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| objects |  bool  | True | If True, the Replicant will stop when it collides with an object unless the is in the `exclude_objects`. |
+| avoid |  bool  | True | If True, while walking, the Replicant will try to stop *before* colliding with objects. |
+| held |  bool  | True | If True, ignore collisions between a held object and hand + lower arm holding the object. |
+| exclude_objects |  List[int] | None | The Replicant will ignore a collision with any object in this list, *regardless* of whether or not `objects == True`. Can be None. |
+| previous_was_same |  bool  | True | If True, the Replicant will stop if the previous action resulted in a collision and was the same sort of action as the current one. |
+
diff --git a/Documentation/python/replicant/image_frequency.md b/Documentation/python/replicant/image_frequency.md
new file mode 100644
index 000000000..a32655ee3
--- /dev/null
+++ b/Documentation/python/replicant/image_frequency.md
@@ -0,0 +1,11 @@
+# ImageFrequency
+
+`from tdw.replicant.image_frequency import ImageFrequency`
+
+The per-frame frequency of image capture.
+
+| Value | Description |
+| --- | --- |
+| `once` | Capture an image only on this `communicate()` call. |
+| `always` | Capture images on every `communicate()` call. |
+| `never` | Don't capture images on any `communicate()` call. |
\ No newline at end of file
diff --git a/Documentation/python/replicant/replicant_body_part.md b/Documentation/python/replicant/replicant_body_part.md
new file mode 100644
index 000000000..2ad889997
--- /dev/null
+++ b/Documentation/python/replicant/replicant_body_part.md
@@ -0,0 +1,24 @@
+# ReplicantBodyPart
+
+`from tdw.replicant.replicant_body_part import ReplicantBodyPart`
+
+The name of each Replicant body part.
+
+| Value | Description |
+| --- | --- |
+| `hand_l` |  |
+| `hand_r` |  |
+| `head` |  |
+| `lowerarm_l` |  |
+| `lowerarm_r` |  |
+| `lowerleg_l` |  |
+| `lowerleg_r` |  |
+| `spine_01` |  |
+| `spine_02` |  |
+| `spine_03` |  |
+| `upperarm_l` |  |
+| `upperarm_r` |  |
+| `upperleg_l` |  |
+| `upperleg_r` |  |
+| `# The body part enum values in a list.` | The body part enum values in a list. |
+| `BODY_PARTS: List[ReplicantBodyPart]` |  |
\ No newline at end of file
diff --git a/Documentation/python/replicant/replicant_dynamic.md b/Documentation/python/replicant/replicant_dynamic.md
new file mode 100644
index 000000000..fed5bed5a
--- /dev/null
+++ b/Documentation/python/replicant/replicant_dynamic.md
@@ -0,0 +1,67 @@
+# ReplicantDynamic
+
+`from tdw.replicant.replicant_dynamic import ReplicantDynamic`
+
+Dynamic data for a replicant that can change per `communicate()` call (such as the position of the Replicant).
+
+***
+
+## Fields
+
+- `transform` The [`Transform`](../object_data/transform.md) of the Replicant.
+
+- `held_objects` A dictionary of objects held in each hand. Key = [`Arm`](arm.md). Value = Object ID.
+
+- `images` The images rendered by the robot as dictionary. Key = the name of the pass. Value = the pass as a numpy array.
+
+- `projection_matrix` The [camera projection matrix](../../api/output_data.md#cameramatrices) of the Replicant's camera as a numpy array.
+
+- `camera_matrix` The [camera matrix](../../api/output_data.md#cameramatrices) of the Replicant's camera as a numpy array.
+
+- `got_images` If True, we got images from the output data.
+
+- `body_parts` Transform data for each body part. Key = Body part ID. Value = [`Transform`](../object_data/transform.md).
+
+- `collisions` Collision data per body part. Key = Body part ID. Value = A list of object IDs that the body part collided with.
+
+- `output_data_status` This is meant for internal use only. For certain actions, the build will update the Replicant's `ActionStatus`. *Do not use this field to check the Replicant's status.* Always check `replicant.action.status` instead.
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+**`ReplicantDynamic(resp, replicant_id, frame_count)`**
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| resp |  List[bytes] |  | The response from the build, which we assume contains `replicant` output data. |
+| replicant_id |  int |  | The ID of this replicant. |
+| frame_count |  int |  | The current frame count. |
+
+#### save_images
+
+**`self.save_images(output_directory)`**
+
+Save the ID pass (segmentation colors) and the depth pass to disk.
+Images will be named: `[frame_number]_[pass_name].[extension]`
+For example, the depth pass on the first frame will be named: `00000000_depth.png`
+
+The `img` pass is either a .jpg. The `id` and `depth` passes are .png files.
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| output_directory |  Union[str, Path] |  | The directory that the images will be saved to. |
+
+#### get_collision_enters
+
+**`self.get_collision_enters(collision_detection)`**
+
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| collision_detection |  CollisionDetection |  | The [`CollisionDetection`](collision_detection.md) rules. |
+
+_Returns:_  A list of body IDs that entered a collision on this frame, filtered by the collision detection rules.
+
diff --git a/Documentation/python/replicant/replicant_static.md b/Documentation/python/replicant/replicant_static.md
new file mode 100644
index 000000000..e4dd256ae
--- /dev/null
+++ b/Documentation/python/replicant/replicant_static.md
@@ -0,0 +1,41 @@
+# ReplicantStatic
+
+`from tdw.replicant.replicant_static import ReplicantStatic`
+
+Static data for the Replicant.
+
+***
+
+## Class Variables
+
+| Variable | Type | Description | Value |
+| --- | --- | --- | --- |
+| `ARM_JOINTS` | Dict[Arm, List[ReplicantBodyPart]] | A dictionary of arms and their constituent joints. | `{Arm.left: [__b for __b in ReplicantBodyPart if __b.name.endswith("_l")],` |
+
+***
+
+## Fields
+
+- `replicant_id` The ID of the Replicant.
+
+- `avatar_id` The ID of the Replicant's avatar (camera). This is used internally for API calls.
+
+- `body_parts` Body parts by name. Key = [`ReplicantBodyPart`](replicant_body_part.md). Value = Object ID.
+
+- `hands` The Replicant's hands. Key = [`Arm`](arm.md). Value = Hand ID.
+
+- `body_parts_by_id` Body parts by ID. Key = Object ID. Value = [`ReplicantBodyPart`](replicant_body_part.md).
+
+***
+
+## Functions
+
+#### \_\_init\_\_
+
+**`ReplicantStatic(replicant_id, resp)`**
+
+| Parameter | Type | Default | Description |
+| --- | --- | --- | --- |
+| replicant_id |  int |  | The ID of the Replicant. |
+| resp |  List[bytes] |  | The response from the build. |
+
diff --git a/Documentation/upgrade_guides/v1.10_to_v1.11.md b/Documentation/upgrade_guides/v1.10_to_v1.11.md
new file mode 100644
index 000000000..292c9636e
--- /dev/null
+++ b/Documentation/upgrade_guides/v1.10_to_v1.11.md
@@ -0,0 +1,19 @@
+# How to upgrade from TDW v1.10 to v1.11
+
+## Read the changelog
+
+[Read this](../Changelog.md) for a complete list of changes. TDW v1.11 introduces one major feature, the Replicant API, and various smaller changes to the API.
+
+## 1. Added Replicants
+
+Added Replicants, physically-embodied humanoid agents. For more information, [read this](../lessons/replicants/overview.md).
+
+![](../lessons/replicants/images/multiple_replicants/carry_couch.gif)
+
+## 2. Revised containment data
+
+[Containment output data](../lessons/semantic_states/containment.md) has been revised to make it more accessible outside of the `ContainerManager`. The `send_containment` command now returns `Containment` output data instead of `Overlap` data; `Containment` is similar but faster and includes the semantic containment tag.
+
+Previously, `ContainerManager` added container shapes to objects after adding objects to the scene. This was finicky (container shapes didn't exist on the initial `communicate()` call), buggy, and inflexible (in that other add-ons such as `Replicant` couldn't easily initialize a scene with container shapes). 
+
+Now, container shapes are automatically added if you add objects with `Controller.get_add_physics_object()`. This is much easier to use, but, as a trade-off, if you add objects with `Controller.get_add_object()`, the controller *won't* add container shapes. This is because container shapes are added as commands such as `add_box_container` and `Controller.get_add_object()` has  to return a single command, as opposed to a list of commands.
\ No newline at end of file
diff --git a/Python/example_controllers/semantic_states/composite_object.py b/Python/example_controllers/composite_objects/composite_object.py
similarity index 100%
rename from Python/example_controllers/semantic_states/composite_object.py
rename to Python/example_controllers/composite_objects/composite_object.py
diff --git a/Python/example_controllers/semantic_states/composite_object_open.py b/Python/example_controllers/composite_objects/composite_object_open.py
similarity index 100%
rename from Python/example_controllers/semantic_states/composite_object_open.py
rename to Python/example_controllers/composite_objects/composite_object_open.py
diff --git a/Python/example_controllers/semantic_states/composite_object_torque.py b/Python/example_controllers/composite_objects/composite_object_torque.py
similarity index 100%
rename from Python/example_controllers/semantic_states/composite_object_torque.py
rename to Python/example_controllers/composite_objects/composite_object_torque.py
diff --git a/Python/example_controllers/replicant/animate.py b/Python/example_controllers/replicant/animate.py
new file mode 100644
index 000000000..9e81597ef
--- /dev/null
+++ b/Python/example_controllers/replicant/animate.py
@@ -0,0 +1,28 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.action_status import ActionStatus
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+"""
+Minimal example of how to play an animation.
+"""
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": -0.5, "y": 1.175, "z": 3},
+                           look_at={"x": 0, "y": 1, "z": 0},
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_animate")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=["a"], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate([TDWUtils.create_empty_room(12, 12)])
+# Play an animation.
+replicant.animate(animation="kitchen_refrigerator_f")
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/animate_collision_detection.py b/Python/example_controllers/replicant/animate_collision_detection.py
new file mode 100644
index 000000000..c50de4878
--- /dev/null
+++ b/Python/example_controllers/replicant/animate_collision_detection.py
@@ -0,0 +1,62 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.action_status import ActionStatus
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+class AnimateCollisionDetection(Controller):
+    """
+    A minimal demo of how collision detection parameters affect animations.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        self.replicant = Replicant(position={"x": -2, "y": 0, "z": 0},
+                                   rotation={"x": 0, "y": 90, "z": 0})
+        self.camera = ThirdPersonCamera(position={"x": -0.5, "y": 1.175, "z": 3},
+                                        look_at={"x": 0, "y": 1, "z": 0},
+                                        avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_animate_collision_detection")
+        print(f"Images will be saved to: {path}")
+        self.capture = ImageCapture(avatar_ids=["a"], path=path)
+        self.add_ons.extend([self.replicant, self.camera, self.capture])
+        self.object_id_0 = Controller.get_unique_id()
+        self.object_id_1 = Controller.get_unique_id()
+
+    def initialize_scene(self):
+        self.communicate([TDWUtils.create_empty_room(12, 12),
+                          Controller.get_add_object(model_name="rh10",
+                                                    object_id=self.object_id_0,
+                                                    position={"x": -0.6, "y": 0, "z": 0.01}),
+                          Controller.get_add_object(model_name="chair_billiani_doll",
+                                                    object_id=self.object_id_1,
+                                                    position={"x": -1.5, "y": 0, "z": 0},
+                                                    rotation={"x": 0, "y": 30, "z": 0})])
+        self.camera.look_at(target=self.replicant.replicant_id)
+
+    def animate(self):
+        self.replicant.animate(animation="wading_through_water")
+        while self.replicant.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        self.communicate([])
+        print(self.replicant.action.status)
+
+
+if __name__ == "__main__":
+    c = AnimateCollisionDetection()
+    c.initialize_scene()
+    print("Trying to animate with default collision detection:")
+    c.animate()
+    print("Trying to ignore the rh10 model:")
+    c.replicant.collision_detection.exclude_objects.append(c.object_id_0)
+    c.animate()
+    print("Trying to ignore the previous animation:")
+    c.replicant.collision_detection.previous_was_same = False
+    c.animate()
+    print("Trying to ignore all objects:")
+    c.replicant.collision_detection.objects = False
+    c.animate()
+    c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/carry_couch.py b/Python/example_controllers/replicant/carry_couch.py
new file mode 100644
index 000000000..d61b20332
--- /dev/null
+++ b/Python/example_controllers/replicant/carry_couch.py
@@ -0,0 +1,214 @@
+import numpy as np
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.output_data import OutputData, NavMeshPath, Bounds
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.arm import Arm
+from tdw.replicant.action_status import ActionStatus
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+class CarryCouch(Controller):
+    """
+    Both Replicants will approach opposite sides of the sofa.
+    They will reach to lift up the sofa (although only one Replicant is grasp), carry the sofa, and put it down again.
+
+    In some cases, the Replicants will act asynchronously.
+    In some cases, the Replicants will wait for each other to finish their respective actions.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        # Set the object ID.
+        self.object_id: int = Controller.get_unique_id()
+        # Set the Replicants.
+        self.replicant_0: Replicant = Replicant(replicant_id=0, position={"x": -1, "y": 0, "z": -1})
+        self.replicant_1: Replicant = Replicant(replicant_id=1, position={"x": 1, "y": 0, "z": -1})
+        # Add a camera and enable image capture.
+        camera = ThirdPersonCamera(position={"x": 0, "y": 2.2, "z": -2.61},
+                                   avatar_id="a",
+                                   look_at=self.object_id)
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_carry_couch")
+        print(f"Images will be saved to: {path}")
+        capture = ImageCapture(avatar_ids=["a"], path=path)
+        self.add_ons.extend([self.replicant_0, self.replicant_1, camera, capture])
+        # Initialize the scene.
+        commands = [TDWUtils.create_empty_room(12, 12),
+                    {"$type": "bake_nav_mesh"}]
+        commands.extend(Controller.get_add_physics_object(model_name="arflex_strips_sofa",
+                                                          object_id=self.object_id,
+                                                          position={"x": 0, "y": 0, "z": 1}))
+        commands.append({"$type": "make_nav_mesh_obstacle",
+                         "id": self.object_id,
+                         "carve_type": "stationary"})
+        self.communicate(commands)
+
+    def carry_sofa(self) -> None:
+        """
+        A "meta-action" directing both Replicants to "carry" a sofa.
+        """
+
+        # Get a response from the build.
+        resp = self.communicate([])
+        # Get the left and right sides of the sofa.
+        left = np.zeros(shape=3)
+        right = np.zeros(shape=3)
+        center = np.zeros(shape=3)
+        for i in range(len(resp) - 1):
+            r_id = OutputData.get_data_type_id(resp[i])
+            if r_id == "boun":
+                bounds = Bounds(resp[i])
+                for j in range(bounds.get_num()):
+                    if bounds.get_id(j) == self.object_id:
+                        left = bounds.get_left(j)
+                        right = bounds.get_right(j)
+                        center = bounds.get_center(j)
+                        break
+                break
+        # Set the height of each bounds point to 0.
+        left[1] = 0
+        right[1] = 0
+        center[1] = 0
+        # Offset the bounds positions.
+        v = left - center
+        v = v / np.linalg.norm(v)
+        left += v * 0.25
+        v = right - center
+        v = v / np.linalg.norm(v)
+        right += v * 0.25
+        # Set target positions for each.
+        distance_left = np.linalg.norm(self.replicant_0.dynamic.transform.position - left)
+        distance_right = np.linalg.norm(self.replicant_0.dynamic.transform.position - right)
+        if distance_left < distance_right:
+            replicant_0_target = left
+            replicant_1_target = right
+        else:
+            replicant_0_target = right
+            replicant_1_target = left
+        # Get paths to each target.
+        resp = self.communicate([{"$type": "send_nav_mesh_path",
+                                  "origin": TDWUtils.array_to_vector3(self.replicant_0.dynamic.transform.position),
+                                  "destination": TDWUtils.array_to_vector3(replicant_0_target),
+                                  "id": self.replicant_0.replicant_id},
+                                 {"$type": "send_nav_mesh_path",
+                                  "origin": TDWUtils.array_to_vector3(self.replicant_1.dynamic.transform.position),
+                                  "destination": TDWUtils.array_to_vector3(replicant_1_target),
+                                  "id": self.replicant_1.replicant_id}])
+        # Determine which Replicant requested this path.
+        replicant_0_path = np.zeros(shape=0)
+        replicant_1_path = np.zeros(shape=0)
+        for i in range(len(resp) - 1):
+            r_id = OutputData.get_data_type_id(resp[i])
+            if r_id == "path":
+                nav_mesh_path = NavMeshPath(resp[i])
+                path_id = nav_mesh_path.get_id()
+                if path_id == self.replicant_0.replicant_id:
+                    replicant_0_path = nav_mesh_path.get_path()
+                elif path_id == self.replicant_1.replicant_id:
+                    replicant_1_path = nav_mesh_path.get_path()
+        # These are the indices of the current waypoint in each path.
+        replicant_0_path_index: int = 1
+        replicant_1_path_index: int = 1
+        # Start moving to each waypoint.
+        self.replicant_0.collision_detection.avoid = False
+        self.replicant_1.collision_detection.avoid = False
+        self.replicant_0.collision_detection.objects = False
+        self.replicant_1.collision_detection.objects = False
+        self.replicant_0.move_to(target=replicant_0_path[replicant_0_path_index])
+        self.replicant_1.move_to(target=replicant_1_path[replicant_1_path_index])
+        # Wait until the Replicants stop pathfinding.
+        done = False
+        while not done:
+            # Check if both Replicants are done navigating.
+            replicant_0_done = False
+            if self.replicant_0.action.status != ActionStatus.ongoing:
+                replicant_0_path_index += 1
+                if replicant_0_path_index >= replicant_0_path.shape[0]:
+                    replicant_0_done = True
+                else:
+                    self.replicant_0.move_to(replicant_0_path[replicant_0_path_index])
+            replicant_1_done = False
+            if self.replicant_1.action.status != ActionStatus.ongoing:
+                replicant_1_path_index += 1
+                if replicant_1_path_index >= replicant_1_path.shape[0]:
+                    replicant_1_done = True
+                else:
+                    self.replicant_1.move_to(replicant_1_path[replicant_1_path_index])
+            done = replicant_0_done and replicant_1_done
+            # Continue the loop.
+            self.communicate([])
+        # Look at the sofa's center.
+        self.replicant_0.turn_to(target=center)
+        self.replicant_1.turn_to(target=center)
+        self.do_actions()
+        # Reach for the couch.
+        self.replicant_0.reach_for(target=self.object_id, arm=[Arm.left, Arm.right])
+        self.replicant_1.reach_for(target=self.object_id, arm=[Arm.left, Arm.right])
+        self.do_actions()
+        # Only the first Replicant grasps the couch.
+        self.replicant_0.grasp(target=self.object_id, arm=Arm.left, angle=None)
+        self.do_replicant_0_action()
+        # Everyone raises their hands.
+        self.reach_for_height(y=0.8)
+        # Do the actions.
+        self.do_actions()
+        # The first Replicant walks backwards. The second Replicant walks forwards.
+        self.replicant_0.move_by(distance=-2,
+                                 reset_arms=False)
+        self.replicant_1.move_by(distance=2,
+                                 reset_arms=False)
+        self.do_actions()
+        # Lower the sofa.
+        self.reach_for_height(y=0.1)
+        # Drop the sofa.
+        self.replicant_0.drop(arm=Arm.left)
+        self.do_replicant_0_action()
+        # Reset the arms.
+        self.replicant_0.reset_arm(arm=[Arm.left, Arm.right])
+        self.replicant_1.reset_arm(arm=[Arm.left, Arm.right])
+        self.do_actions()
+
+    def do_actions(self) -> None:
+        """
+        Wait for both Replicants' actions to end.
+        """
+
+        while self.replicant_0.action.status == ActionStatus.ongoing or self.replicant_1.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        self.communicate([])
+
+    def do_replicant_0_action(self) -> None:
+        """
+        Wait for only the first Replicant's action to end.
+        """
+
+        while self.replicant_0.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        self.communicate([])
+
+    def reach_for_height(self, y: float) -> None:
+        """
+        Tell both Replicants to reach for positions relative to their left hands.
+
+        :param y: Add this y value to the target position.
+        """
+
+        for replicant in [self.replicant_0, self.replicant_1]:
+            # Get the position of the left hand.
+            target = TDWUtils.array_to_vector3(replicant.dynamic.body_parts[replicant.static.hands[Arm.left]].position)
+            # Raise the target's height.
+            target["y"] = y
+            # The left hand reaches for the target and the right hand follows.
+            replicant.reach_for(target=target,
+                                arm=Arm.left,
+                                offhand_follows=True)
+        # Both Replicants do their actions.
+        self.do_actions()
+
+
+if __name__ == "__main__":
+    c = CarryCouch()
+    c.carry_sofa()
+    c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/clap.py b/Python/example_controllers/replicant/clap.py
new file mode 100644
index 000000000..a38f2effa
--- /dev/null
+++ b/Python/example_controllers/replicant/clap.py
@@ -0,0 +1,134 @@
+from typing import List
+from enum import Enum
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.controller import Controller
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.actions.arm_motion import ArmMotion
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.arm import Arm
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class ClapState(Enum):
+    raising_hands = 1
+    coming_together = 2
+    pulling_apart = 4
+
+
+class Clap(ArmMotion):
+    """
+    Clap your hands.
+    """
+
+    def __init__(self, dynamic: ReplicantDynamic, collision_detection: CollisionDetection):
+        super().__init__(arms=[Arm.left, Arm.right], dynamic=dynamic, collision_detection=collision_detection,
+                         duration=0.1, previous=None)
+        self.clap_state: ClapState = ClapState.raising_hands
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        # Get the standard initialization commands.
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        # Get the initial position of each hand.
+        commands.extend(self.get_initial_position_commands(static=static, dynamic=dynamic))
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Continue the action, checking for collisions.
+        commands = super().get_ongoing_commands(resp=resp, static=static, dynamic=dynamic)
+        # The motion ended. Decide if we need to do more motions.
+        # It's ok in this case if the motion ends in failed_to_reach because we don't need it to be precise.
+        if self.status == ActionStatus.success or self.status == ActionStatus.failed_to_reach:
+            # We're done raising the hands. Bring the hands together.
+            if self.clap_state == ClapState.raising_hands:
+                # The action is ongoing.
+                self.status = ActionStatus.ongoing
+                # The state is coming together.
+                self.clap_state = ClapState.coming_together
+                # Get a position in front of the Replicant.
+                position = self.get_clap_position(dynamic=dynamic)
+                # Tell both hands to reach for the target position.
+                commands = []
+                for arm in self.arms:
+                    commands.append({"$type": "replicant_reach_for_position",
+                                     "id": static.replicant_id,
+                                     "position": TDWUtils.array_to_vector3(position),
+                                     "duration": self.duration,
+                                     "arm": arm.name})
+            # We're done moving the hands together. Bring the hands apart again.
+            elif self.clap_state == ClapState.coming_together:
+                # The action is ongoing.
+                self.status = ActionStatus.ongoing
+                # The state is pulling apart.
+                self.clap_state = ClapState.pulling_apart
+                # Reach for the initial positions.
+                commands.extend(self.get_initial_position_commands(static=static, dynamic=dynamic))
+            # If the motion is successful and the state is `pulling_apart`, then we're done.
+            elif self.clap_state == ClapState.pulling_apart:
+                self.status = ActionStatus.success
+        return commands
+
+    @staticmethod
+    def get_clap_position(dynamic: ReplicantDynamic) -> np.ndarray:
+        # Get a position in front of the Replicant.
+        position = dynamic.transform.position.copy()
+        # Move the position in front of the Replicant.
+        position += dynamic.transform.forward * 0.4
+        # Set the height of the position.
+        position[1] = 1.5
+        return position
+
+    def get_initial_position_commands(self, static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Get the position.
+        position = Clap.get_clap_position(dynamic=dynamic)
+        origin = dynamic.transform.position.copy()
+        origin[1] = position[1]
+        forward = dynamic.transform.forward * 0.4
+        # Rotate the position to the side.
+        left = TDWUtils.rotate_position_around(position=position,
+                                               origin=origin,
+                                               angle=-90)
+        # Move the position forward.
+        left += forward
+        # Rotate the position to the side.
+        right = TDWUtils.rotate_position_around(position=position,
+                                                origin=origin,
+                                                angle=90)
+        # Move the position forward.
+        right += forward
+        commands = []
+        # Reach for the initial positions.
+        for arm, position in zip(self.arms, [left, right]):
+            commands.append({"$type": "replicant_reach_for_position",
+                             "id": static.replicant_id,
+                             "position": TDWUtils.array_to_vector3(position),
+                             "duration": self.duration,
+                             "arm": arm.name})
+        return commands
+
+
+if __name__ == "__main__":
+    c = Controller()
+    replicant = Replicant()
+    camera = ThirdPersonCamera(position={"x": 0, "y": 1.5, "z": 2.5},
+                               look_at=replicant.replicant_id,
+                               avatar_id="a")
+    path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_clap")
+    print(f"Images will be saved to: {path}")
+    capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+    c.add_ons.extend([replicant, camera, capture])
+    c.communicate(TDWUtils.create_empty_room(12, 12))
+    replicant.action = Clap(dynamic=replicant.dynamic, collision_detection=replicant.collision_detection)
+    while replicant.action.status == ActionStatus.ongoing:
+        c.communicate([])
+    c.communicate([])
+    print(replicant.action.status)
+    c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/crash.py b/Python/example_controllers/replicant/crash.py
new file mode 100644
index 000000000..c059dda87
--- /dev/null
+++ b/Python/example_controllers/replicant/crash.py
@@ -0,0 +1,36 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+"""
+Collide with an obstacle.
+"""
+
+c = Controller()
+camera = ThirdPersonCamera(position={"x": 3, "y": 1.175, "z": 2}, avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_crash")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=["a"], path=path)
+replicant = Replicant(position={"x": 0, "y": 0, "z": 0})
+# Don't try to avoid obstacles.
+replicant.collision_detection.avoid = False
+replicant.collision_detection.objects = False
+# Append the add-ons.
+c.add_ons.extend([replicant, camera, capture])
+# Create a scene. Add a sofa.
+commands = [TDWUtils.create_empty_room(12, 20),
+            Controller.get_add_object(model_name="arflex_strips_sofa",
+                                      position={"x": 0, "y": 0, "z": 4},
+                                      object_id=Controller.get_unique_id())]
+# Send the commands.
+c.communicate(commands)
+camera.look_at(target=replicant.replicant_id)
+replicant.move_by(8)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/egocentric_images.py b/Python/example_controllers/replicant/egocentric_images.py
new file mode 100644
index 000000000..a983b43a7
--- /dev/null
+++ b/Python/example_controllers/replicant/egocentric_images.py
@@ -0,0 +1,27 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.image_frequency import ImageFrequency
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+"""
+Capture image data from the Replicant per `communicate()` call and save it to disk.
+"""
+
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_egocentric")
+print(f"Images will be saved to: {path}")
+c = Controller()
+# Request image data on every `communicate()` call.
+replicant = Replicant(image_frequency=ImageFrequency.always)
+c.add_ons.append(replicant)
+c.communicate(TDWUtils.create_empty_room(12, 12))
+replicant.move_by(2)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+    # Save the images.
+    replicant.dynamic.save_images(output_directory=path)
+c.communicate([])
+# Save the images.
+replicant.dynamic.save_images(output_directory=path)
+c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/grasp_basket_with_object.py b/Python/example_controllers/replicant/grasp_basket_with_object.py
new file mode 100644
index 000000000..f18463161
--- /dev/null
+++ b/Python/example_controllers/replicant/grasp_basket_with_object.py
@@ -0,0 +1,51 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+"""
+Grasp a basket containing an object.
+"""
+
+
+def do_action():
+    """
+    A helper function to handle the basic Replicant action loop.
+    """
+
+    while replicant.action.status == ActionStatus.ongoing:
+        c.communicate([])
+    c.communicate([])
+
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": -2.4, "y": 2, "z": 3.2},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_grasp_basket_with_object")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+object_id = Controller.get_unique_id()
+commands = [TDWUtils.create_empty_room(12, 12)]
+commands.extend(Controller.get_add_physics_object(model_name="basket_18inx18inx12iin_wicker",
+                                                  object_id=object_id,
+                                                  position={"x": -2, "y": 0, "z": 2}))
+commands.extend(Controller.get_add_physics_object(model_name="vase_02",
+                                                  object_id=Controller.get_unique_id(),
+                                                  position={"x": -2, "y": 0.1, "z": 2}))
+c.communicate(commands)
+replicant.move_to(target=object_id)
+do_action()
+replicant.reach_for(target=object_id, arm=Arm.right)
+do_action()
+replicant.grasp(target=object_id, arm=Arm.right)
+do_action()
+replicant.reach_for(target={"x": 0.1, "y": 1.1, "z": 0.6}, arm=Arm.right, absolute=False)
+do_action()
+c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/grasp_basket_with_object_both_hands.py b/Python/example_controllers/replicant/grasp_basket_with_object_both_hands.py
new file mode 100644
index 000000000..5e26667d0
--- /dev/null
+++ b/Python/example_controllers/replicant/grasp_basket_with_object_both_hands.py
@@ -0,0 +1,54 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+"""
+Grasp a basket containing an object with both hands.
+"""
+
+
+def do_action():
+    """
+    A helper function to handle the basic Replicant action loop.
+    """
+
+    while replicant.action.status == ActionStatus.ongoing:
+        c.communicate([])
+    c.communicate([])
+
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": -2.4, "y": 2, "z": 3.2},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_grasp_basket_with_object_both_hands")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+object_id = Controller.get_unique_id()
+commands = [TDWUtils.create_empty_room(12, 12)]
+commands.extend(Controller.get_add_physics_object(model_name="basket_18inx18inx12iin_wicker",
+                                                  object_id=object_id,
+                                                  position={"x": -2, "y": 0, "z": 2}))
+commands.extend(Controller.get_add_physics_object(model_name="vase_02",
+                                                  object_id=Controller.get_unique_id(),
+                                                  position={"x": -2, "y": 0.1, "z": 2}))
+c.communicate(commands)
+replicant.move_to(target=object_id)
+do_action()
+# Reach with both hands.
+replicant.reach_for(target=object_id, arm=[Arm.right, Arm.left])
+do_action()
+# Grasp with the right hand.
+replicant.grasp(target=object_id, arm=Arm.right)
+do_action()
+# Reach for a target position with the right hand; the left hand will follow.
+replicant.reach_for(target={"x": 0, "y": 1.1, "z": 0.4}, arm=Arm.right, absolute=False, offhand_follows=True)
+do_action()
+c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/look_at.py b/Python/example_controllers/replicant/look_at.py
new file mode 100644
index 000000000..f9458a74f
--- /dev/null
+++ b/Python/example_controllers/replicant/look_at.py
@@ -0,0 +1,38 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.image_frequency import ImageFrequency
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+"""
+Look at an object and a position, then reset the head.
+"""
+
+def do_action():
+    while replicant.action.status == ActionStatus.ongoing:
+        c.communicate([])
+
+
+c = Controller()
+replicant = Replicant(image_frequency=ImageFrequency.always)
+c.add_ons.append(replicant)
+object_id = Controller.get_unique_id()
+commands = [TDWUtils.create_empty_room(12, 12)]
+commands.extend(Controller.get_add_physics_object(model_name="basket_18inx18inx12iin_wicker",
+                                                  object_id=object_id,
+                                                  position={"x": -2, "y": 0, "z": 3}))
+c.communicate(commands)
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_look_at")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[replicant.static.avatar_id], path=path)
+c.add_ons.append(capture)
+c.communicate([])
+replicant.look_at(target=object_id)
+do_action()
+replicant.look_at(target={"x": 2, "y": 0.3, "z": 3})
+do_action()
+replicant.reset_head()
+do_action()
+c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/minimal_custom_action.py b/Python/example_controllers/replicant/minimal_custom_action.py
new file mode 100644
index 000000000..e5c3dee9a
--- /dev/null
+++ b/Python/example_controllers/replicant/minimal_custom_action.py
@@ -0,0 +1,43 @@
+from typing import List
+from tdw.tdw_utils import TDWUtils
+from tdw.controller import Controller
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.actions.action import Action
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class DoNothing(Action):
+    """
+    A minimal custom action.
+    """
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        # Get the standard initialization commands.
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        self.status = ActionStatus.success
+        return []
+
+    def get_end_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                         image_frequency: ImageFrequency) -> List[dict]:
+        return super().get_end_commands(resp=resp, static=static, dynamic=dynamic, image_frequency=image_frequency)
+
+
+if __name__ == "__main__":
+    c = Controller()
+    replicant = Replicant()
+    c.add_ons.append(replicant)
+    c.communicate(TDWUtils.create_empty_room(12, 12))
+    replicant.action = DoNothing()
+    while replicant.action.status == ActionStatus.ongoing:
+        c.communicate([])
+    c.communicate([])
+    print(replicant.action.status)
+    c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/move_by.py b/Python/example_controllers/replicant/move_by.py
new file mode 100644
index 000000000..20950d40d
--- /dev/null
+++ b/Python/example_controllers/replicant/move_by.py
@@ -0,0 +1,33 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+from tdw.replicant.action_status import ActionStatus
+
+"""
+Move a Replicant by a target distance and print its status.
+"""
+
+c = Controller()
+replicant = Replicant(position={"x": 0, "y": 0, "z": 2})
+camera = ThirdPersonCamera(position={"x": 2, "y": 1.6, "z": 1},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("move_by")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=["a"],
+                       path=path)
+# Note the order in which the add-ons are added. The replicant needs to be first so that the camera can look at it.
+c.add_ons.extend([replicant, camera, capture])
+# Create the scene.
+c.communicate(TDWUtils.create_empty_room(12, 12))
+# Start walking.
+replicant.move_by(2)
+# Continue walking until the action ends.
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+print(replicant.action.status)
+c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/move_grasp_drop.py b/Python/example_controllers/replicant/move_grasp_drop.py
new file mode 100644
index 000000000..b22e74de1
--- /dev/null
+++ b/Python/example_controllers/replicant/move_grasp_drop.py
@@ -0,0 +1,63 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+class MoveGraspDrop(Controller):
+    """
+    Walk to an object, grasp it, walk away, and drop it.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        # Set the replicant and the object IDs here because we need to reference them elsewhere.
+        self.replicant = Replicant()
+        self.trunk_id = 1
+        self.mug_id = 2
+
+    def do_action(self) -> None:
+        while self.replicant.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        self.communicate([])
+
+    def run(self) -> None:
+        camera = ThirdPersonCamera(position={"x": -3.5, "y": 1.175, "z": 3},
+                                   avatar_id="a",
+                                   look_at=self.replicant.replicant_id)
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_move_grasp_drop")
+        print(f"Images will be saved to: {path}")
+        capture = ImageCapture(avatar_ids=["a"], path=path)
+        self.add_ons.extend([self.replicant, camera, capture])
+        # Create the room.
+        commands = [TDWUtils.create_empty_room(12, 12)]
+        commands.extend(Controller.get_add_physics_object(model_name="trunck",
+                                                          object_id=self.trunk_id,
+                                                          position={"x": 0, "y": 0, "z": 3},
+                                                          kinematic=True))
+        commands.extend(Controller.get_add_physics_object(model_name="coffeemug",
+                                                          object_id=self.mug_id,
+                                                          position={"x": 0, "y": 0.9888946, "z": 2.7}))
+        self.communicate(commands)
+        self.replicant.move_to(target=self.trunk_id)
+        self.do_action()
+        # Ignore the trunk.
+        self.replicant.collision_detection.exclude_objects.append(self.trunk_id)
+        self.replicant.reach_for(target=self.mug_id, arm=Arm.right)
+        self.do_action()
+        self.replicant.grasp(target=self.mug_id, arm=Arm.right, angle=None)
+        self.do_action()
+        self.replicant.move_by(-2)
+        self.do_action()
+        self.replicant.drop(arm=Arm.right)
+        self.do_action()
+        self.communicate({"$type": "terminate"})
+
+
+if __name__ == "__main__":
+    c = MoveGraspDrop()
+    c.run()
diff --git a/Python/example_controllers/replicant/move_to.py b/Python/example_controllers/replicant/move_to.py
new file mode 100644
index 000000000..f86182e68
--- /dev/null
+++ b/Python/example_controllers/replicant/move_to.py
@@ -0,0 +1,31 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+from tdw.replicant.action_status import ActionStatus
+
+"""
+Move a Replicant to a target behind it.
+"""
+
+c = Controller()
+replicant = Replicant(position={"x": 0, "y": 0, "z": 2})
+camera = ThirdPersonCamera(position={"x": 2, "y": 1.6, "z": 1},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_move_to")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=["a"],
+                       path=path)
+c.add_ons.extend([replicant, camera, capture])
+# Create the scene.
+c.communicate(TDWUtils.create_empty_room(12, 12))
+# Start walking.
+replicant.move_to(target={"x": 0, "y": 0, "z": -3})
+# Continue walking until the action ends.
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/multi_navigate.py b/Python/example_controllers/replicant/multi_navigate.py
new file mode 100644
index 000000000..3b0a5f671
--- /dev/null
+++ b/Python/example_controllers/replicant/multi_navigate.py
@@ -0,0 +1,265 @@
+from enum import Enum
+from typing import List, Union, Dict, Optional
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.controller import Controller
+from tdw.output_data import OutputData, NavMeshPath
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.actions.action import Action
+from tdw.replicant.actions.move_to import MoveTo
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class NavigateState(Enum):
+    getting_path = 1
+    navigating = 2
+
+
+class Navigate(Action):
+    """
+    Navigate using a NavMeshPath and multiple MoveTo child actions.
+    """
+
+    def __init__(self, target: Union[Dict[str, float], np.ndarray], collision_detection: CollisionDetection):
+        super().__init__()
+        # My collision detection. This will be used to instantiate MoveTo child actions.
+        self.collision_detection: CollisionDetection = collision_detection
+        # Convert the target position to a dictionary if needed.
+        if isinstance(target, np.ndarray):
+            self.target: Dict[str, float] = TDWUtils.array_to_vector3(target)
+        elif isinstance(target, dict):
+            self.target = target
+        else:
+            raise Exception(target)
+        # We are getting the path.
+        self.navigate_state: NavigateState = NavigateState.getting_path
+        # My path.
+        self.path: np.ndarray = np.zeros(shape=0)
+        # The index of the current waypoint in the path. The point at index=0 is the current position.
+        self.path_index: int = 1
+        # My MoveTo action.
+        self.move_to: Optional[MoveTo] = None
+        # The ImageFrequency. This will be used to instantiate MoveTo child actions.
+        # This will be set in get_initialization_commands()
+        self.image_frequency: ImageFrequency = ImageFrequency.once
+        # Don't try to detect obstacles while pathfinding.
+        self.original_avoid: bool = self.collision_detection.avoid
+        self.collision_detection.avoid = False
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        # Remember the ImageFrequency because we'll need it to instantiate MoveTo child actions.
+        self.image_frequency = image_frequency
+        # Get the usual initialization commands.
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        # Request a NavMeshPath. Set the path's ID to the Replicant's ID so we know whose path this is.
+        commands.append({"$type": "send_nav_mesh_path",
+                         "origin": TDWUtils.array_to_vector3(dynamic.transform.position),
+                         "destination": self.target,
+                         "id": static.replicant_id})
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Get my path.
+        if self.navigate_state == NavigateState.getting_path:
+            # We are now navigating.
+            self.navigate_state = NavigateState.navigating
+            # Parse the output data.
+            for i in range(len(resp) - 1):
+                r_id = OutputData.get_data_type_id(resp[i])
+                # Got NavMeshPath data.
+                if r_id == "path":
+                    path = NavMeshPath(resp[i])
+                    # This is my path.
+                    if path.get_id() == static.replicant_id:
+                        # We failed to pathfind to this destination.
+                        if path.get_state() != "complete":
+                            self.status = ActionStatus.failed_to_move
+                            return []
+                        # This is a valid path.
+                        else:
+                            self.path = path.get_path()
+                            # Start moving.
+                            return self.set_move_to(resp=resp, static=static, dynamic=dynamic)
+        # Continue to navigate.
+        else:
+            # Get the child action's ongoing commands. This will also update the child action's status.
+            commands = self.move_to.get_ongoing_commands(resp=resp, static=static, dynamic=dynamic)
+            # The child action ended in success. Check if this is the last waypoint.
+            if self.move_to.status == ActionStatus.success:
+                # We arrived at the destination.
+                if self.path_index >= self.path.shape[0]:
+                    self.status = ActionStatus.success
+                    return commands
+                # Move to the next waypoint.
+                else:
+                    # End the current move_to action.
+                    commands.extend(self.move_to.get_end_commands(resp=resp, static=static, dynamic=dynamic,
+                                                                  image_frequency=self.image_frequency))
+                    # Start a new move_to action. Append the new child action's initialization commands.
+                    commands.extend(self.set_move_to(resp=resp, static=static, dynamic=dynamic))
+            # The action ended in failure.
+            elif self.move_to.status != ActionStatus.ongoing:
+                self.status = self.move_to.status
+            return commands
+
+    def get_end_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                         image_frequency: ImageFrequency) -> List[dict]:
+        # Reset obstacle avoidance.
+        self.collision_detection.avoid = self.original_avoid
+        # Get end commands.
+        if self.move_to is not None:
+            return self.move_to.get_end_commands(resp=resp, static=static, dynamic=dynamic,
+                                                 image_frequency=image_frequency)
+        else:
+            return super().get_end_commands(resp=resp, static=static, dynamic=dynamic,
+                                            image_frequency=image_frequency)
+
+    def set_move_to(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Set the action.
+        self.move_to = MoveTo(target=self.path[self.path_index],
+                              collision_detection=self.collision_detection,
+                              reset_arms=True,
+                              reset_arms_duration=0.25,
+                              arrived_at=0.1,
+                              max_walk_cycles=100,
+                              bounds_position="center",
+                              previous=None)
+        # Update the path index
+        self.path_index += 1
+        # Return the initialization commands.
+        return self.move_to.get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                        image_frequency=self.image_frequency)
+
+
+class NavigatorState(Enum):
+    navigating = 1
+    moving_backward = 2
+    turning = 3
+    moving_forward = 4
+    done = 5
+
+
+class Navigator(Replicant):
+    """
+    A Replicant trying to navigate to a target position.
+    """
+
+    def __init__(self, target: Dict[str, float], replicant_id: int = 0, position: Dict[str, float] = None, rotation: Dict[str, float] = None):
+        super().__init__(replicant_id=replicant_id, position=position, rotation=rotation)
+        # Set the initial target, the initial state, and the action.
+        self.target = target
+        self.navigator_state: NavigatorState = NavigatorState.navigating
+        self.navigate(target=self.target)
+        # We don't care if the previous action ended in a collision.
+        self.collision_detection.previous_was_same = False
+
+    def navigate(self, target: Union[Dict[str, float], np.ndarray]) -> None:
+        self.action = Navigate(target=target, collision_detection=self.collision_detection)
+
+    def on_send(self, resp: List[bytes]) -> None:
+        # Process the Replicant's action, commands, etc. as normal.
+        super().on_send(resp=resp)
+        # The action is ongoing. Do nothing extra.
+        if self.action.status == ActionStatus.ongoing:
+            return
+        # The action succeeded. Check if we're done navigating.
+        elif self.action.status == ActionStatus.success:
+            # We're done!
+            if self.navigator_state == NavigatorState.navigating:
+                self.navigator_state = NavigatorState.done
+            # The Replicant finished moving backwards. Start turning.
+            elif self.navigator_state == NavigatorState.moving_backward:
+                self.navigator_state = NavigatorState.turning
+                self.turn_by(35)
+            # The Replicant finished turning. Start moving forward.
+            elif self.navigator_state == NavigatorState.turning:
+                self.move_by(distance=0.75)
+                self.navigator_state = NavigatorState.moving_forward
+            # The Replicant finished moving forward. Start navigating to the target again.
+            elif self.navigator_state == NavigatorState.moving_forward:
+                self.navigate(target=self.target)
+                self.navigator_state = NavigatorState.navigating
+        # The action ended in failure. Start backing up.
+        else:
+            self.navigator_state = NavigatorState.moving_backward
+            self.move_by(distance=-0.5)
+
+
+class MultiNavigate(Controller):
+    """
+    An example of how to utilize the NavMesh to pathfind in a multi-Replicant simulation.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        self.replicant_0 = Navigator(replicant_id=0,
+                                     position={"x": 0, "y": 0, "z": -5},
+                                     target={"x": 0, "y": 0, "z": 4})
+        self.replicant_1 = Navigator(replicant_id=1,
+                                     position={"x": 0, "y": 0, "z": 5},
+                                     target={"x": 0, "y": 0, "z": -4})
+        self.camera: ThirdPersonCamera = ThirdPersonCamera(position={"x": 0, "y": 13.8, "z": 0},
+                                                           look_at={"x": 0, "y": 0, "z": 0},
+                                                           avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("multi_replicant_navigate")
+        print(f"Images will be saved to: {path}")
+        self.capture: ImageCapture = ImageCapture(avatar_ids=["a"], path=path)
+        self.add_ons.extend([self.replicant_0, self.replicant_1, self.camera, self.capture])
+        # Set the object IDs.
+        self.trunk_id = Controller.get_unique_id()
+        self.chair_id = Controller.get_unique_id()
+        self.table_id = Controller.get_unique_id()
+        self.rocking_horse_id = Controller.get_unique_id()
+
+    def init_scene(self):
+        self.communicate([{"$type": "load_scene",
+                           "scene_name": "ProcGenScene"},
+                          TDWUtils.create_empty_room(12, 12),
+                          {"$type": "bake_nav_mesh"},
+                          Controller.get_add_object(model_name="trunck",
+                                                    object_id=self.trunk_id,
+                                                    position={"x": 1.5, "y": 0, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.trunk_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="chair_billiani_doll",
+                                                    object_id=self.chair_id,
+                                                    position={"x": -2.25, "y": 0, "z": 2.5},
+                                                    rotation={"x": 0, "y": 20, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.chair_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="live_edge_coffee_table",
+                                                    object_id=self.table_id,
+                                                    position={"x": 0.2, "y": 0, "z": -2.25},
+                                                    rotation={"x": 0, "y": 20, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.table_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="rh10",
+                                                    object_id=self.rocking_horse_id,
+                                                    position={"x": -1, "y": 0, "z": 1.5}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.rocking_horse_id,
+                           "carve_type": "stationary"}])
+
+    def run(self) -> None:
+        self.init_scene()
+        # Wait until both Replicants are done navigating.
+        while self.replicant_0.navigator_state != NavigatorState.done or self.replicant_1.navigator_state != NavigatorState.done:
+            self.communicate([])
+        self.communicate({"$type": "terminate"})
+
+
+if __name__ == "__main__":
+    c = MultiNavigate()
+    c.run()
diff --git a/Python/example_controllers/replicant/multi_replicant.py b/Python/example_controllers/replicant/multi_replicant.py
new file mode 100644
index 000000000..cb6569c02
--- /dev/null
+++ b/Python/example_controllers/replicant/multi_replicant.py
@@ -0,0 +1,62 @@
+from typing import Dict
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+class MultiReplicant(Controller):
+    """
+    A minimal multi-Replicant simulation.
+    """
+
+    def __init__(self, replicants: Dict[int, Dict[str, float]],
+                 port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        # Remember the Replicants. Key = ID. Value = Replicant.
+        self.replicants: Dict[int, Replicant] = dict()
+        for replicant_id in replicants:
+            # Create a Replicant add-on. Set its ID and position.
+            replicant = Replicant(replicant_id=replicant_id,
+                                  position=replicants[replicant_id])
+            # Append the add-on.
+            self.add_ons.append(replicant)
+            self.replicants[replicant_id] = replicant
+        # Add a camera and enable image capture.
+        # These aren't field (they don't start with self. ) because we don't need to reference them again.
+        camera = ThirdPersonCamera(position={"x": -2.4, "y": 6, "z": 3.2},
+                                   look_at={"x": 0, "y": 1, "z": 0},
+                                   avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("multi_replicant")
+        print(f"Images will be saved to: {path}")
+        capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+        self.add_ons.extend([camera, capture])
+        # Create an empty scene.
+        self.communicate(TDWUtils.create_empty_room(12, 12))
+
+    def do_actions(self) -> None:
+        # Loop.
+        done = False
+        while not done:
+            for replicant_id in self.replicants:
+                # One of the actions ended. Stop.
+                if self.replicants[replicant_id].action.status != ActionStatus.ongoing:
+                    done = True
+            # Continue the loop.
+            if not done:
+                self.communicate([])
+        self.communicate([])
+
+
+if __name__ == "__main__":
+    c = MultiReplicant(replicants={0: {"x": 1.5, "y": 0, "z": -1},
+                                   1: {"x": -1.5, "y": 0, "z": -1}})
+    c.replicants[0].move_by(distance=2)
+    c.replicants[1].move_by(distance=4)
+    c.do_actions()
+    print(c.replicants[0].action.status)
+    print(c.replicants[1].action.status)
+    c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/multi_replicant_state_machine.py b/Python/example_controllers/replicant/multi_replicant_state_machine.py
new file mode 100644
index 000000000..7fd7a8a08
--- /dev/null
+++ b/Python/example_controllers/replicant/multi_replicant_state_machine.py
@@ -0,0 +1,87 @@
+from enum import Enum
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+class Replicant1State(Enum):
+    moving_to = 1
+    moving_backward = 2
+    turning = 3
+    moving_forward = 4
+
+
+class MultiReplicantStateMachine(Controller):
+    """
+    Control the actions of multiple Replicants using a state machine.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        self.replicant_0 = Replicant(replicant_id=0,
+                                     position={"x": 0, "y": 0, "z": 0})
+        self.replicant_1 = Replicant(replicant_id=1,
+                                     position={"x": 0, "y": 0, "z": -2})
+        camera = ThirdPersonCamera(position={"x": -4, "y": 3, "z": 3.2},
+                                   look_at=self.replicant_1.replicant_id,
+                                   avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("multi_replicant_state_machine")
+        print(f"Images will be saved to: {path}")
+        capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+        self.add_ons.extend([self.replicant_0, self.replicant_1, camera, capture])
+        # Create an empty scene.
+        self.communicate(TDWUtils.create_empty_room(12, 12))
+        # Start animating.
+        self.replicant_0_animation: str = "dancing_3"
+        self.replicant_0.animate(animation=self.replicant_0_animation)
+        # Set Replicant 1's target to be 6 meters in front of it.
+        self.replicant_1_target = {"x": self.replicant_1.initial_position["x"],
+                                   "y": self.replicant_1.initial_position["y"],
+                                   "z": self.replicant_1.initial_position["z"] + 6}
+        # Start moving.
+        self.replicant_1.move_to(target=self.replicant_1_target)
+        self.replicant_1_state = Replicant1State.moving_to
+
+    def run(self) -> None:
+        done = False
+        while not done:
+            # The animation ended. Play it again.
+            if self.replicant_0.action.status != ActionStatus.ongoing:
+                self.replicant_0.animate(animation=self.replicant_0_animation)
+
+            # Handle Replicant 1's state machine.
+            # Check if the action succeeded.
+            if self.replicant_1.action.status == ActionStatus.success:
+                # The Replicant finished moving forward. We're done.
+                if self.replicant_1_state == Replicant1State.moving_to:
+                    done = True
+                # The Replicant finished moving backwards. Start turning.
+                elif self.replicant_1_state == Replicant1State.moving_backward:
+                    self.replicant_1_state = Replicant1State.turning
+                    self.replicant_1.turn_by(35)
+                # The Replicant finished turning. Start moving forward.
+                elif self.replicant_1_state == Replicant1State.turning:
+                    self.replicant_1.move_by(distance=0.75)
+                    self.replicant_1_state = Replicant1State.moving_forward
+                # The Replicant finished moving forward. Start moving to the target again.
+                elif self.replicant_1_state == Replicant1State.moving_forward:
+                    self.replicant_1.move_to(target=self.replicant_1_target)
+                    self.replicant_1_state = Replicant1State.moving_to
+            # The action ended in failure.
+            elif self.replicant_1.action.status != ActionStatus.ongoing:
+                # Start backing up.
+                self.replicant_1_state = Replicant1State.moving_backward
+                self.replicant_1.move_by(distance=-0.5)
+
+            # Continue the loop.
+            self.communicate([])
+        self.communicate({"$type": "terminate"})
+
+
+if __name__ == "__main__":
+    c = MultiReplicantStateMachine()
+    c.run()
diff --git a/Python/example_controllers/replicant/navigate.py b/Python/example_controllers/replicant/navigate.py
new file mode 100644
index 000000000..eb52c299a
--- /dev/null
+++ b/Python/example_controllers/replicant/navigate.py
@@ -0,0 +1,218 @@
+from enum import Enum
+from typing import List, Union, Dict, Optional
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.controller import Controller
+from tdw.output_data import OutputData, NavMeshPath
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.actions.action import Action
+from tdw.replicant.actions.move_to import MoveTo
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class NavigateState(Enum):
+    getting_path = 1
+    navigating = 2
+
+
+class Navigate(Action):
+    """
+    Navigate using a NavMeshPath and multiple MoveTo child actions.
+    """
+
+    def __init__(self, target: Union[Dict[str, float], np.ndarray], collision_detection: CollisionDetection):
+        super().__init__()
+        # My collision detection. This will be used to instantiate MoveTo child actions.
+        self.collision_detection: CollisionDetection = collision_detection
+        # Convert the target position to a dictionary if needed.
+        if isinstance(target, np.ndarray):
+            self.target: Dict[str, float] = TDWUtils.array_to_vector3(target)
+        elif isinstance(target, dict):
+            self.target = target
+        else:
+            raise Exception(target)
+        # We are getting the path.
+        self.navigate_state: NavigateState = NavigateState.getting_path
+        # My path.
+        self.path: np.ndarray = np.zeros(shape=0)
+        # The index of the current waypoint in the path. The point at index=0 is the current position.
+        self.path_index: int = 1
+        # My MoveTo action.
+        self.move_to: Optional[MoveTo] = None
+        # The ImageFrequency. This will be used to instantiate MoveTo child actions.
+        # This will be set in get_initialization_commands()
+        self.image_frequency: ImageFrequency = ImageFrequency.once
+        # Don't try to detect obstacles while pathfinding.
+        self.original_avoid: bool = self.collision_detection.avoid
+        self.collision_detection.avoid = False
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        # Remember the ImageFrequency because we'll need it to instantiate MoveTo child actions.
+        self.image_frequency = image_frequency
+        # Get the usual initialization commands.
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        # Request a NavMeshPath. Set the path's ID to the Replicant's ID so we know whose path this is.
+        commands.append({"$type": "send_nav_mesh_path",
+                         "origin": TDWUtils.array_to_vector3(dynamic.transform.position),
+                         "destination": self.target,
+                         "id": static.replicant_id})
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Get my path.
+        if self.navigate_state == NavigateState.getting_path:
+            # We are now navigating.
+            self.navigate_state = NavigateState.navigating
+            # Parse the output data.
+            for i in range(len(resp) - 1):
+                r_id = OutputData.get_data_type_id(resp[i])
+                # Got NavMeshPath data.
+                if r_id == "path":
+                    path = NavMeshPath(resp[i])
+                    # This is my path.
+                    if path.get_id() == static.replicant_id:
+                        # We failed to pathfind to this destination.
+                        if path.get_state() != "complete":
+                            self.status = ActionStatus.failed_to_move
+                            return []
+                        # This is a valid path.
+                        else:
+                            self.path = path.get_path()
+                            # Start moving.
+                            return self.set_move_to(resp=resp, static=static, dynamic=dynamic)
+        # Continue to navigate.
+        else:
+            # Get the child action's ongoing commands. This will also update the child action's status.
+            commands = self.move_to.get_ongoing_commands(resp=resp, static=static, dynamic=dynamic)
+            # The child action ended in success. Check if this is the last waypoint.
+            if self.move_to.status == ActionStatus.success:
+                # We arrived at the destination.
+                if self.path_index >= self.path.shape[0]:
+                    self.status = ActionStatus.success
+                    return commands
+                # Move to the next waypoint.
+                else:
+                    # End the current move_to action.
+                    commands.extend(self.move_to.get_end_commands(resp=resp, static=static, dynamic=dynamic,
+                                                                  image_frequency=self.image_frequency))
+                    # Start a new move_to action. Append the new child action's initialization commands.
+                    commands.extend(self.set_move_to(resp=resp, static=static, dynamic=dynamic))
+            # The action ended in failure.
+            elif self.move_to.status != ActionStatus.ongoing:
+                self.status = self.move_to.status
+            return commands
+
+    def get_end_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                         image_frequency: ImageFrequency) -> List[dict]:
+        # Reset obstacle avoidance.
+        self.collision_detection.avoid = self.original_avoid
+        # Get end commands.
+        if self.move_to is not None:
+            return self.move_to.get_end_commands(resp=resp, static=static, dynamic=dynamic,
+                                                 image_frequency=image_frequency)
+        else:
+            return super().get_end_commands(resp=resp, static=static, dynamic=dynamic,
+                                            image_frequency=image_frequency)
+
+    def set_move_to(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Set the action.
+        self.move_to = MoveTo(target=self.path[self.path_index],
+                              collision_detection=self.collision_detection,
+                              reset_arms=True,
+                              reset_arms_duration=0.25,
+                              arrived_at=0.1,
+                              max_walk_cycles=100,
+                              bounds_position="center",
+                              previous=None)
+        # Update the path index
+        self.path_index += 1
+        # Return the initialization commands.
+        return self.move_to.get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                        image_frequency=self.image_frequency)
+
+
+class Pathfind(Controller):
+    """
+    An example of how to utilize the NavMesh to pathfind.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        self.replicant = Replicant(position={"x": 0.1, "y": 0, "z": -5})
+        self.camera: ThirdPersonCamera = ThirdPersonCamera(position={"x": 0, "y": 13.8, "z": 0},
+                                                           look_at={"x": 0, "y": 0, "z": 0},
+                                                           avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_navigate")
+        print(f"Images will be saved to: {path}")
+        self.capture: ImageCapture = ImageCapture(avatar_ids=["a"], path=path)
+        self.add_ons.extend([self.replicant, self.camera, self.capture])
+        # Set the object IDs.
+        self.trunk_id = Controller.get_unique_id()
+        self.chair_id = Controller.get_unique_id()
+        self.table_id = Controller.get_unique_id()
+        self.rocking_horse_id = Controller.get_unique_id()
+
+    def init_scene(self):
+        # Load the scene.
+        # Bake the NavMesh.
+        # Add objects to the scene and add NavMesh obstacles.
+        self.communicate([{"$type": "load_scene",
+                           "scene_name": "ProcGenScene"},
+                          TDWUtils.create_empty_room(12, 12),
+                          {"$type": "bake_nav_mesh"},
+                          Controller.get_add_object(model_name="trunck",
+                                                    object_id=self.trunk_id,
+                                                    position={"x": 1.5, "y": 0, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.trunk_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="chair_billiani_doll",
+                                                    object_id=self.chair_id,
+                                                    position={"x": -2.25, "y": 0, "z": 2.5},
+                                                    rotation={"x": 0, "y": 20, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.chair_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="live_edge_coffee_table",
+                                                    object_id=self.table_id,
+                                                    position={"x": 0.2, "y": 0, "z": -2.25},
+                                                    rotation={"x": 0, "y": 20, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.table_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="rh10",
+                                                    object_id=self.rocking_horse_id,
+                                                    position={"x": -1, "y": 0, "z": 1.5}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.rocking_horse_id,
+                           "carve_type": "stationary"}])
+
+    def do_action(self) -> None:
+        while self.replicant.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        self.communicate([])
+
+    def end(self) -> None:
+        self.communicate({"$type": "terminate"})
+
+
+if __name__ == "__main__":
+    c = Pathfind()
+    # Initialize the scene.
+    c.init_scene()
+    # Navigate to a destination.
+    c.replicant.action = Navigate(target={"x": 0, "y": 0, "z": 4},
+                                  collision_detection=c.replicant.collision_detection)
+    c.do_action()
+    print(c.replicant.action.status)
+    # End the simulation.
+    c.end()
diff --git a/Python/example_controllers/replicant/non_kinematic.py b/Python/example_controllers/replicant/non_kinematic.py
new file mode 100644
index 000000000..be0dd44a4
--- /dev/null
+++ b/Python/example_controllers/replicant/non_kinematic.py
@@ -0,0 +1,43 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+from tdw.replicant.action_status import ActionStatus
+
+"""
+Move a Replicant through a kinematic object and a wall.
+"""
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 2, "y": 1.6, "z": 1},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_non_kinematic")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=["a"],
+                       path=path)
+c.add_ons.extend([replicant, camera, capture])
+# Create the scene.
+commands = [TDWUtils.create_empty_room(12, 12)]
+# Add a kinematic object to the scene.
+commands.extend(Controller.get_add_physics_object(model_name="trunck",
+                                                  object_id=Controller.get_unique_id(),
+                                                  position={"x": 0, "y": 0, "z": 3},
+                                                  kinematic=True))
+c.communicate(commands)
+# Look at the Replicant.
+camera.look_at(replicant.replicant_id)
+# Disable obstacle avoidance.
+replicant.collision_detection.avoid = False
+# Disable checks for object collisions.
+replicant.collision_detection.objects = False
+# Start walking.
+replicant.move_by(20)
+# Continue walking until the action ends.
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/obstacle_avoidance.py b/Python/example_controllers/replicant/obstacle_avoidance.py
new file mode 100644
index 000000000..47b012aba
--- /dev/null
+++ b/Python/example_controllers/replicant/obstacle_avoidance.py
@@ -0,0 +1,99 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.image_frequency import ImageFrequency
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+class ObstacleAvoidance(Controller):
+    """
+    A very simple method for navigating around obstacles.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        # Set the target object's ID here so it can be referenced from more than one function.
+        self.target_id = Controller.get_unique_id()
+        # Set the replicant here so that it can be referenced from more than one function.
+        self.replicant = Replicant(position={"x": 0, "y": 0, "z": -8},
+                                   image_frequency=ImageFrequency.never)
+
+    def run(self) -> None:
+        # Enable image capture from a third-person camera.
+        camera = ThirdPersonCamera(position={"x": -0.5, "y": 1.175, "z": 8.45},
+                                   look_at={"x": 0.5, "y": 1, "z": 0},
+                                   avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_obstacle_avoidance")
+        print(f"Images will be saved to: {path}")
+        capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+        self.add_ons.extend([self.replicant, camera, capture])
+        # Create an empty room.
+        commands = [TDWUtils.create_empty_room(12, 20)]
+        # Add some objects.
+        commands.extend(Controller.get_add_physics_object(model_name="chair_billiani_doll",
+                                                          object_id=Controller.get_unique_id(),
+                                                          position={"x": 1.35, "y": 0, "z": 2.75},
+                                                          rotation={"x": 0, "y": 20, "z": 0},
+                                                          library="models_core.json"))
+        commands.extend(Controller.get_add_physics_object(model_name="live_edge_coffee_table",
+                                                          object_id=Controller.get_unique_id(),
+                                                          position={"x": 0, "y": 0, "z": 2},
+                                                          rotation={"x": 0, "y": 20, "z": 0},
+                                                          library="models_core.json"))
+        # Add a ball.
+        ball_id = Controller.get_unique_id()
+        commands.extend(Controller.get_add_physics_object(model_name="prim_sphere",
+                                                          object_id=ball_id,
+                                                          position={"x": 5, "y": 0, "z": 3.5},
+                                                          rotation={"x": 0, "y": 0, "z": 0},
+                                                          scale_factor={"x": 0.2, "y": 0.2, "z": 0.2},
+                                                          kinematic=True,
+                                                          gravity=False,
+                                                          library="models_special.json"))
+        # Add the target object.
+        commands.extend(Controller.get_add_physics_object(model_name="prim_sphere",
+                                                          object_id=self.target_id,
+                                                          position={"x": 0, "y": 0, "z": 4.0},
+                                                          rotation={"x": 0, "y": 0, "z": 0},
+                                                          scale_factor={"x": 0.2, "y": 0.2, "z": 0.2},
+                                                          kinematic=True,
+                                                          gravity=False,
+                                                          library="models_special.json"))
+        # Send the commands.
+        self.communicate(commands)
+        # Exclude the balls so they do not act as obstacles and trigger the avoidance mechanisms.
+        self.replicant.collision_detection.exclude_objects = [self.target_id, ball_id]
+        # Start to navigate.
+        done = False
+        while not done:
+            # Move to the target.
+            self.replicant.move_to(target=self.target_id)
+            self.do_action()
+            # We arrived at the target.
+            if self.replicant.action.status == ActionStatus.success:
+                done = True
+            # Take evasive action.
+            else:
+                self.replicant.move_by(distance=-0.5)
+                self.do_action()
+                self.replicant.turn_by(angle=45.0)
+                self.do_action()
+                self.replicant.move_by(distance=3.0)
+                self.do_action()
+        self.communicate({"$type": "terminate"})
+
+    def do_action(self) -> None:
+        """
+        Call `communicate([])` until the Replicant's action is done.
+        """
+
+        while self.replicant.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        self.communicate([])
+
+
+if __name__ == "__main__":
+    ObstacleAvoidance().run()
diff --git a/Python/example_controllers/replicant/pathfind.py b/Python/example_controllers/replicant/pathfind.py
new file mode 100644
index 000000000..6319ed420
--- /dev/null
+++ b/Python/example_controllers/replicant/pathfind.py
@@ -0,0 +1,128 @@
+from typing import Dict
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.controller import Controller
+from tdw.output_data import OutputData, NavMeshPath
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.action_status import ActionStatus
+
+
+class Pathfind(Controller):
+    """
+    An example of how to utilize the NavMesh to pathfind.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        self.replicant = Replicant(position={"x": 0.1, "y": 0, "z": -5})
+        self.camera: ThirdPersonCamera = ThirdPersonCamera(position={"x": 0, "y": 13.8, "z": 0},
+                                                           look_at={"x": 0, "y": 0, "z": 0},
+                                                           avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_pathfind")
+        print(f"Images will be saved to: {path}")
+        self.capture: ImageCapture = ImageCapture(avatar_ids=["a"], path=path)
+        self.add_ons.extend([self.replicant, self.camera, self.capture])
+        # Set the object IDs.
+        self.trunk_id = Controller.get_unique_id()
+        self.chair_id = Controller.get_unique_id()
+        self.table_id = Controller.get_unique_id()
+        self.rocking_horse_id = Controller.get_unique_id()
+
+    def init_scene(self):
+        # Load the scene.
+        # Bake the NavMesh.
+        # Add objects to the scene and add NavMesh obstacles.
+        self.communicate([{"$type": "load_scene",
+                           "scene_name": "ProcGenScene"},
+                          TDWUtils.create_empty_room(12, 12),
+                          {"$type": "bake_nav_mesh"},
+                          Controller.get_add_object(model_name="trunck",
+                                                    object_id=self.trunk_id,
+                                                    position={"x": 1.5, "y": 0, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.trunk_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="chair_billiani_doll",
+                                                    object_id=self.chair_id,
+                                                    position={"x": -2.25, "y": 0, "z": 2.5},
+                                                    rotation={"x": 0, "y": 20, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.chair_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="live_edge_coffee_table",
+                                                    object_id=self.table_id,
+                                                    position={"x": 0.2, "y": 0, "z": -2.25},
+                                                    rotation={"x": 0, "y": 20, "z": 0}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.table_id,
+                           "carve_type": "stationary"},
+                          Controller.get_add_object(model_name="rh10",
+                                                    object_id=self.rocking_horse_id,
+                                                    position={"x": -1, "y": 0, "z": 1.5}),
+                          {"$type": "make_nav_mesh_obstacle",
+                           "id": self.rocking_horse_id,
+                           "carve_type": "stationary"}])
+
+    def navigate(self, destination: Dict[str, float]) -> bool:
+        # Don't handle collision detection (it will interfere with NavMesh pathfinding).
+        self.replicant.collision_detection.avoid = False
+        # The origin of the path is the current position of the Replicant.
+        origin = TDWUtils.array_to_vector3(self.replicant.dynamic.transform.position)
+        # Request a NavMeshPath.
+        resp = self.communicate({"$type": "send_nav_mesh_path",
+                                 "origin": origin,
+                                 "destination": destination,
+                                 "id": self.replicant.replicant_id})
+        # Parse the output data to get the path.
+        path: np.ndarray = np.zeros(shape=1)
+        for i in range(len(resp) - 1):
+            r_id = OutputData.get_data_type_id(resp[i])
+            if r_id == "path":
+                nav_mesh_path = NavMeshPath(resp[i])
+                # This path belongs to the Replicant (this is useful when there is more than one agent).
+                if nav_mesh_path.get_id() == self.replicant.replicant_id:
+                    # We failed to get a valid path.
+                    if nav_mesh_path.get_state() != "complete":
+                        self.replicant.collision_detection.avoid = True
+                        return False
+                    # Get the path.
+                    path = nav_mesh_path.get_path()
+                    # Break because we have the data we need.
+                    break
+        # Move to each waypoint. Ignore the first waypoint because it's the position of the Replicant.
+        for i in range(1, path.shape[0]):
+            target = TDWUtils.array_to_vector3(path[i])
+            target["y"] = 0
+            # Move to the waypoint.
+            self.replicant.move_to(target=target)
+            # Do the action.
+            self.do_action()
+            # If the Replicant failed to reach the waypoint, end here.
+            if self.replicant.action.status != ActionStatus.success:
+                self.replicant.collision_detection.avoid = True
+                return False
+        # We arrived at the destination. Re-enable obstacle detection and return True.
+        self.replicant.collision_detection.avoid = True
+        return True
+
+    def do_action(self) -> None:
+        while self.replicant.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        self.communicate([])
+
+    def end(self) -> None:
+        self.communicate({"$type": "terminate"})
+
+
+if __name__ == "__main__":
+    c = Pathfind()
+    # Initialize the scene.
+    c.init_scene()
+    # Navigate to a destination.
+    success = c.navigate(destination={"x": 0, "y": 0, "z": 4})
+    print(success)
+    # End the simulation.
+    c.end()
diff --git a/Python/example_controllers/replicant/reach_for_follow.py b/Python/example_controllers/replicant/reach_for_follow.py
new file mode 100644
index 000000000..2e11e7c38
--- /dev/null
+++ b/Python/example_controllers/replicant/reach_for_follow.py
@@ -0,0 +1,39 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+"""
+Reach for a target position and have the offhand follow the main hand.
+"""
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 0, "y": 1.5, "z": 2.5},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reach_for_follow")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate(TDWUtils.create_empty_room(12, 12))
+# Reach for a target with the right hand.
+replicant.reach_for(target={"x": 0.6, "y": 1.5, "z": 0.3}, arm=Arm.right)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+# Reach for a target with the left hand.
+replicant.reach_for(target={"x": -0.4, "y": 1, "z": 0.1}, arm=Arm.left)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+# Reach for a target with the right hand and have the left hand follow.
+replicant.reach_for(target={"x": 0.8, "y": 0.8, "z": 0.3}, arm=Arm.right, offhand_follows=True)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/reach_for_move.py b/Python/example_controllers/replicant/reach_for_move.py
new file mode 100644
index 000000000..30abbd3d4
--- /dev/null
+++ b/Python/example_controllers/replicant/reach_for_move.py
@@ -0,0 +1,32 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+"""
+Reach for a target position and then move forward, resetting the arm.
+"""
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 2, "y": 3, "z": 2.53},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reach_for_move")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate(TDWUtils.create_empty_room(12, 12))
+replicant.reach_for(target={"x": 0.8, "y": 0.9, "z": 0.3}, arm=Arm.right)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+replicant.move_by(distance=3)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/reach_for_object.py b/Python/example_controllers/replicant/reach_for_object.py
new file mode 100644
index 000000000..b1d873d31
--- /dev/null
+++ b/Python/example_controllers/replicant/reach_for_object.py
@@ -0,0 +1,34 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+"""
+Reach for a target object.
+"""
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 0, "y": 1.5, "z": 2.5},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reach_for_object")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+# Set the object ID.
+object_id = Controller.get_unique_id()
+commands = [TDWUtils.create_empty_room(12, 12)]
+commands.extend(Controller.get_add_physics_object(model_name="basket_18inx18inx12iin_wicker",
+                                                  object_id=object_id,
+                                                  position={"x": -0.1, "y": 0, "z": 0.6}))
+c.communicate(commands)
+replicant.reach_for(target=object_id, arm=Arm.right)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/reach_for_position.py b/Python/example_controllers/replicant/reach_for_position.py
new file mode 100644
index 000000000..f1539b398
--- /dev/null
+++ b/Python/example_controllers/replicant/reach_for_position.py
@@ -0,0 +1,28 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+"""
+Reach for a target position.
+"""
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 0, "y": 1.5, "z": 2.1},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reach_for_position")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate(TDWUtils.create_empty_room(12, 12))
+replicant.reach_for(target={"x": 0.8, "y": 0.9, "z": 0.3}, arm=Arm.right)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/reach_too_far.py b/Python/example_controllers/replicant/reach_too_far.py
new file mode 100644
index 000000000..228220ee0
--- /dev/null
+++ b/Python/example_controllers/replicant/reach_too_far.py
@@ -0,0 +1,39 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+"""
+Reach for a target that is too far away.
+"""
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 3.2, "y": 3, "z": 3.5},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reach_too_far")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate([TDWUtils.create_empty_room(12, 12)])
+target = {"x": 3, "y": 0.9, "z": 6}
+# The target is too far away.
+replicant.reach_for(target=target, arm=Arm.right)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+print(replicant.action.status)
+# Try to reach the target anyway.
+replicant.reach_for(target=target,
+                    arm=Arm.right,
+                    max_distance=8)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+print(replicant.action.status)
+c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/reset.py b/Python/example_controllers/replicant/reset.py
new file mode 100644
index 000000000..38479c1ca
--- /dev/null
+++ b/Python/example_controllers/replicant/reset.py
@@ -0,0 +1,55 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+class Reset(Controller):
+    """
+    Run a series of trials and reset the scene each time.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        self.replicant = Replicant()
+        self.camera = ThirdPersonCamera(position={"x": 2, "y": 3, "z": 2.53},
+                                        look_at=self.replicant.replicant_id,
+                                        avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reset")
+        print(f"Images will be saved to: {path}")
+        self.capture = ImageCapture(avatar_ids=[self.camera.avatar_id], path=path)
+        self.add_ons.extend([self.replicant, self.camera, self.capture])
+
+    def trial(self, distance: float):
+        # Load a new scene.
+        self.communicate([{"$type": "load_scene",
+                           "scene_name": "ProcGenScene"},
+                          TDWUtils.create_empty_room(12, 12)])
+        self.replicant.move_by(distance=distance)
+        while self.replicant.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        self.communicate([])
+        # End the trial by resetting the add-ons.
+        self.replicant.reset()
+        self.camera.initialized = False
+        # Remember the current frame count.
+        f = self.capture.frame
+        # Reset image capture.
+        self.capture.initialized = False
+        # Set the current frame count.
+        self.capture.frame = f
+
+    def end(self) -> None:
+        # Clear the add-ons so that nothing tries to initialize when we terminate.
+        self.add_ons.clear()
+        self.communicate({"$type": "terminate"})
+
+
+if __name__ == "__main__":
+    c = Reset()
+    for d in [1, 2, 3]:
+        c.trial(distance=d)
+    c.end()
diff --git a/Python/example_controllers/replicant/reset_arm.py b/Python/example_controllers/replicant/reset_arm.py
new file mode 100644
index 000000000..c47d3376b
--- /dev/null
+++ b/Python/example_controllers/replicant/reset_arm.py
@@ -0,0 +1,32 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+"""
+Reach for a target position and then reset the arm.
+"""
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": 0, "y": 1.5, "z": 2.5},
+                           look_at=replicant.replicant_id,
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_reset_arm")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate(TDWUtils.create_empty_room(12, 12))
+replicant.reach_for(target={"x": 0.8, "y": 0.9, "z": 0.3}, arm=Arm.right)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+replicant.reset_arm(arm=Arm.right)
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/smpl_animate.py b/Python/example_controllers/replicant/smpl_animate.py
new file mode 100644
index 000000000..91bb65bbe
--- /dev/null
+++ b/Python/example_controllers/replicant/smpl_animate.py
@@ -0,0 +1,29 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.add_ons.replicant import Replicant
+from tdw.replicant.action_status import ActionStatus
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+"""
+Minimal example of how to play an animation from a non-default library.
+"""
+
+c = Controller()
+replicant = Replicant()
+camera = ThirdPersonCamera(position={"x": -0.5, "y": 1.175, "z": 3},
+                           look_at={"x": 0, "y": 1, "z": 0},
+                           avatar_id="a")
+path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_idle_to_crouch")
+print(f"Images will be saved to: {path}")
+capture = ImageCapture(avatar_ids=["a"], path=path)
+c.add_ons.extend([replicant, camera, capture])
+c.communicate(TDWUtils.create_empty_room(12, 12))
+# Play an animation. Note that we've set the library parameter.
+replicant.animate(animation="idle_to_crouch",
+                  library="smpl_animations.json")
+while replicant.action.status == ActionStatus.ongoing:
+    c.communicate([])
+c.communicate([])
+c.communicate({"$type": "terminate"})
diff --git a/Python/setup.py b/Python/setup.py
index bde0b61c2..5ce150024 100644
--- a/Python/setup.py
+++ b/Python/setup.py
@@ -2,7 +2,7 @@
 from pathlib import Path
 import re
 
-__version__ = "1.10.9.0"
+__version__ = "1.11.0.0"
 readme_path = Path('../README.md')
 if readme_path.exists():
     long_description = readme_path.read_text(encoding='utf-8')
diff --git a/Python/tdw/FBOutput/Containment.py b/Python/tdw/FBOutput/Containment.py
new file mode 100644
index 000000000..d8238360a
--- /dev/null
+++ b/Python/tdw/FBOutput/Containment.py
@@ -0,0 +1,94 @@
+# automatically generated by the FlatBuffers compiler, do not modify
+
+# namespace: FBOutput
+
+import tdw.flatbuffers
+
+class Containment(object):
+    __slots__ = ['_tab']
+
+    @classmethod
+    def GetRootAsContainment(cls, buf, offset):
+        n = tdw.flatbuffers.encode.Get(tdw.flatbuffers.packer.uoffset, buf, offset)
+        x = Containment()
+        x.Init(buf, n + offset)
+        return x
+
+    # Containment
+    def Init(self, buf, pos):
+        self._tab = tdw.flatbuffers.table.Table(buf, pos)
+
+    # Containment
+    def Ids(self, j):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+        if o != 0:
+            a = self._tab.Vector(o)
+            return self._tab.Get(tdw.flatbuffers.number_types.Int32Flags, a + tdw.flatbuffers.number_types.UOffsetTFlags.py_type(j * 4))
+        return 0
+
+    # Containment
+    def IdsAsNumpy(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+        if o != 0:
+            return self._tab.GetVectorAsNumpy(tdw.flatbuffers.number_types.Int32Flags, o)
+        return 0
+
+    # Containment
+    def IdsLength(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+        if o != 0:
+            return self._tab.VectorLen(o)
+        return 0
+
+    # Containment
+    def Tag(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
+        if o != 0:
+            return self._tab.Get(tdw.flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
+        return 0
+
+    # Containment
+    def OverlapIds(self, j):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
+        if o != 0:
+            a = self._tab.Vector(o)
+            return self._tab.Get(tdw.flatbuffers.number_types.Int32Flags, a + tdw.flatbuffers.number_types.UOffsetTFlags.py_type(j * 4))
+        return 0
+
+    # Containment
+    def OverlapIdsAsNumpy(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
+        if o != 0:
+            return self._tab.GetVectorAsNumpy(tdw.flatbuffers.number_types.Int32Flags, o)
+        return 0
+
+    # Containment
+    def OverlapIdsLength(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
+        if o != 0:
+            return self._tab.VectorLen(o)
+        return 0
+
+    # Containment
+    def Env(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
+        if o != 0:
+            return bool(self._tab.Get(tdw.flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
+        return False
+
+    # Containment
+    def Walls(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
+        if o != 0:
+            return bool(self._tab.Get(tdw.flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
+        return False
+
+def ContainmentStart(builder): builder.StartObject(5)
+def ContainmentAddIds(builder, ids): builder.PrependUOffsetTRelativeSlot(0, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(ids), 0)
+def ContainmentStartIdsVector(builder, numElems): return builder.StartVector(4, numElems, 4)
+def ContainmentAddTag(builder, tag): builder.PrependUint8Slot(1, tag, 0)
+def ContainmentAddOverlapIds(builder, overlapIds): builder.PrependUOffsetTRelativeSlot(2, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(overlapIds), 0)
+def ContainmentStartOverlapIdsVector(builder, numElems): return builder.StartVector(4, numElems, 4)
+def ContainmentAddEnv(builder, env): builder.PrependBoolSlot(3, env, 0)
+def ContainmentAddWalls(builder, walls): builder.PrependBoolSlot(4, walls, 0)
+def ContainmentEnd(builder): return builder.EndObject()
diff --git a/Python/tdw/FBOutput/DynamicEmptyObjects.py b/Python/tdw/FBOutput/DynamicEmptyObjects.py
new file mode 100644
index 000000000..8ffd2b207
--- /dev/null
+++ b/Python/tdw/FBOutput/DynamicEmptyObjects.py
@@ -0,0 +1,46 @@
+# automatically generated by the FlatBuffers compiler, do not modify
+
+# namespace: FBOutput
+
+import tdw.flatbuffers
+
+class DynamicEmptyObjects(object):
+    __slots__ = ['_tab']
+
+    @classmethod
+    def GetRootAsDynamicEmptyObjects(cls, buf, offset):
+        n = tdw.flatbuffers.encode.Get(tdw.flatbuffers.packer.uoffset, buf, offset)
+        x = DynamicEmptyObjects()
+        x.Init(buf, n + offset)
+        return x
+
+    # DynamicEmptyObjects
+    def Init(self, buf, pos):
+        self._tab = tdw.flatbuffers.table.Table(buf, pos)
+
+    # DynamicEmptyObjects
+    def Positions(self, j):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+        if o != 0:
+            a = self._tab.Vector(o)
+            return self._tab.Get(tdw.flatbuffers.number_types.Float32Flags, a + tdw.flatbuffers.number_types.UOffsetTFlags.py_type(j * 4))
+        return 0
+
+    # DynamicEmptyObjects
+    def PositionsAsNumpy(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+        if o != 0:
+            return self._tab.GetVectorAsNumpy(tdw.flatbuffers.number_types.Float32Flags, o)
+        return 0
+
+    # DynamicEmptyObjects
+    def PositionsLength(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+        if o != 0:
+            return self._tab.VectorLen(o)
+        return 0
+
+def DynamicEmptyObjectsStart(builder): builder.StartObject(1)
+def DynamicEmptyObjectsAddPositions(builder, positions): builder.PrependUOffsetTRelativeSlot(0, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(positions), 0)
+def DynamicEmptyObjectsStartPositionsVector(builder, numElems): return builder.StartVector(4, numElems, 4)
+def DynamicEmptyObjectsEnd(builder): return builder.EndObject()
diff --git a/Python/tdw/FBOutput/EmptyObjects.py b/Python/tdw/FBOutput/EmptyObjects.py
deleted file mode 100644
index d1573d468..000000000
--- a/Python/tdw/FBOutput/EmptyObjects.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# automatically generated by the FlatBuffers compiler, do not modify
-
-# namespace: FBOutput
-
-import tdw.flatbuffers
-
-class EmptyObjects(object):
-    __slots__ = ['_tab']
-
-    @classmethod
-    def GetRootAsEmptyObjects(cls, buf, offset):
-        n = tdw.flatbuffers.encode.Get(tdw.flatbuffers.packer.uoffset, buf, offset)
-        x = EmptyObjects()
-        x.Init(buf, n + offset)
-        return x
-
-    # EmptyObjects
-    def Init(self, buf, pos):
-        self._tab = tdw.flatbuffers.table.Table(buf, pos)
-
-    # EmptyObjects
-    def Ids(self, j):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
-        if o != 0:
-            a = self._tab.Vector(o)
-            return self._tab.Get(tdw.flatbuffers.number_types.Int32Flags, a + tdw.flatbuffers.number_types.UOffsetTFlags.py_type(j * 4))
-        return 0
-
-    # EmptyObjects
-    def IdsAsNumpy(self):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
-        if o != 0:
-            return self._tab.GetVectorAsNumpy(tdw.flatbuffers.number_types.Int32Flags, o)
-        return 0
-
-    # EmptyObjects
-    def IdsLength(self):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
-        if o != 0:
-            return self._tab.VectorLen(o)
-        return 0
-
-    # EmptyObjects
-    def Positions(self, j):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
-        if o != 0:
-            a = self._tab.Vector(o)
-            return self._tab.Get(tdw.flatbuffers.number_types.Float32Flags, a + tdw.flatbuffers.number_types.UOffsetTFlags.py_type(j * 4))
-        return 0
-
-    # EmptyObjects
-    def PositionsAsNumpy(self):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
-        if o != 0:
-            return self._tab.GetVectorAsNumpy(tdw.flatbuffers.number_types.Float32Flags, o)
-        return 0
-
-    # EmptyObjects
-    def PositionsLength(self):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
-        if o != 0:
-            return self._tab.VectorLen(o)
-        return 0
-
-def EmptyObjectsStart(builder): builder.StartObject(2)
-def EmptyObjectsAddIds(builder, ids): builder.PrependUOffsetTRelativeSlot(0, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(ids), 0)
-def EmptyObjectsStartIdsVector(builder, numElems): return builder.StartVector(4, numElems, 4)
-def EmptyObjectsAddPositions(builder, positions): builder.PrependUOffsetTRelativeSlot(1, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(positions), 0)
-def EmptyObjectsStartPositionsVector(builder, numElems): return builder.StartVector(4, numElems, 4)
-def EmptyObjectsEnd(builder): return builder.EndObject()
diff --git a/Python/tdw/FBOutput/Replicants.py b/Python/tdw/FBOutput/Replicants.py
new file mode 100644
index 000000000..ee0b18709
--- /dev/null
+++ b/Python/tdw/FBOutput/Replicants.py
@@ -0,0 +1,214 @@
+# automatically generated by the FlatBuffers compiler, do not modify
+
+# namespace: FBOutput
+
+import tdw.flatbuffers
+
+class Replicants(object):
+    __slots__ = ['_tab']
+
+    @classmethod
+    def GetRootAsReplicants(cls, buf, offset):
+        n = tdw.flatbuffers.encode.Get(tdw.flatbuffers.packer.uoffset, buf, offset)
+        x = Replicants()
+        x.Init(buf, n + offset)
+        return x
+
+    # Replicants
+    def Init(self, buf, pos):
+        self._tab = tdw.flatbuffers.table.Table(buf, pos)
+
+    # Replicants
+    def Ids(self, j):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+        if o != 0:
+            a = self._tab.Vector(o)
+            return self._tab.Get(tdw.flatbuffers.number_types.Int32Flags, a + tdw.flatbuffers.number_types.UOffsetTFlags.py_type(j * 4))
+        return 0
+
+    # Replicants
+    def IdsAsNumpy(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+        if o != 0:
+            return self._tab.GetVectorAsNumpy(tdw.flatbuffers.number_types.Int32Flags, o)
+        return 0
+
+    # Replicants
+    def IdsLength(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+        if o != 0:
+            return self._tab.VectorLen(o)
+        return 0
+
+    # Replicants
+    def Positions(self, j):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
+        if o != 0:
+            a = self._tab.Vector(o)
+            return self._tab.Get(tdw.flatbuffers.number_types.Float32Flags, a + tdw.flatbuffers.number_types.UOffsetTFlags.py_type(j * 4))
+        return 0
+
+    # Replicants
+    def PositionsAsNumpy(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
+        if o != 0:
+            return self._tab.GetVectorAsNumpy(tdw.flatbuffers.number_types.Float32Flags, o)
+        return 0
+
+    # Replicants
+    def PositionsLength(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
+        if o != 0:
+            return self._tab.VectorLen(o)
+        return 0
+
+    # Replicants
+    def Rotations(self, j):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
+        if o != 0:
+            a = self._tab.Vector(o)
+            return self._tab.Get(tdw.flatbuffers.number_types.Float32Flags, a + tdw.flatbuffers.number_types.UOffsetTFlags.py_type(j * 4))
+        return 0
+
+    # Replicants
+    def RotationsAsNumpy(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
+        if o != 0:
+            return self._tab.GetVectorAsNumpy(tdw.flatbuffers.number_types.Float32Flags, o)
+        return 0
+
+    # Replicants
+    def RotationsLength(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
+        if o != 0:
+            return self._tab.VectorLen(o)
+        return 0
+
+    # Replicants
+    def Forwards(self, j):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
+        if o != 0:
+            a = self._tab.Vector(o)
+            return self._tab.Get(tdw.flatbuffers.number_types.Float32Flags, a + tdw.flatbuffers.number_types.UOffsetTFlags.py_type(j * 4))
+        return 0
+
+    # Replicants
+    def ForwardsAsNumpy(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
+        if o != 0:
+            return self._tab.GetVectorAsNumpy(tdw.flatbuffers.number_types.Float32Flags, o)
+        return 0
+
+    # Replicants
+    def ForwardsLength(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
+        if o != 0:
+            return self._tab.VectorLen(o)
+        return 0
+
+    # Replicants
+    def Held(self, j):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
+        if o != 0:
+            a = self._tab.Vector(o)
+            return self._tab.Get(tdw.flatbuffers.number_types.Int32Flags, a + tdw.flatbuffers.number_types.UOffsetTFlags.py_type(j * 4))
+        return 0
+
+    # Replicants
+    def HeldAsNumpy(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
+        if o != 0:
+            return self._tab.GetVectorAsNumpy(tdw.flatbuffers.number_types.Int32Flags, o)
+        return 0
+
+    # Replicants
+    def HeldLength(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(12))
+        if o != 0:
+            return self._tab.VectorLen(o)
+        return 0
+
+    # Replicants
+    def CollisionIds(self, j):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
+        if o != 0:
+            a = self._tab.Vector(o)
+            return self._tab.Get(tdw.flatbuffers.number_types.Int32Flags, a + tdw.flatbuffers.number_types.UOffsetTFlags.py_type(j * 4))
+        return 0
+
+    # Replicants
+    def CollisionIdsAsNumpy(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
+        if o != 0:
+            return self._tab.GetVectorAsNumpy(tdw.flatbuffers.number_types.Int32Flags, o)
+        return 0
+
+    # Replicants
+    def CollisionIdsLength(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(14))
+        if o != 0:
+            return self._tab.VectorLen(o)
+        return 0
+
+    # Replicants
+    def IsCollisions(self, j):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16))
+        if o != 0:
+            a = self._tab.Vector(o)
+            return self._tab.Get(tdw.flatbuffers.number_types.BoolFlags, a + tdw.flatbuffers.number_types.UOffsetTFlags.py_type(j * 1))
+        return 0
+
+    # Replicants
+    def IsCollisionsAsNumpy(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16))
+        if o != 0:
+            return self._tab.GetVectorAsNumpy(tdw.flatbuffers.number_types.BoolFlags, o)
+        return 0
+
+    # Replicants
+    def IsCollisionsLength(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(16))
+        if o != 0:
+            return self._tab.VectorLen(o)
+        return 0
+
+    # Replicants
+    def Statuses(self, j):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18))
+        if o != 0:
+            a = self._tab.Vector(o)
+            return self._tab.Get(tdw.flatbuffers.number_types.Uint8Flags, a + tdw.flatbuffers.number_types.UOffsetTFlags.py_type(j * 1))
+        return 0
+
+    # Replicants
+    def StatusesAsNumpy(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18))
+        if o != 0:
+            return self._tab.GetVectorAsNumpy(tdw.flatbuffers.number_types.Uint8Flags, o)
+        return 0
+
+    # Replicants
+    def StatusesLength(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(18))
+        if o != 0:
+            return self._tab.VectorLen(o)
+        return 0
+
+def ReplicantsStart(builder): builder.StartObject(8)
+def ReplicantsAddIds(builder, ids): builder.PrependUOffsetTRelativeSlot(0, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(ids), 0)
+def ReplicantsStartIdsVector(builder, numElems): return builder.StartVector(4, numElems, 4)
+def ReplicantsAddPositions(builder, positions): builder.PrependUOffsetTRelativeSlot(1, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(positions), 0)
+def ReplicantsStartPositionsVector(builder, numElems): return builder.StartVector(4, numElems, 4)
+def ReplicantsAddRotations(builder, rotations): builder.PrependUOffsetTRelativeSlot(2, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(rotations), 0)
+def ReplicantsStartRotationsVector(builder, numElems): return builder.StartVector(4, numElems, 4)
+def ReplicantsAddForwards(builder, forwards): builder.PrependUOffsetTRelativeSlot(3, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(forwards), 0)
+def ReplicantsStartForwardsVector(builder, numElems): return builder.StartVector(4, numElems, 4)
+def ReplicantsAddHeld(builder, held): builder.PrependUOffsetTRelativeSlot(4, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(held), 0)
+def ReplicantsStartHeldVector(builder, numElems): return builder.StartVector(4, numElems, 4)
+def ReplicantsAddCollisionIds(builder, collisionIds): builder.PrependUOffsetTRelativeSlot(5, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(collisionIds), 0)
+def ReplicantsStartCollisionIdsVector(builder, numElems): return builder.StartVector(4, numElems, 4)
+def ReplicantsAddIsCollisions(builder, isCollisions): builder.PrependUOffsetTRelativeSlot(6, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(isCollisions), 0)
+def ReplicantsStartIsCollisionsVector(builder, numElems): return builder.StartVector(1, numElems, 1)
+def ReplicantsAddStatuses(builder, statuses): builder.PrependUOffsetTRelativeSlot(7, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(statuses), 0)
+def ReplicantsStartStatusesVector(builder, numElems): return builder.StartVector(1, numElems, 1)
+def ReplicantsEnd(builder): return builder.EndObject()
diff --git a/Python/tdw/FBOutput/Robot.py b/Python/tdw/FBOutput/Robot.py
deleted file mode 100644
index 747536fee..000000000
--- a/Python/tdw/FBOutput/Robot.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# automatically generated by the FlatBuffers compiler, do not modify
-
-# namespace: FBOutput
-
-import tdw.flatbuffers
-
-class Robot(object):
-    __slots__ = ['_tab']
-
-    @classmethod
-    def GetRootAsRobot(cls, buf, offset):
-        n = tdw.flatbuffers.encode.Get(tdw.flatbuffers.packer.uoffset, buf, offset)
-        x = Robot()
-        x.Init(buf, n + offset)
-        return x
-
-    # Robot
-    def Init(self, buf, pos):
-        self._tab = tdw.flatbuffers.table.Table(buf, pos)
-
-    # Robot
-    def Id(self):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
-        if o != 0:
-            return self._tab.Get(tdw.flatbuffers.number_types.Int32Flags, o + self._tab.Pos)
-        return 0
-
-    # Robot
-    def Immovable(self):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
-        if o != 0:
-            return bool(self._tab.Get(tdw.flatbuffers.number_types.BoolFlags, o + self._tab.Pos))
-        return False
-
-    # Robot
-    def Transform(self):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
-        if o != 0:
-            x = o + self._tab.Pos
-            from .SimpleTransform import SimpleTransform
-            obj = SimpleTransform()
-            obj.Init(self._tab.Bytes, x)
-            return obj
-        return None
-
-    # Robot
-    def Joints(self, j):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
-        if o != 0:
-            x = self._tab.Vector(o)
-            x += tdw.flatbuffers.number_types.UOffsetTFlags.py_type(j) * 4
-            x = self._tab.Indirect(x)
-            from .RobotJoint import RobotJoint
-            obj = RobotJoint()
-            obj.Init(self._tab.Bytes, x)
-            return obj
-        return None
-
-    # Robot
-    def JointsLength(self):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(10))
-        if o != 0:
-            return self._tab.VectorLen(o)
-        return 0
-
-def RobotStart(builder): builder.StartObject(4)
-def RobotAddId(builder, id): builder.PrependInt32Slot(0, id, 0)
-def RobotAddImmovable(builder, immovable): builder.PrependBoolSlot(1, immovable, 0)
-def RobotAddTransform(builder, transform): builder.PrependStructSlot(2, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(transform), 0)
-def RobotAddJoints(builder, joints): builder.PrependUOffsetTRelativeSlot(3, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(joints), 0)
-def RobotStartJointsVector(builder, numElems): return builder.StartVector(4, numElems, 4)
-def RobotEnd(builder): return builder.EndObject()
diff --git a/Python/tdw/FBOutput/RobotJoint.py b/Python/tdw/FBOutput/RobotJoint.py
deleted file mode 100644
index befde0805..000000000
--- a/Python/tdw/FBOutput/RobotJoint.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# automatically generated by the FlatBuffers compiler, do not modify
-
-# namespace: FBOutput
-
-import tdw.flatbuffers
-
-class RobotJoint(object):
-    __slots__ = ['_tab']
-
-    @classmethod
-    def GetRootAsRobotJoint(cls, buf, offset):
-        n = tdw.flatbuffers.encode.Get(tdw.flatbuffers.packer.uoffset, buf, offset)
-        x = RobotJoint()
-        x.Init(buf, n + offset)
-        return x
-
-    # RobotJoint
-    def Init(self, buf, pos):
-        self._tab = tdw.flatbuffers.table.Table(buf, pos)
-
-    # RobotJoint
-    def Id(self):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
-        if o != 0:
-            return self._tab.Get(tdw.flatbuffers.number_types.Int32Flags, o + self._tab.Pos)
-        return 0
-
-    # RobotJoint
-    def Position(self, j):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
-        if o != 0:
-            a = self._tab.Vector(o)
-            return self._tab.Get(tdw.flatbuffers.number_types.Float32Flags, a + tdw.flatbuffers.number_types.UOffsetTFlags.py_type(j * 4))
-        return 0
-
-    # RobotJoint
-    def PositionAsNumpy(self):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
-        if o != 0:
-            return self._tab.GetVectorAsNumpy(tdw.flatbuffers.number_types.Float32Flags, o)
-        return 0
-
-    # RobotJoint
-    def PositionLength(self):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
-        if o != 0:
-            return self._tab.VectorLen(o)
-        return 0
-
-    # RobotJoint
-    def Positions(self, j):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
-        if o != 0:
-            a = self._tab.Vector(o)
-            return self._tab.Get(tdw.flatbuffers.number_types.Float32Flags, a + tdw.flatbuffers.number_types.UOffsetTFlags.py_type(j * 4))
-        return 0
-
-    # RobotJoint
-    def PositionsAsNumpy(self):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
-        if o != 0:
-            return self._tab.GetVectorAsNumpy(tdw.flatbuffers.number_types.Float32Flags, o)
-        return 0
-
-    # RobotJoint
-    def PositionsLength(self):
-        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
-        if o != 0:
-            return self._tab.VectorLen(o)
-        return 0
-
-def RobotJointStart(builder): builder.StartObject(3)
-def RobotJointAddId(builder, id): builder.PrependInt32Slot(0, id, 0)
-def RobotJointAddPosition(builder, position): builder.PrependUOffsetTRelativeSlot(1, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(position), 0)
-def RobotJointStartPositionVector(builder, numElems): return builder.StartVector(4, numElems, 4)
-def RobotJointAddPositions(builder, positions): builder.PrependUOffsetTRelativeSlot(2, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(positions), 0)
-def RobotJointStartPositionsVector(builder, numElems): return builder.StartVector(4, numElems, 4)
-def RobotJointEnd(builder): return builder.EndObject()
diff --git a/Python/tdw/FBOutput/StaticEmptyObjects.py b/Python/tdw/FBOutput/StaticEmptyObjects.py
new file mode 100644
index 000000000..f4870a5a1
--- /dev/null
+++ b/Python/tdw/FBOutput/StaticEmptyObjects.py
@@ -0,0 +1,46 @@
+# automatically generated by the FlatBuffers compiler, do not modify
+
+# namespace: FBOutput
+
+import tdw.flatbuffers
+
+class StaticEmptyObjects(object):
+    __slots__ = ['_tab']
+
+    @classmethod
+    def GetRootAsStaticEmptyObjects(cls, buf, offset):
+        n = tdw.flatbuffers.encode.Get(tdw.flatbuffers.packer.uoffset, buf, offset)
+        x = StaticEmptyObjects()
+        x.Init(buf, n + offset)
+        return x
+
+    # StaticEmptyObjects
+    def Init(self, buf, pos):
+        self._tab = tdw.flatbuffers.table.Table(buf, pos)
+
+    # StaticEmptyObjects
+    def Ids(self, j):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+        if o != 0:
+            a = self._tab.Vector(o)
+            return self._tab.Get(tdw.flatbuffers.number_types.Int32Flags, a + tdw.flatbuffers.number_types.UOffsetTFlags.py_type(j * 4))
+        return 0
+
+    # StaticEmptyObjects
+    def IdsAsNumpy(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+        if o != 0:
+            return self._tab.GetVectorAsNumpy(tdw.flatbuffers.number_types.Int32Flags, o)
+        return 0
+
+    # StaticEmptyObjects
+    def IdsLength(self):
+        o = tdw.flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+        if o != 0:
+            return self._tab.VectorLen(o)
+        return 0
+
+def StaticEmptyObjectsStart(builder): builder.StartObject(1)
+def StaticEmptyObjectsAddIds(builder, ids): builder.PrependUOffsetTRelativeSlot(0, tdw.flatbuffers.number_types.UOffsetTFlags.py_type(ids), 0)
+def StaticEmptyObjectsStartIdsVector(builder, numElems): return builder.StartVector(4, numElems, 4)
+def StaticEmptyObjectsEnd(builder): return builder.EndObject()
diff --git a/Python/tdw/add_ons/container_manager.py b/Python/tdw/add_ons/container_manager.py
index 501cef600..031e95f5b 100644
--- a/Python/tdw/add_ons/container_manager.py
+++ b/Python/tdw/add_ons/container_manager.py
@@ -1,15 +1,10 @@
 from typing import List, Dict
 import numpy as np
-from tdw.output_data import OutputData, SegmentationColors, StaticCompositeObjects, Overlap
+from tdw.output_data import OutputData, StaticCompositeObjects, Containment
 from tdw.add_ons.add_on import AddOn
 from tdw.container_data.container_tag import ContainerTag
-from tdw.container_data.box_container import BoxContainer
-from tdw.container_data.sphere_container import SphereContainer
-from tdw.container_data.cylinder_container import CylinderContainer
 from tdw.container_data.containment_event import ContainmentEvent
 from tdw.object_data.composite_object.composite_object_static import CompositeObjectStatic
-from tdw.controller import Controller
-from tdw.librarian import ModelLibrarian
 
 
 class ContainerManager(AddOn):
@@ -21,15 +16,13 @@ class ContainerManager(AddOn):
     An object is 'contained' by a 'container' if it overlaps with a "containment" space, for example the interior of a pot.
     """
 
-    _NEXT_CONTAINER_ID: int = 0
-
     def __init__(self):
         """
         (no parameters)
         """
 
         super().__init__()
-        self._getting_model_names: bool = True
+        self._getting_static_data: bool = True
         """:field
         A dictionary describing which objects contain other objects on this frame. This is updated per-frame. Key = The container shape ID (not the object ID). Value = A list of [`ContainmentEvent`](../container_data/containment_event.md) data.
         """
@@ -45,143 +38,40 @@ def __init__(self):
         self._excluded_objects: List[int] = list()
 
     def get_initialization_commands(self) -> List[dict]:
-        return [{"$type": "send_segmentation_colors"},
-                {"$type": "send_static_composite_objects"},
+        return [{"$type": "send_static_composite_objects"},
                 {"$type": "send_containment",
                  "frequency": "always"}]
 
     def on_send(self, resp: List[bytes]) -> None:
         # Get model names.
-        if self._getting_model_names:
-            self._getting_model_names = False
+        if self._getting_static_data:
+            self._getting_static_data = False
             for i in range(len(resp) - 1):
                 r_id = OutputData.get_data_type_id(resp[i])
-                # Use the model names from SegmentationColors output data to add container shapes.
-                if r_id == "segm":
-                    segmentation_colors = SegmentationColors(resp[i])
-                    for j in range(segmentation_colors.get_num()):
-                        object_id = segmentation_colors.get_object_id(j)
-                        model_name = segmentation_colors.get_object_name(j).lower()
-                        # Add the model librarians.
-                        for library_path in ModelLibrarian.get_library_filenames():
-                            if library_path not in Controller.MODEL_LIBRARIANS:
-                                Controller.MODEL_LIBRARIANS[library_path] = ModelLibrarian(library_path)
-                        # Fine the model record.
-                        for library_path in Controller.MODEL_LIBRARIANS:
-                            record = Controller.MODEL_LIBRARIANS[library_path].get_record(model_name)
-                            if record is not None:
-                                for container_shape in record.container_shapes:
-                                    if isinstance(container_shape, BoxContainer):
-                                        self.add_box(object_id=object_id,
-                                                     position=container_shape.position,
-                                                     tag=container_shape.tag,
-                                                     half_extents=container_shape.half_extents,
-                                                     rotation=container_shape.rotation)
-                                    elif isinstance(container_shape, CylinderContainer):
-                                        self.add_cylinder(object_id=object_id,
-                                                          position=container_shape.position,
-                                                          tag=container_shape.tag,
-                                                          radius=container_shape.radius,
-                                                          height=container_shape.height,
-                                                          rotation=container_shape.rotation)
-                                    elif isinstance(container_shape, SphereContainer):
-                                        self.add_sphere(object_id=object_id,
-                                                        position=container_shape.position,
-                                                        tag=container_shape.tag,
-                                                        radius=container_shape.radius)
-                                    else:
-                                        raise Exception(container_shape)
-                                break
-                elif r_id == "scom":
+                # Exclude composite sub-objects.
+                if r_id == "scom":
                     static_composite_objects = StaticCompositeObjects(resp[i])
                     for j in range(static_composite_objects.get_num()):
                         s = CompositeObjectStatic(static_composite_objects, j)
                         self._excluded_objects.extend(s.sub_object_ids)
-        # Get containment.
+                    break
         self.events.clear()
         for i in range(len(resp) - 1):
-            if OutputData.get_data_type_id(resp[i]) == "over":
-                overlap = Overlap(resp[i])
-                overlap_id = overlap.get_id()
-                # This overlap is from a container.
-                if overlap_id in self.container_shapes:
-                    # Get the object ID.
-                    object_id = self.container_shapes[overlap_id]
-                    # Get the IDs of the contained objects.
-                    contained_ids = np.array([o_id for o_id in overlap.get_object_ids() if int(o_id) not in self._excluded_objects], dtype=int)
-                    if len(contained_ids) > 0:
-                        # Record the containment event.
-                        self.events[overlap_id] = ContainmentEvent(container_id=object_id,
-                                                                   object_ids=contained_ids,
-                                                                   tag=self.tags[overlap_id])
-
-    def add_box(self, object_id: int, position: Dict[str, float], tag: ContainerTag, half_extents: Dict[str, float],
-                rotation: Dict[str, float]) -> int:
-        """
-        Add a box container shape to an object.
-
-        :param object_id: The ID of the object.
-        :param position: The position of the box relative to the parent object.
-        :param tag: The box's semantic [`ContainerTag`](../container_data/container_tag.md).
-        :param half_extents: The half-extents (half the scale) of the box.
-        :param rotation: The rotation of the box in Euler angles relative to the parent object.
-
-        :return: The ID of the container shape.
-        """
-
-        command = self._get_container_shape_command(command_name="add_box_container",
-                                                    object_id=object_id,
-                                                    position=position,
-                                                    tag=tag)
-        command["half_extents"] = half_extents
-        command["rotation"] = rotation
-        self.commands.append(command)
-        return command["id"]
-
-    def add_cylinder(self, object_id: int, position: Dict[str, float], tag: ContainerTag, radius: float,
-                     height: float, rotation: Dict[str, float]) -> int:
-        """
-        Add a cylinder container shape to an object.
-
-        :param object_id: The ID of the object.
-        :param position: The position of the cylinder relative to the parent object.
-        :param tag: The cylinder's semantic [`ContainerTag`](../container_data/container_tag.md).
-        :param radius: The radius of the cylinder.
-        :param height: The height of the cylinder.
-        :param rotation: The rotation of the cylinder in Euler angles relative to the parent object.
-
-        :return: The ID of the container shape.
-        """
-
-        command = self._get_container_shape_command(command_name="add_cylinder_container",
-                                                    object_id=object_id,
-                                                    position=position,
-                                                    tag=tag)
-        command["radius"] = radius
-        command["height"] = height
-        command["rotation"] = rotation
-        self.commands.append(command)
-        return command["id"]
-
-    def add_sphere(self, object_id: int, position: Dict[str, float], tag: ContainerTag, radius: float) -> int:
-        """
-        Add a sphere container shape to an object.
-
-        :param object_id: The ID of the object.
-        :param position: The position of the sphere relative to the parent object.
-        :param tag: The sphere's semantic [`ContainerTag`](../container_data/container_tag.md).
-        :param radius: The radius of the sphere.
-
-        :return: The ID of the container shape.
-        """
-
-        command = self._get_container_shape_command(command_name="add_sphere_container",
-                                                    object_id=object_id,
-                                                    position=position,
-                                                    tag=tag)
-        command["radius"] = radius
-        self.commands.append(command)
-        return command["id"]
+            r_id = OutputData.get_data_type_id(resp[i])
+            # Use the model names from SegmentationColors output data to add container shapes.
+            if r_id == "cont":
+                containment = Containment(resp[i])
+                container_id = containment.get_container_id()
+                tag = containment.get_tag()
+                # Add the shape.
+                if container_id not in self.container_shapes:
+                    self.container_shapes[container_id] = containment.get_object_id()
+                    self.tags[container_id] = tag
+                # Add the event.
+                self.events[container_id] = ContainmentEvent(container_id=container_id,
+                                                             object_ids=np.array([o_id for o_id in containment.get_overlap_ids()
+                                                                                  if int(o_id) not in self._excluded_objects], dtype=int),
+                                                             tag=tag)
 
     def reset(self) -> None:
         """
@@ -190,34 +80,8 @@ def reset(self) -> None:
 
         self.initialized = False
         self.commands.clear()
-        self._getting_model_names = True
+        self._getting_static_data = True
         self.events.clear()
         self.tags.clear()
         self._excluded_objects.clear()
         self.container_shapes.clear()
-        ContainerManager._NEXT_CONTAINER_ID = 0
-
-    def _get_container_shape_command(self, command_name: str, object_id: int, position: Dict[str, float],
-                                     tag: ContainerTag) -> dict:
-        """
-        :param command_name: The name of the command.
-        :param object_id: The object ID.
-        :param tag: The semantic tag.
-        :param position: The local position of the container shape.
-
-        :return: A partial command to add a container shape to an object.
-        """
-
-        # Get the container ID.
-        container_id = ContainerManager._NEXT_CONTAINER_ID
-        ContainerManager._NEXT_CONTAINER_ID += 1
-        assert container_id not in self.tags, f"Tried adding {container_id} but it already exists."
-        # Record the tag.
-        self.tags[container_id] = tag
-        # Record the object-shape association.
-        self.container_shapes[container_id] = object_id
-        # Return a partial command.
-        return {"$type": command_name,
-                "id": int(object_id),
-                "container_id": int(container_id),
-                "position": position}
diff --git a/Python/tdw/add_ons/replicant.py b/Python/tdw/add_ons/replicant.py
new file mode 100644
index 000000000..7c0d84b35
--- /dev/null
+++ b/Python/tdw/add_ons/replicant.py
@@ -0,0 +1,508 @@
+from __future__ import annotations
+from typing import List, Optional, Dict, Union
+from copy import deepcopy
+import numpy as np
+from tdw.add_ons.add_on import AddOn
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.actions.action import Action
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.actions.turn_by import TurnBy
+from tdw.replicant.actions.turn_to import TurnTo
+from tdw.replicant.actions.move_by import MoveBy
+from tdw.replicant.actions.move_to import MoveTo
+from tdw.replicant.actions.reach_for import ReachFor
+from tdw.replicant.actions.grasp import Grasp
+from tdw.replicant.actions.drop import Drop
+from tdw.replicant.actions.reset_arm import ResetArm
+from tdw.replicant.actions.animate import Animate
+from tdw.replicant.actions.look_at import LookAt
+from tdw.replicant.actions.reset_head import ResetHead
+from tdw.replicant.image_frequency import ImageFrequency
+from tdw.replicant.arm import Arm
+from tdw.librarian import HumanoidRecord, HumanoidLibrarian
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+
+
+class Replicant(AddOn):
+    """
+    A Replicant is a human-like agent that can interact with the scene with pseudo-physics behavior.
+
+    When a Replicant collides with objects, it initiates a physics-driven collision. The Replicant's own movements are driven by non-physics animation.
+
+    A Replicant can walk, turn, reach for positions or objects, grasp and drop objects, and turn its head to look around.
+    """
+
+    """:class_var
+    The Replicants library file. You can override this to use a custom library (e.g. a local library).
+    """
+    LIBRARY_NAME: str = "replicants.json"
+
+    def __init__(self, replicant_id: int = 0, position: Union[Dict[str, float], np.ndarray] = None,
+                 rotation: Union[Dict[str, float], np.ndarray] = None,
+                 image_frequency: ImageFrequency = ImageFrequency.once, name: str = "replicant_0"):
+        """
+        :param replicant_id: The ID of the Replicant.
+        :param position: The position of the Replicant as an x, y, z dictionary or numpy array. If None, defaults to `{"x": 0, "y": 0, "z": 0}`.
+        :param rotation: The rotation of the Replicant in Euler angles (degrees) as an x, y, z dictionary or numpy array. If None, defaults to `{"x": 0, "y": 0, "z": 0}`.
+        :param image_frequency: An [`ImageFrequency`](../replicant/image_frequency.md) value that sets how often images are captured.
+        :param name: The name of the Replicant model.
+        """
+
+        super().__init__()
+        """:field
+        The initial position of the Replicant.
+        """
+        self.initial_position: Dict[str, float] = {"x": 0, "y": 0, "z": 0}
+        """:field
+        The initial rotation of the Replicant.
+        """
+        self.initial_rotation: Dict[str, float] = {"x": 0, "y": 0, "z": 0}
+        self._set_initial_position_and_rotation(position=position, rotation=rotation)
+        """:field
+        The [`ReplicantStatic`](../replicant/replicant_static.md) data.
+        """
+        self.static: Optional[ReplicantStatic] = None
+        """:field
+        The [`ReplicantDynamic`](../replicant/replicant_dynamic.md) data.
+        """
+        self.dynamic: Optional[ReplicantDynamic] = None
+        """:field
+        The ID of this replicant.
+        """
+        self.replicant_id: int = replicant_id
+        """:field
+        The Replicant's current [action](../replicant/actions/action.md). Can be None (no ongoing action).
+        """
+        self.action: Optional[Action] = None
+        """:field
+        An [`ImageFrequency`](../replicant/image_frequency.md) value that sets how often images are captured.
+        """
+        self.image_frequency: ImageFrequency = image_frequency
+        """:field
+        [The collision detection rules.](../replicant/collision_detection.md) This determines whether the Replicant will immediately stop moving or turning when it collides with something.
+        """
+        self.collision_detection: CollisionDetection = CollisionDetection()
+        # This is used for collision detection. If the previous action is the "same" as this one, this action fails.
+        self._previous_action: Optional[Action] = None
+        # This is used when saving images.
+        self._frame_count: int = 0
+        # Initialize the Replicant metdata library.
+        if Replicant.LIBRARY_NAME not in Controller.HUMANOID_LIBRARIANS:
+            Controller.HUMANOID_LIBRARIANS[Replicant.LIBRARY_NAME] = HumanoidLibrarian(Replicant.LIBRARY_NAME)
+        # The Replicant metadata record.
+        self._record: HumanoidRecord = Controller.HUMANOID_LIBRARIANS[Replicant.LIBRARY_NAME].get_record(name)
+
+    def get_initialization_commands(self) -> List[dict]:
+        """
+        This function gets called exactly once per add-on. To re-initialize, set `self.initialized = False`.
+
+        :return: A list of commands that will initialize this add-on.
+        """
+
+        # Add the replicant. Send output data: Replicants, Transforms, Bounds, Containment.
+        commands = [{"$type": "add_replicant",
+                     "name": self._record.name,
+                     "position": self.initial_position,
+                     "rotation": self.initial_rotation,
+                     "url": self._record.get_url(),
+                     "id": self.replicant_id},
+                    {"$type": "add_replicant_rigidbody",
+                     "id": self.replicant_id},
+                    {"$type": "send_replicants",
+                     "frequency": "always"},
+                    {"$type": "send_transforms",
+                     "frequency": "always"},
+                    {"$type": "send_bounds",
+                     "frequency": "always"},
+                    {"$type": "send_containment",
+                     "frequency": "always"}]
+        return commands
+
+    def on_send(self, resp: List[bytes]) -> None:
+        """
+        This is called within `Controller.communicate(commands)` after commands are sent to the build and a response is received.
+
+        Use this function to send commands to the build on the next `Controller.communicate(commands)` call, given the `resp` response.
+        Any commands in the `self.commands` list will be sent on the *next* `Controller.communicate(commands)` call.
+
+        :param resp: The response from the build.
+        """
+
+        # If there isn't cached static data, assume we have the output data we need and cache it now.
+        if self.static is None:
+            self._cache_static_data(resp=resp)
+        # Update the dynamic data per `communicate()` call.
+        self._set_dynamic_data(resp=resp)
+        # Don't do anything if there isn't an action or if the action is done.
+        if self.action is None or self.action.done:
+            return
+        # Start or continue the action.
+        else:
+            # Initialize the action.
+            if not self.action.initialized:
+                # The action's status defaults to `ongoing`, but actions sometimes fail prior to initialization.
+                if self.action.status == ActionStatus.ongoing:
+                    # Initialize the action and get initialization commands.
+                    self.action.initialized = True
+                    initialization_commands = self.action.get_initialization_commands(resp=resp,
+                                                                                      static=self.static,
+                                                                                      dynamic=self.dynamic,
+                                                                                      image_frequency=self.image_frequency)
+
+                    # Most actions are `ongoing` after initialization, but they might've succeeded or failed already.
+                    if self.action.status == ActionStatus.ongoing:
+                        self.commands.extend(initialization_commands)
+                    else:
+                        self.commands.extend(self.action.get_end_commands(resp=resp,
+                                                                          static=self.static,
+                                                                          dynamic=self.dynamic,
+                                                                          image_frequency=self.image_frequency))
+            # Continue an ongoing action.
+            else:
+                # Get the ongoing action commands.
+                action_commands = self.action.get_ongoing_commands(resp=resp, static=self.static, dynamic=self.dynamic)
+                # This is an ongoing action. Append ongoing commands.
+                if self.action.status == ActionStatus.ongoing:
+                    self.commands.extend(action_commands)
+                # This action is done. Append end commands.
+                else:
+                    self.commands.extend(self.action.get_end_commands(resp=resp,
+                                                                      static=self.static,
+                                                                      dynamic=self.dynamic,
+                                                                      image_frequency=self.image_frequency))
+            # This action ended. Remember it as the previous action.
+            if self.action.status != ActionStatus.ongoing:
+                # Mark the action as done.
+                self.action.done = True
+                # Remember the previous action.
+                self._previous_action = deepcopy(self.action)
+
+    def turn_by(self, angle: float) -> None:
+        """
+        Turn the Replicant by an angle.
+
+        This is a non-animated action, meaning that the Replicant will immediately snap to the angle.
+
+        :param angle: The target angle in degrees. Positive value = clockwise turn.
+        """
+
+        self.action = TurnBy(angle=angle)
+
+    def turn_to(self, target: Union[int, Dict[str, float], np.ndarray]) -> None:
+        """
+        Turn the Replicant to face a target object or position.
+
+        This is a non-animated action, meaning that the Replicant will immediately snap to the angle.
+
+        :param target: The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array.
+        """
+
+        self.action = TurnTo(target=target)
+
+    def move_by(self, distance: float, reset_arms: bool = True, reset_arms_duration: float = 0.25,
+                arrived_at: float = 0.1, max_walk_cycles: int = 100) -> None:
+        """
+        Walk a given distance.
+
+        The Replicant will continuously play a walk cycle animation until the action ends.
+
+        The action can end for several reasons depending on the collision detection rules (see [`self.collision_detection`](../replicant/collision_detection.md).
+
+        - If the Replicant walks the target distance, the action succeeds.
+        - If `collision_detection.previous_was_same == True`, and the previous action was `move_by()` or `move_to()`, and it was in the same direction (forwards/backwards), and the previous action ended in failure, this action ends immediately.
+        - If `self.collision_detection.avoid_obstacles == True` and the Replicant encounters a wall or object in its path:
+          - If the object is in `self.collision_detection.exclude_objects`, the Replicant ignores it.
+          - Otherwise, the action ends in failure.
+        - If the Replicant collides with an object or a wall and `self.collision_detection.objects == True` and/or `self.collision_detection.walls == True` respectively:
+          - If the object is in `self.collision_detection.exclude_objects`, the Replicant ignores it.
+          - Otherwise, the action ends in failure.
+        - If the Replicant takes too long to reach the target distance, the action ends in failure (see `self.max_walk_cycles`).
+
+        :param distance: The target distance. If less than 0, the Replicant will walk backwards.
+        :param reset_arms: If True, reset the arms to their neutral positions while beginning the walk cycle.
+        :param reset_arms_duration: The speed at which the arms are reset in seconds.
+        :param arrived_at: If at any point during the action the difference between the target distance and distance traversed is less than this, then the action is successful.
+        :param max_walk_cycles: The walk animation will loop this many times maximum. If by that point the Replicant hasn't reached its destination, the action fails.
+        """
+
+        self.action = MoveBy(distance=distance,
+                             dynamic=self.dynamic,
+                             collision_detection=self.collision_detection,
+                             previous=self._previous_action,
+                             reset_arms=reset_arms,
+                             reset_arms_duration=reset_arms_duration,
+                             arrived_at=arrived_at,
+                             max_walk_cycles=max_walk_cycles)
+
+    def move_to(self, target: Union[int, Dict[str, float], np.ndarray], reset_arms: bool = True,
+                reset_arms_duration: float = 0.25, arrived_at: float = 0.1, max_walk_cycles: int = 100,
+                bounds_position: str = "center") -> None:
+        """
+        Turn the Replicant to a target position or object and then walk to it.
+
+        While walking, the Replicant will continuously play a walk cycle animation until the action ends.
+
+        The action can end for several reasons depending on the collision detection rules (see [`self.collision_detection`](../replicant/collision_detection.md).
+
+        - If the Replicant walks the target distance, the action succeeds.
+        - If `collision_detection.previous_was_same == True`, and the previous action was `move_by()` or `move_to()`, and it was in the same direction (forwards/backwards), and the previous action ended in failure, this action ends immediately.
+        - If `self.collision_detection.avoid_obstacles == True` and the Replicant encounters a wall or object in its path:
+          - If the object is in `self.collision_detection.exclude_objects`, the Replicant ignores it.
+          - Otherwise, the action ends in failure.
+        - If the Replicant collides with an object or a wall and `self.collision_detection.objects == True` and/or `self.collision_detection.walls == True` respectively:
+          - If the object is in `self.collision_detection.exclude_objects`, the Replicant ignores it.
+          - Otherwise, the action ends in failure.
+        - If the Replicant takes too long to reach the target distance, the action ends in failure (see `self.max_walk_cycles`).
+
+        :param target: The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array.
+        :param reset_arms: If True, reset the arms to their neutral positions while beginning the walk cycle.
+        :param reset_arms_duration: The speed at which the arms are reset in seconds.
+        :param arrived_at: If at any point during the action the difference between the target distance and distance traversed is less than this, then the action is successful.
+        :param max_walk_cycles: The walk animation will loop this many times maximum. If by that point the Replicant hasn't reached its destination, the action fails.
+        :param bounds_position: If `target` is an integer object ID, move towards this bounds point of the object. Options: `"center"`, `"top`", `"bottom"`, `"left"`, `"right"`, `"front"`, `"back"`.
+        """
+
+        self.action = MoveTo(target=target,
+                             collision_detection=self.collision_detection,
+                             previous=self._previous_action,
+                             reset_arms=reset_arms,
+                             reset_arms_duration=reset_arms_duration,
+                             arrived_at=arrived_at,
+                             max_walk_cycles=max_walk_cycles,
+                             bounds_position=bounds_position)
+
+    def reach_for(self, target: Union[int, Dict[str,  float], np.ndarray], arm: Union[Arm, List[Arm]],
+                  absolute: bool = True, offhand_follows: bool = False, arrived_at: float = 0.09,
+                  max_distance: float = 1.5, duration: float = 0.25) -> None:
+        """
+        Reach for a target object or position. One or both hands can reach for the target at the same time.
+
+        If target is an object, the target position is a point on the object.
+        If the object has affordance points, the target position is the affordance point closest to the hand.
+        Otherwise, the target position is the bounds position closest to the hand.
+
+        The Replicant's arm(s) will continuously over multiple `communicate()` calls move until either the motion is complete or the arm collides with something (see `self.collision_detection`).
+
+        - If the hand is near the target at the end of the action, the action succeeds.
+        - If the target is too far away at the start of the action, the action fails.
+        - The collision detection will respond normally to walls, objects, obstacle avoidance, etc.
+        - If `self.collision_detection.previous_was_same == True`, and if the previous action was a subclass of `ArmMotion`, and it ended in a collision, this action ends immediately.
+
+        :param target: The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array.
+        :param arm: The [`Arm`](../replicant/arm.md) value(s) that will reach for the `target` as a single value or a list. Example: `Arm.left` or `[Arm.left, Arm.right]`.
+        :param absolute: If True, the target position is in world space coordinates. If False, the target position is relative to the Replicant. Ignored if `target` is an int.
+        :param offhand_follows: If True, the offhand will follow the primary hand, meaning that it will maintain the same relative position. Ignored if `arm` is a list or `target` is an int.
+        :param arrived_at: If at the end of the action the hand(s) is this distance or less from the target position, the action succeeds.
+        :param max_distance: The maximum distance from the hand to the target position.
+        :param duration: The duration of the motion in seconds.
+        """
+
+        # Convert the relative position to an absolute position.
+        if not isinstance(target, int) and not absolute:
+            if isinstance(target, np.ndarray):
+                target = self.dynamic.transform.position + target
+            elif isinstance(target, dict):
+                target = self.dynamic.transform.position + TDWUtils.vector3_to_array(target)
+        self.action = ReachFor(target=target,
+                               arms=Replicant._arms_to_list(arm),
+                               dynamic=self.dynamic,
+                               collision_detection=self.collision_detection,
+                               offhand_follows=offhand_follows,
+                               arrived_at=arrived_at,
+                               previous=self._previous_action,
+                               duration=duration,
+                               max_distance=max_distance)
+
+    def grasp(self, target: int, arm: Arm, angle: Optional[float] = 90, axis: Optional[str] = "pitch") -> None:
+        """
+        Grasp a target object.
+
+        The action fails if the hand is already holding an object. Otherwise, the action succeeds.
+
+        When an object is grasped, it is made kinematic. Any objects contained by the object are parented to it and also made kinematic. For more information regarding containment in TDW, [read this](../../lessons/semantic_states/containment.md).
+
+        :param target: The target object ID.
+        :param arm: The [`Arm`](../replicant/arm.md) value for the hand that will grasp the target object.
+        :param angle: Continuously (per `communicate()` call, including after this action ends), rotate the the grasped object by this many degrees relative to the hand. If None, the grasped object will maintain its initial rotation.
+        :param axis: Continuously (per `communicate()` call, including after this action ends) rotate the grasped object around this axis relative to the hand. Options: `"pitch"`, `"yaw"`, `"roll"`. If None, the grasped object will maintain its initial rotation.
+        """
+
+        self.action = Grasp(target=target,
+                            arm=arm,
+                            dynamic=self.dynamic,
+                            angle=angle,
+                            axis=axis)
+
+    def drop(self, arm: Arm, max_num_frames: int = 100) -> None:
+        """
+        Drop a held target object.
+
+        The action ends when the object stops moving or the number of consecutive `communicate()` calls since dropping the object exceeds `self.max_num_frames`.
+
+        When an object is dropped, it is made non-kinematic. Any objects contained by the object are parented to it and also made non-kinematic. For more information regarding containment in TDW, [read this](../../lessons/semantic_states/containment.md).
+
+        :param arm: The [`Arm`](../replicant/arm.md) holding the object.
+        :param max_num_frames: Wait this number of `communicate()` calls maximum for the object to stop moving before ending the action.
+        """
+
+        self.action = Drop(arm=arm, dynamic=self.dynamic, max_num_frames=max_num_frames)
+
+    def animate(self, animation: str, library: str = "humanoid_animations.json") -> None:
+        """
+        Play an animation.
+
+        The animation will end either when the animation clip is finished or if the Replicant collides with something (see [`self.collision_detection`](../replicant/collision_detection.md)).
+
+        - The collision detection will respond normally to walls, objects, obstacle avoidance, etc.
+        - If `self.collision_detection.previous_was_same == True`, and it was the same animation, and it ended in a collision, this action ends immediately.
+
+        :param animation: The name of the animation.
+        :param library: The animation library.
+        """
+
+        self.action = Animate(animation=animation,
+                              collision_detection=self.collision_detection,
+                              forward=True,
+                              library=library,
+                              previous=self._previous_action)
+
+    def reset_arm(self, arm: Union[Arm, List[Arm]], duration: float = 0.25) -> None:
+        """
+        Move arm(s) back to rest position(s). One or both arms can be reset at the same time.
+
+        The Replicant's arm(s) will continuously over multiple `communicate()` calls move until either the motion is complete or the arm collides with something (see `self.collision_detection`).
+
+        - The collision detection will respond normally to walls, objects, obstacle avoidance, etc.
+        - If `self.collision_detection.previous_was_same == True`, and if the previous action was an arm motion, and it ended in a collision, this action ends immediately.
+       
+        :param arm: The [`Arm`](../replicant/arm.md) value(s) that will reach for the `target` as a single value or a list. Example: `Arm.left` or `[Arm.left, Arm.right]`.
+        :param duration: The duration of the motion in seconds.
+        """
+
+        self.action = ResetArm(arms=Replicant._arms_to_list(arm),
+                               dynamic=self.dynamic,
+                               collision_detection=self.collision_detection,
+                               previous=self._previous_action,
+                               duration=duration)
+
+    def look_at(self, target: Union[int, np.ndarray, Dict[str,  float]], duration: float = 0.1):
+        """
+        Look at a target object or position.
+
+        The head will continuously move over multiple `communicate()` calls until it is looking at the target.
+
+        :param target: The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array.
+        :param duration: The duration of the motion in seconds.
+        """
+
+        self.action = LookAt(target=target, duration=duration)
+
+    def reset_head(self, duration: float = 0.1):
+        """
+        Reset the head to its neutral rotation.
+
+        The head will continuously move over multiple `communicate()` calls until it is at its neutral rotation.
+
+        :param duration: The duration of the motion in seconds.
+        """
+
+        self.action = ResetHead(duration=duration)
+
+    def reset(self, position: Union[Dict[str, float], np.ndarray] = None,
+              rotation: Union[Dict[str, float], np.ndarray] = None) -> None:
+        """
+        Reset the Replicant. Call this when you reset the scene.
+
+        :param position: The position of the Replicant as an x, y, z dictionary or numpy array. If None, defaults to `{"x": 0, "y": 0, "z": 0}`.
+        :param rotation: The rotation of the Replicant in Euler angles (degrees) as an x, y, z dictionary or numpy array. If None, defaults to `{"x": 0, "y": 0, "z": 0}`.
+        """
+
+        self.initialized = False
+        self.dynamic = None
+        self.static = None
+        self.action = None
+        self._previous_action = None
+        self._frame_count: int = 0
+        self.collision_detection = CollisionDetection()
+        self._set_initial_position_and_rotation(position=position, rotation=rotation)
+        self.commands.clear()
+
+    def _set_initial_position_and_rotation(self, position: Union[Dict[str, float], np.ndarray] = None,
+                                           rotation: Union[Dict[str, float], np.ndarray] = None) -> None:
+        """
+        Set the intial position and rotation.
+
+        :param position: The position of the Replicant as an x, y, z dictionary or numpy array. If None, defaults to `{"x": 0, "y": 0, "z": 0}`.
+        :param rotation: The rotation of the Replicant in Euler angles (degrees) as an x, y, z dictionary or numpy array. If None, defaults to `{"x": 0, "y": 0, "z": 0}`.
+        """
+
+        if position is None:
+            self.initial_position = {"x": 0, "y": 0, "z": 0}
+        elif isinstance(position, dict):
+            self.initial_position = position
+        elif isinstance(position, np.ndarray):
+            self.initial_position = TDWUtils.array_to_vector3(position)
+        else:
+            raise Exception(position)
+        if rotation is None:
+            self.initial_rotation = {"x": 0, "y": 0, "z": 0}
+        elif isinstance(rotation, dict):
+            self.initial_rotation = rotation
+        elif isinstance(rotation, np.ndarray):
+            self.initial_rotation = TDWUtils.array_to_vector3(rotation)
+
+    def _cache_static_data(self, resp: List[bytes]) -> None:
+        """
+        Cache static output data.
+
+        :param resp: The response from the build.
+        """
+
+        self.static = ReplicantStatic(replicant_id=self.replicant_id, resp=resp)
+        # Add an avatar and set up its camera.
+        self.commands.extend([{"$type": "create_avatar",
+                               "type": "A_Img_Caps_Kinematic",
+                               "id": self.static.avatar_id},
+                              {"$type": "set_pass_masks",
+                               "pass_masks": ["_img", "_id", "_depth"],
+                               "avatar_id": self.static.avatar_id},
+                              {"$type": "parent_avatar_to_replicant",
+                               "position": {"x": -0.1, "y": -0.1, "z": 0},
+                               "avatar_id": self.static.avatar_id,
+                               "id": self.replicant_id},
+                              {"$type": "enable_image_sensor",
+                               "enable": False,
+                               "avatar_id": self.static.avatar_id},
+                              {"$type": "set_img_pass_encoding",
+                               "value": False}])
+
+    def _set_dynamic_data(self, resp: List[bytes]) -> None:
+        """
+        Set dynamic data.
+
+        :param resp: The response from the build.
+        """
+
+        self.dynamic = ReplicantDynamic(resp=resp, replicant_id=self.replicant_id, frame_count=self._frame_count)
+        if self.dynamic.got_images:
+            self._frame_count += 1
+
+    @staticmethod
+    def _arms_to_list(arm: Union[Arm, List[Arm]]) -> List[Arm]:
+        """
+        Converts a single `Arm` value to a list if needed.
+
+        :param arm: Either a single `Arm` value or a list of `Arm` values.
+
+        :return: A list of `Arm` values.
+        """
+
+        if isinstance(arm, Arm):
+            return [arm]
+        elif isinstance(arm, list):
+            return arm
+        else:
+            raise Exception(f"Invalid arms: {arm}")
diff --git a/Python/tdw/container_data/containment_event.py b/Python/tdw/container_data/containment_event.py
index 397710d27..0d2e56557 100644
--- a/Python/tdw/container_data/containment_event.py
+++ b/Python/tdw/container_data/containment_event.py
@@ -7,7 +7,7 @@ class ContainmentEvent:
     Data describing a containment event i.e. when a container shape overlaps with one or more objects.
     """
 
-    def __init__(self, container_id: int, object_ids: np.array, tag: ContainerTag):
+    def __init__(self, container_id: int, object_ids: np.ndarray, tag: ContainerTag):
         """
         :param container_id: The ID of the container.
         :param object_ids: The IDs of the contained objects as a numpy array.
@@ -21,7 +21,7 @@ def __init__(self, container_id: int, object_ids: np.array, tag: ContainerTag):
         """:field
         The IDs of the contained objects as a numpy array
         """
-        self.object_ids: np.array = object_ids
+        self.object_ids: np.ndarray = object_ids
         """:field
         A semantic [`ContainerTag`](container_tag.md) describing the semantic nature of the event.
         """
diff --git a/Python/tdw/controller.py b/Python/tdw/controller.py
index 5f4cc5318..8b5dd5647 100644
--- a/Python/tdw/controller.py
+++ b/Python/tdw/controller.py
@@ -14,6 +14,10 @@
 from tdw.physics_audio.object_audio_static import DEFAULT_OBJECT_AUDIO_STATIC_DATA
 from tdw.physics_audio.audio_material import AudioMaterial
 from tdw.physics_audio.audio_material_constants import STATIC_FRICTION, DYNAMIC_FRICTION, DENSITIES
+from tdw.container_data.container_tag import ContainerTag
+from tdw.container_data.box_container import BoxContainer
+from tdw.container_data.sphere_container import SphereContainer
+from tdw.container_data.cylinder_container import CylinderContainer
 
 
 class Controller:
@@ -181,7 +185,8 @@ def get_add_object(model_name: str, object_id: int, position: Dict[str, float] =
                 "position": position if position is not None else {"x": 0, "y": 0, "z": 0},
                 "rotation": rotation if rotation is not None else {"x": 0, "y": 0, "z": 0},
                 "category": record.wcategory,
-                "id": object_id}
+                "id": object_id,
+                "affordance_points": record.affordance_points}
 
     @staticmethod
     def get_add_physics_object(model_name: str, object_id: int, position: Dict[str, float] = None, rotation: Dict[str, float] = None, library: str = "", scale_factor: Dict[str, float] = None, kinematic: bool = False, gravity: bool = True, default_physics_values: bool = True, mass: float = 1, dynamic_friction: float = 0.3, static_friction: float = 0.3, bounciness: float = 0.7, scale_mass: bool = True) -> List[dict]:
@@ -221,7 +226,8 @@ def get_add_physics_object(model_name: str, object_id: int, position: Dict[str,
                      "scale_factor": record.scale_factor,
                      "position": position,
                      "category": record.wcategory,
-                     "id": object_id}]
+                     "id": object_id,
+                     "affordance_points": record.affordance_points}]
         if rotation is not None:
             # The rotation is a quaternion.
             if "w" in rotation:
@@ -242,7 +248,6 @@ def get_add_physics_object(model_name: str, object_id: int, position: Dict[str,
             commands.append({"$type": "set_object_collision_detection_mode",
                              "id": object_id,
                              "mode": "continuous_speculative"})
-
         if default_physics_values:
             # Use default physics values.
             if model_name in DEFAULT_OBJECT_AUDIO_STATIC_DATA:
@@ -301,6 +306,26 @@ def get_add_physics_object(model_name: str, object_id: int, position: Dict[str,
                 commands.append({"$type": "scale_object",
                                  "scale_factor": scale_factor,
                                  "id": object_id})
+        # Add container shapes.
+        for container_shape in record.container_shapes:
+            if isinstance(container_shape, BoxContainer):
+                commands.append(Controller._add_box_container(object_id=object_id,
+                                                              position=container_shape.position,
+                                                              tag=container_shape.tag,
+                                                              half_extents=container_shape.half_extents,
+                                                              rotation=container_shape.rotation))
+            elif isinstance(container_shape, CylinderContainer):
+                commands.append(Controller._add_cylinder_container(object_id=object_id,
+                                                                   position=container_shape.position,
+                                                                   tag=container_shape.tag,
+                                                                   radius=container_shape.radius,
+                                                                   height=container_shape.height,
+                                                                   rotation=container_shape.rotation))
+            elif isinstance(container_shape, SphereContainer):
+                commands.append(Controller._add_sphere_container(object_id=object_id,
+                                                                 position=container_shape.position,
+                                                                 tag=container_shape.tag,
+                                                                 radius=container_shape.radius))
         return commands
 
     @staticmethod
@@ -613,3 +638,90 @@ def _check_pypi_version(v_installed_override: str = None, v_pypi_override: str =
                           f"\npip3 install tdw -U")
         else:
             print("Your installed tdw Python module is up to date with PyPi.")
+
+    @staticmethod
+    def _get_container_shape_command(command_name: str, object_id: int, position: Dict[str, float],
+                                     tag: ContainerTag) -> dict:
+        """
+        :param command_name: The name of the command.
+        :param object_id: The object ID.
+        :param tag: The semantic tag.
+        :param position: The local position of the container shape.
+
+        :return: A partial command to add a container shape to an object.
+        """
+
+        # Return a partial command.
+        return {"$type": command_name,
+                "id": object_id,
+                "container_id": int(Controller.get_unique_id()),
+                "position": position,
+                "tag": tag.name}
+
+    @staticmethod
+    def _add_box_container(object_id: int, position: Dict[str, float], tag: ContainerTag, half_extents: Dict[str, float],
+                           rotation: Dict[str, float]) -> dict:
+        """
+        Add a box container shape to an object.
+
+        :param object_id: The ID of the object.
+        :param position: The position of the box relative to the parent object.
+        :param tag: The box's semantic [`ContainerTag`](../container_data/container_tag.md).
+        :param half_extents: The half-extents (half the scale) of the box.
+        :param rotation: The rotation of the box in Euler angles relative to the parent object.
+
+        :return: A command to add a container shape.
+        """
+
+        command = Controller._get_container_shape_command(command_name="add_box_container",
+                                                          object_id=object_id,
+                                                          position=position,
+                                                          tag=tag)
+        command["half_extents"] = half_extents
+        command["rotation"] = rotation
+        return command
+
+    @staticmethod
+    def _add_cylinder_container(object_id: int, position: Dict[str, float], tag: ContainerTag, radius: float,
+                                height: float, rotation: Dict[str, float]) -> dict:
+        """
+        Add a cylinder container shape to an object.
+
+        :param object_id: The ID of the object.
+        :param position: The position of the cylinder relative to the parent object.
+        :param tag: The cylinder's semantic [`ContainerTag`](../container_data/container_tag.md).
+        :param radius: The radius of the cylinder.
+        :param height: The height of the cylinder.
+        :param rotation: The rotation of the cylinder in Euler angles relative to the parent object.
+
+        :return: A command to add a container shape.
+        """
+
+        command = Controller._get_container_shape_command(command_name="add_cylinder_container",
+                                                          object_id=object_id,
+                                                          position=position,
+                                                          tag=tag)
+        command["radius"] = radius
+        command["height"] = height
+        command["rotation"] = rotation
+        return command
+
+    @staticmethod
+    def _add_sphere_container(object_id: int, position: Dict[str, float], tag: ContainerTag, radius: float) -> dict:
+        """
+        Add a sphere container shape to an object.
+
+        :param object_id: The ID of the object.
+        :param position: The position of the sphere relative to the parent object.
+        :param tag: The sphere's semantic [`ContainerTag`](../container_data/container_tag.md).
+        :param radius: The radius of the sphere.
+
+        :return: A command to add a container shape.
+        """
+
+        command = Controller._get_container_shape_command(command_name="add_sphere_container",
+                                                          object_id=object_id,
+                                                          position=position,
+                                                          tag=tag)
+        command["radius"] = radius
+        return command
diff --git a/Python/tdw/metadata_libraries/models_core.json b/Python/tdw/metadata_libraries/models_core.json
index e7d43159e..9eac0db3c 100644
--- a/Python/tdw/metadata_libraries/models_core.json
+++ b/Python/tdw/metadata_libraries/models_core.json
@@ -2316,6 +2316,48 @@
             "wnid": "n02694662"
         },
         "alivar_tech_bench_sofa": {
+            "affordance_points": [
+                {
+                    "x": 0.8386390805244446,
+                    "y": 0.5745464563369751,
+                    "z": 0.3105928599834442
+                },
+                {
+                    "x": 0.8400009274482727,
+                    "y": 0.5509949922561646,
+                    "z": -0.3193787932395935
+                },
+                {
+                    "x": 1.108490228652954,
+                    "y": 0.42146721482276917,
+                    "z": -0.28289514780044556
+                },
+                {
+                    "x": 1.1031583547592163,
+                    "y": 0.39840275049209595,
+                    "z": 0.2996269762516022
+                },
+                {
+                    "x": -0.8332502245903015,
+                    "y": 0.5742858648300171,
+                    "z": 0.3274528682231903
+                },
+                {
+                    "x": -0.8400046229362488,
+                    "y": 0.5588834285736084,
+                    "z": -0.29759931564331055
+                },
+                {
+                    "x": -1.0957090854644775,
+                    "y": 0.4214935302734375,
+                    "z": -0.29069387912750244
+                },
+                {
+                    "x": -1.1124807596206665,
+                    "y": 0.40374985337257385,
+                    "z": 0.2853088676929474
+                }
+            ],
             "asset_bundle_sizes": {
                 "Darwin": 14866946,
                 "Linux": 15092300,
@@ -3618,6 +3660,28 @@
             "wnid": "n04256520"
         },
         "arflex_strips_sofa": {
+            "affordance_points": [
+                {
+                    "x": 0.9513953328132629,
+                    "y": 0.10104460269212723,
+                    "z": -0.2641777992248535
+                },
+                {
+                    "x": 0.95229172706604,
+                    "y": 0.10805728286504745,
+                    "z": 0.24683722853660583
+                },
+                {
+                    "x": -0.9492408633232117,
+                    "y": 0.09749942272901535,
+                    "z": 0.22346439957618713
+                },
+                {
+                    "x": -0.948550820350647,
+                    "y": 0.09958212077617645,
+                    "z": -0.2326636016368866
+                }
+            ],
             "asset_bundle_sizes": {
                 "Darwin": 7688311,
                 "Linux": 7917835,
@@ -18789,6 +18853,28 @@
             "wnid": "n02801938"
         },
         "basket_18inx18inx12iin_bamboo": {
+            "affordance_points": [
+                {
+                    "x": -0.2285,
+                    "y": 0.305,
+                    "z": 0.0
+                },
+                {
+                    "x": 0.2285,
+                    "y": 0.305,
+                    "z": 0.0
+                },
+                {
+                    "x": 0,
+                    "y": 0.305,
+                    "z": 0.2285
+                },
+                {
+                    "x": 0,
+                    "y": 0.305,
+                    "z": -0.2285
+                }
+            ],
             "asset_bundle_sizes": {
                 "Darwin": 1970632,
                 "Linux": 2192962,
@@ -18850,9 +18936,9 @@
                         "z": 0
                     },
                     "rotation": {
-                        "x": 0,
-                        "y": 0,
-                        "z": 0
+                        "x": 0.0,
+                        "y": 0.0,
+                        "z": 0.0
                     },
                     "shape": "box",
                     "tag": "inside"
@@ -18882,6 +18968,28 @@
             "wnid": "n02801938"
         },
         "basket_18inx18inx12iin_plastic_lattice": {
+            "affordance_points": [
+                {
+                    "x": -0.2285,
+                    "y": 0.305,
+                    "z": 0.0
+                },
+                {
+                    "x": 0.2285,
+                    "y": 0.305,
+                    "z": 0.0
+                },
+                {
+                    "x": 0,
+                    "y": 0.305,
+                    "z": 0.2285
+                },
+                {
+                    "x": 0,
+                    "y": 0.305,
+                    "z": -0.2285
+                }
+            ],
             "asset_bundle_sizes": {
                 "Darwin": 2028504,
                 "Linux": 2293259,
@@ -18943,9 +19051,9 @@
                         "z": 0
                     },
                     "rotation": {
-                        "x": 0,
-                        "y": 0,
-                        "z": 0
+                        "x": 0.0,
+                        "y": 0.0,
+                        "z": 0.0
                     },
                     "shape": "box",
                     "tag": "inside"
@@ -18975,6 +19083,28 @@
             "wnid": "n02801938"
         },
         "basket_18inx18inx12iin_wicker": {
+            "affordance_points": [
+                {
+                    "x": -0.2285,
+                    "y": 0.305,
+                    "z": 0.0
+                },
+                {
+                    "x": 0.2285,
+                    "y": 0.305,
+                    "z": 0.0
+                },
+                {
+                    "x": 0,
+                    "y": 0.305,
+                    "z": 0.2285
+                },
+                {
+                    "x": 0,
+                    "y": 0.305,
+                    "z": -0.2285
+                }
+            ],
             "asset_bundle_sizes": {
                 "Darwin": 15045207,
                 "Linux": 15232237,
@@ -19036,9 +19166,9 @@
                         "z": 0
                     },
                     "rotation": {
-                        "x": 0,
-                        "y": 0,
-                        "z": 0
+                        "x": 0.0,
+                        "y": 0.0,
+                        "z": 0.0
                     },
                     "shape": "box",
                     "tag": "inside"
@@ -19068,6 +19198,28 @@
             "wnid": "n02801938"
         },
         "basket_18inx18inx12iin_wood_mesh": {
+            "affordance_points": [
+                {
+                    "x": -0.2285,
+                    "y": 0.305,
+                    "z": 0.0
+                },
+                {
+                    "x": 0.2285,
+                    "y": 0.305,
+                    "z": 0.0
+                },
+                {
+                    "x": 0,
+                    "y": 0.305,
+                    "z": 0.2285
+                },
+                {
+                    "x": 0,
+                    "y": 0.305,
+                    "z": -0.2285
+                }
+            ],
             "asset_bundle_sizes": {
                 "Darwin": 1987559,
                 "Linux": 2251488,
@@ -19129,9 +19281,9 @@
                         "z": 0
                     },
                     "rotation": {
-                        "x": 0,
-                        "y": 0,
-                        "z": 0
+                        "x": 0.0,
+                        "y": 0.0,
+                        "z": 0.0
                     },
                     "shape": "box",
                     "tag": "inside"
@@ -48722,6 +48874,48 @@
             "wnid": "n04461879"
         },
         "rh10": {
+            "affordance_points": [
+                {
+                    "x": -0.20485186576843262,
+                    "y": 0.5167179703712463,
+                    "z": 0.12209056317806244
+                },
+                {
+                    "x": -0.19725313782691956,
+                    "y": 0.5039609670639038,
+                    "z": -0.1273145079612732
+                },
+                {
+                    "x": 0.1090635284781456,
+                    "y": 0.3982941508293152,
+                    "z": 0.05886673554778099
+                },
+                {
+                    "x": -0.056706201285123825,
+                    "y": 0.3981224000453949,
+                    "z": 0.062156084924936295
+                },
+                {
+                    "x": -0.053864773362874985,
+                    "y": 0.39878857135772705,
+                    "z": -0.06045682728290558
+                },
+                {
+                    "x": 0.10858231782913208,
+                    "y": 0.399463951587677,
+                    "z": -0.05012769252061844
+                },
+                {
+                    "x": 0.212923064827919,
+                    "y": 0.3742195665836334,
+                    "z": 0.014543075114488602
+                },
+                {
+                    "x": -0.3340177536010742,
+                    "y": 0.5571339726448059,
+                    "z": 0.011872617527842522
+                }
+            ],
             "asset_bundle_sizes": {
                 "Darwin": 141123,
                 "Linux": 180251,
diff --git a/Python/tdw/metadata_libraries/models_full.json b/Python/tdw/metadata_libraries/models_full.json
index 60dafe1ad..e92eb6516 100644
--- a/Python/tdw/metadata_libraries/models_full.json
+++ b/Python/tdw/metadata_libraries/models_full.json
@@ -19250,6 +19250,48 @@
             "wnid": "n04405907"
         },
         "alivar_tech_bench_sofa": {
+            "affordance_points": [
+                {
+                    "x": 0.8386390805244446,
+                    "y": 0.5745464563369751,
+                    "z": 0.3105928599834442
+                },
+                {
+                    "x": 0.8400009274482727,
+                    "y": 0.5509949922561646,
+                    "z": -0.3193787932395935
+                },
+                {
+                    "x": 1.108490228652954,
+                    "y": 0.42146721482276917,
+                    "z": -0.28289514780044556
+                },
+                {
+                    "x": 1.1031583547592163,
+                    "y": 0.39840275049209595,
+                    "z": 0.2996269762516022
+                },
+                {
+                    "x": -0.8332502245903015,
+                    "y": 0.5742858648300171,
+                    "z": 0.3274528682231903
+                },
+                {
+                    "x": -0.8400046229362488,
+                    "y": 0.5588834285736084,
+                    "z": -0.29759931564331055
+                },
+                {
+                    "x": -1.0957090854644775,
+                    "y": 0.4214935302734375,
+                    "z": -0.29069387912750244
+                },
+                {
+                    "x": -1.1124807596206665,
+                    "y": 0.40374985337257385,
+                    "z": 0.2853088676929474
+                }
+            ],
             "asset_bundle_sizes": {
                 "Darwin": 14866946,
                 "Linux": 15092300,
@@ -24310,6 +24352,28 @@
             "wnid": "n04256520"
         },
         "arflex_strips_sofa": {
+            "affordance_points": [
+                {
+                    "x": 0.9513953328132629,
+                    "y": 0.10104460269212723,
+                    "z": -0.2641777992248535
+                },
+                {
+                    "x": 0.95229172706604,
+                    "y": 0.10805728286504745,
+                    "z": 0.24683722853660583
+                },
+                {
+                    "x": -0.9492408633232117,
+                    "y": 0.09749942272901535,
+                    "z": 0.22346439957618713
+                },
+                {
+                    "x": -0.948550820350647,
+                    "y": 0.09958212077617645,
+                    "z": -0.2326636016368866
+                }
+            ],
             "asset_bundle_sizes": {
                 "Darwin": 7688311,
                 "Linux": 7917835,
@@ -265779,6 +265843,28 @@
             "wnid": "n02801938"
         },
         "basket_18inx18inx12iin_bamboo": {
+            "affordance_points": [
+                {
+                    "x": -0.2285,
+                    "y": 0.305,
+                    "z": 0.0
+                },
+                {
+                    "x": 0.2285,
+                    "y": 0.305,
+                    "z": 0.0
+                },
+                {
+                    "x": 0,
+                    "y": 0.305,
+                    "z": 0.2285
+                },
+                {
+                    "x": 0,
+                    "y": 0.305,
+                    "z": -0.2285
+                }
+            ],
             "asset_bundle_sizes": {
                 "Darwin": 1970632,
                 "Linux": 2192962,
@@ -265840,9 +265926,9 @@
                         "z": 0
                     },
                     "rotation": {
-                        "x": 0,
-                        "y": 0,
-                        "z": 0
+                        "x": 0.0,
+                        "y": 0.0,
+                        "z": 0.0
                     },
                     "shape": "box",
                     "tag": "inside"
@@ -265867,11 +265953,33 @@
                 "Linux": "https://tdw-public.s3.amazonaws.com/models/linux/2019.2/basket_18inx18inx12iin_bamboo",
                 "Windows": "https://tdw-public.s3.amazonaws.com/models/windows/2019.2/basket_18inx18inx12iin_bamboo"
             },
-            "volume": 0.007523464970290661,
+            "volume": 0.0075235795229673386,
             "wcategory": "basket",
             "wnid": "n02801938"
         },
         "basket_18inx18inx12iin_plastic_lattice": {
+            "affordance_points": [
+                {
+                    "x": -0.2285,
+                    "y": 0.305,
+                    "z": 0.0
+                },
+                {
+                    "x": 0.2285,
+                    "y": 0.305,
+                    "z": 0.0
+                },
+                {
+                    "x": 0,
+                    "y": 0.305,
+                    "z": 0.2285
+                },
+                {
+                    "x": 0,
+                    "y": 0.305,
+                    "z": -0.2285
+                }
+            ],
             "asset_bundle_sizes": {
                 "Darwin": 2028504,
                 "Linux": 2293259,
@@ -265933,9 +266041,9 @@
                         "z": 0
                     },
                     "rotation": {
-                        "x": 0,
-                        "y": 0,
-                        "z": 0
+                        "x": 0.0,
+                        "y": 0.0,
+                        "z": 0.0
                     },
                     "shape": "box",
                     "tag": "inside"
@@ -265965,6 +266073,28 @@
             "wnid": "n02801938"
         },
         "basket_18inx18inx12iin_wicker": {
+            "affordance_points": [
+                {
+                    "x": -0.2285,
+                    "y": 0.305,
+                    "z": 0.0
+                },
+                {
+                    "x": 0.2285,
+                    "y": 0.305,
+                    "z": 0.0
+                },
+                {
+                    "x": 0,
+                    "y": 0.305,
+                    "z": 0.2285
+                },
+                {
+                    "x": 0,
+                    "y": 0.305,
+                    "z": -0.2285
+                }
+            ],
             "asset_bundle_sizes": {
                 "Darwin": 15045207,
                 "Linux": 15232237,
@@ -266026,9 +266156,9 @@
                         "z": 0
                     },
                     "rotation": {
-                        "x": 0,
-                        "y": 0,
-                        "z": 0
+                        "x": 0.0,
+                        "y": 0.0,
+                        "z": 0.0
                     },
                     "shape": "box",
                     "tag": "inside"
@@ -266053,11 +266183,33 @@
                 "Linux": "https://tdw-public.s3.amazonaws.com/models/linux/2019.2/basket_18inx18inx12iin_wicker",
                 "Windows": "https://tdw-public.s3.amazonaws.com/models/windows/2019.2/basket_18inx18inx12iin_wicker"
             },
-            "volume": 0.007523438427597284,
+            "volume": 0.007523559033870697,
             "wcategory": "basket",
             "wnid": "n02801938"
         },
         "basket_18inx18inx12iin_wood_mesh": {
+            "affordance_points": [
+                {
+                    "x": -0.2285,
+                    "y": 0.305,
+                    "z": 0.0
+                },
+                {
+                    "x": 0.2285,
+                    "y": 0.305,
+                    "z": 0.0
+                },
+                {
+                    "x": 0,
+                    "y": 0.305,
+                    "z": 0.2285
+                },
+                {
+                    "x": 0,
+                    "y": 0.305,
+                    "z": -0.2285
+                }
+            ],
             "asset_bundle_sizes": {
                 "Darwin": 1987559,
                 "Linux": 2251488,
@@ -266119,9 +266271,9 @@
                         "z": 0
                     },
                     "rotation": {
-                        "x": 0,
-                        "y": 0,
-                        "z": 0
+                        "x": 0.0,
+                        "y": 0.0,
+                        "z": 0.0
                     },
                     "shape": "box",
                     "tag": "inside"
@@ -396826,6 +396978,48 @@
             "wnid": "n04461879"
         },
         "rh10": {
+            "affordance_points": [
+                {
+                    "x": -0.20485186576843262,
+                    "y": 0.5167179703712463,
+                    "z": 0.12209056317806244
+                },
+                {
+                    "x": -0.19725313782691956,
+                    "y": 0.5039609670639038,
+                    "z": -0.1273145079612732
+                },
+                {
+                    "x": 0.1090635284781456,
+                    "y": 0.3982941508293152,
+                    "z": 0.05886673554778099
+                },
+                {
+                    "x": -0.056706201285123825,
+                    "y": 0.3981224000453949,
+                    "z": 0.062156084924936295
+                },
+                {
+                    "x": -0.053864773362874985,
+                    "y": 0.39878857135772705,
+                    "z": -0.06045682728290558
+                },
+                {
+                    "x": 0.10858231782913208,
+                    "y": 0.399463951587677,
+                    "z": -0.05012769252061844
+                },
+                {
+                    "x": 0.212923064827919,
+                    "y": 0.3742195665836334,
+                    "z": 0.014543075114488602
+                },
+                {
+                    "x": -0.3340177536010742,
+                    "y": 0.5571339726448059,
+                    "z": 0.011872617527842522
+                }
+            ],
             "asset_bundle_sizes": {
                 "Darwin": 141123,
                 "Linux": 180251,
@@ -396894,7 +397088,7 @@
                 "Linux": "https://tdw-public.s3.amazonaws.com/models/linux/2018-2019.1/rh10",
                 "Windows": "https://tdw-public.s3.amazonaws.com/models/windows/2018-2019.1/rh10"
             },
-            "volume": 0.0247779730707407,
+            "volume": 0.024778643622994423,
             "wcategory": "toy",
             "wnid": "n04461879"
         },
diff --git a/Python/tdw/metadata_libraries/replicants.json b/Python/tdw/metadata_libraries/replicants.json
new file mode 100644
index 000000000..78532e24c
--- /dev/null
+++ b/Python/tdw/metadata_libraries/replicants.json
@@ -0,0 +1,13 @@
+{
+    "description": "Replicants",
+    "records": {
+        "replicant_0": {
+            "name": "replicant_0",
+            "urls": {
+                "Darwin": "https://tdw-public.s3.amazonaws.com/replicants/darwin/2020.3/replicant_0",
+                "Linux": "https://tdw-public.s3.amazonaws.com/replicants/linux/2020.3/replicant_0",
+                "Windows": "https://tdw-public.s3.amazonaws.com/replicants/windows/2020.3/replicant_0"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Python/tdw/object_data/transform.py b/Python/tdw/object_data/transform.py
index 6be61bcb8..ffb8ed8cc 100644
--- a/Python/tdw/object_data/transform.py
+++ b/Python/tdw/object_data/transform.py
@@ -16,12 +16,12 @@ def __init__(self, position: np.array, rotation: np.array, forward: np.array):
         """:field
         The position vector of the object as a numpy array: `[x, y, z]` The position of each object is the bottom-center point of the object. The position of each Magnebot body part is in the exact center of the body part. `y` is the up direction.
         """
-        self.position: np.array = position
+        self.position: np.ndarray = position
         """:field
         The rotation quaternion of the object as a numpy array: `[x, y, z, w]` See: [`tdw.quaternion_utils.QuaternionUtils`](https://github.com/threedworld-mit/tdw/blob/master/Documentation/python/tdw_utils.md#quaternionutils).
         """
-        self.rotation: np.array = rotation
+        self.rotation: np.ndarray = rotation
         """:field
         The forward directional vector of the object as a numpy array: `[x, y, z]`
         """
-        self.forward: np.array = forward
+        self.forward: np.ndarray = forward
diff --git a/Python/tdw/output_data.py b/Python/tdw/output_data.py
index aad8bed07..4ed59efa7 100644
--- a/Python/tdw/output_data.py
+++ b/Python/tdw/output_data.py
@@ -26,9 +26,9 @@
 from tdw.FBOutput import AudioSources as Audi
 from tdw.FBOutput import Raycast as Ray
 from tdw.FBOutput import Overlap as Over
+from tdw.FBOutput import Containment as Cont
 from tdw.FBOutput import NavMeshPath as Path
 from tdw.FBOutput import StaticRobot as StRobo
-from tdw.FBOutput import Robot as Robo
 from tdw.FBOutput import Keyboard as Key
 from tdw.FBOutput import Magnebot as Mag
 from tdw.FBOutput import ScreenPosition as Screen
@@ -42,19 +42,23 @@
 from tdw.FBOutput import Categories as Cats
 from tdw.FBOutput import StaticRigidbodies as StatRig
 from tdw.FBOutput import RobotJointVelocities as RoJoVe
-from tdw.FBOutput import EmptyObjects as Empty
+from tdw.FBOutput import StaticEmptyObjects as StaticEmpty
+from tdw.FBOutput import DynamicEmptyObjects as DynamicEmpty
 from tdw.FBOutput import OculusTouchButtons as OculusTouch
 from tdw.FBOutput import StaticOculusTouch as StatOc
 from tdw.FBOutput import StaticCompositeObjects as StatComp
 from tdw.FBOutput import DynamicCompositeObjects as DynComp
 from tdw.FBOutput import AudioSourceDone as AudDone
 from tdw.FBOutput import ObiParticles as ObiP
-from tdw.vr_data.oculus_touch_button import OculusTouchButton
 from tdw.FBOutput import ObjectColliderIntersection as ObjColInt
 from tdw.FBOutput import EnvironmentColliderIntersection as EnvColInt
 from tdw.FBOutput import Mouse as Mous
 from tdw.FBOutput import DynamicRobots as DynRob
 from tdw.FBOutput import FieldOfView as Fov
+from tdw.FBOutput import Replicants as Repl
+from tdw.vr_data.oculus_touch_button import OculusTouchButton
+from tdw.container_data.container_tag import ContainerTag
+from tdw.replicant.action_status import ActionStatus
 import numpy as np
 from typing import Tuple, Optional, List
 
@@ -822,6 +826,33 @@ def get_walls(self) -> bool:
         return self.data.Walls()
 
 
+class Containment(OutputData):
+    def __init__(self, b):
+        super().__init__(b)
+        self._ids: np.ndarray = self.data.IdsAsNumpy()
+
+    def get_data(self):
+        return Cont.Containment.GetRootAsContainment(self.bytes, 0)
+
+    def get_object_id(self) -> int:
+        return int(self._ids[0])
+
+    def get_container_id(self) -> int:
+        return int(self._ids[1])
+
+    def get_tag(self) -> ContainerTag:
+        return ContainerTag(self.data.Tag())
+
+    def get_overlap_ids(self) -> np.array:
+        return self.data.OverlapIdsAsNumpy()
+
+    def get_env(self) -> bool:
+        return self.data.Env()
+
+    def get_walls(self) -> bool:
+        return self.data.Walls()
+
+
 class NavMeshPath(OutputData):
     _STATES = {PathState.PathState.complete: "complete",
                PathState.PathState.invalid: "invalid",
@@ -837,7 +868,7 @@ def get_path(self) -> np.array:
         return self.data.PathAsNumpy().view(dtype=np.float32).reshape(-1, 3)
 
     def get_id(self) -> int:
-        return self.data.Id()
+        return int(self.data.Id())
 
 
 class StaticRobot(OutputData):
@@ -925,38 +956,6 @@ def get_robot_index(self) -> int:
         return self.data.Index()
 
 
-class Robot(OutputData):
-    def get_data(self) -> Robo.Robot:
-        return Robo.Robot.GetRootAsRobot(self.bytes, 0)
-
-    def get_id(self) -> int:
-        return self.data.Id()
-
-    def get_position(self) -> Tuple[float, float, float]:
-        return OutputData._get_vector3(self.data.Transform().Position)
-
-    def get_rotation(self) -> Tuple[float, float, float, float]:
-        return OutputData._get_quaternion(self.data.Transform().Rotation)
-
-    def get_forward(self) -> Tuple[float, float, float]:
-        return OutputData._get_vector3(self.data.Transform().Forward)
-
-    def get_num_joints(self) -> int:
-        return self.data.JointsLength()
-
-    def get_joint_id(self, index: int) -> int:
-        return self.data.Joints(index).Id()
-
-    def get_joint_position(self, index: int) -> np.array:
-        return self.data.Joints(index).PositionAsNumpy()
-
-    def get_joint_positions(self, index: int) -> np.array:
-        return np.degrees(self.data.Joints(index).PositionsAsNumpy())
-
-    def get_immovable(self) -> bool:
-        return self.data.Immovable()
-
-
 class RobotJointVelocities(OutputData):
     def get_data(self) -> RoJoVe.RobotJointVelocities:
         return RoJoVe.RobotJointVelocities.GetRootAsRobotJointVelocities(self.bytes, 0)
@@ -1206,22 +1205,36 @@ def get_category_color(self, index: int) -> Tuple[float, float, float]:
         return OutputData._get_rgb(self.data.CategoryData(index).Color())
 
 
-class EmptyObjects(OutputData):
+class StaticEmptyObjects(OutputData):
     def __init__(self, b):
         super().__init__(b)
-        self._ids = self.data.IdsAsNumpy().view(dtype=int)
-        self._positions = self.data.PositionsAsNumpy().view(dtype=np.float32).reshape(-1, 3)
+        self._ids: np.ndarray = self.data.IdsAsNumpy().reshape(-1, 2)
 
-    def get_data(self) -> Empty.EmptyObjects:
-        return Empty.EmptyObjects.GetRootAsEmptyObjects(self.bytes, 0)
+    def get_data(self) -> StaticEmpty.StaticEmptyObjects:
+        return StaticEmpty.StaticEmptyObjects.GetRootAsStaticEmptyObjects(self.bytes, 0)
 
     def get_num(self) -> int:
-        return len(self._ids)
+        return int(self._ids.shape[0])
 
-    def get_id(self, index: int) -> int:
-        return int(self._ids[index])
+    def get_object_id(self, index: int) -> int:
+        return int(self._ids[index][0])
 
-    def get_position(self, index: int) -> np.array:
+    def get_empty_object_id(self, index: int) -> int:
+        return int(self._ids[index][1])
+
+
+class DynamicEmptyObjects(OutputData):
+    def __init__(self, b):
+        super().__init__(b)
+        self._positions = self.data.PositionsAsNumpy().view(dtype=np.float32).reshape(-1, 3)
+
+    def get_data(self) -> DynamicEmpty.DynamicEmptyObjects:
+        return DynamicEmpty.DynamicEmptyObjects.GetRootAsDynamicEmptyObjects(self.bytes, 0)
+
+    def get_num(self) -> int:
+        return int(self._positions.shape[0])
+
+    def get_position(self, index: int) -> np.ndarray:
         return self._positions[index]
 
 
@@ -1479,3 +1492,67 @@ def get_fov(self) -> float:
 
     def get_focal_length(self) -> float:
         return self.data.FocalLength()
+
+
+class Replicants(OutputData):
+    def __init__(self, b):
+        super().__init__(b)
+        self._ids = self.data.IdsAsNumpy().reshape(-1, 15)
+        self._positions: np.ndarray = self.data.PositionsAsNumpy().reshape(-1, 15, 3)
+        self._rotations: np.ndarray = self.data.RotationsAsNumpy().reshape(-1, 15, 4)
+        self._forwards: np.ndarray = self.data.ForwardsAsNumpy().reshape(-1, 15, 3)
+        self._held: np.ndarray = self.data.HeldAsNumpy().reshape(-1, 2, 2)
+        self._collision_ids: np.ndarray = self.data.CollisionIdsAsNumpy().reshape(-1, 14, 10)
+        self._is_collisions: np.ndarray = self.data.IsCollisionsAsNumpy().reshape(-1, 14, 10)
+        self._statuses: np.ndarray = self.data.StatusesAsNumpy()
+
+    def get_data(self) -> Repl.Replicants:
+        return Repl.Replicants.GetRootAsReplicants(self.bytes, 0)
+
+    def get_num(self) -> int:
+        return len(self._ids)
+
+    def get_id(self, index: int) -> int:
+        return int(self._ids[index][0])
+
+    def get_position(self, index: int) -> np.ndarray:
+        return self._positions[index][0]
+
+    def get_forward(self, index: int) -> np.ndarray:
+        return self._forwards[index][0]
+
+    def get_rotation(self, index: int) -> np.ndarray:
+        return self._rotations[index][0]
+
+    def get_body_part_id(self, index: int, body_part_index: int) -> int:
+        return int(self._ids[index][body_part_index + 1])
+
+    def get_body_part_position(self, index: int, body_part_index: int) -> np.ndarray:
+        return self._positions[index][body_part_index + 1]
+
+    def get_body_part_rotation(self, index: int, body_part_index: int) -> np.ndarray:
+        return self._rotations[index][body_part_index + 1]
+
+    def get_body_part_forward(self, index: int, body_part_index: int) -> np.ndarray:
+        return self._forwards[index][body_part_index + 1]
+
+    def get_is_holding_left(self, index: int) -> bool:
+        return self._held[index][0][0] == 1
+
+    def get_held_left(self, index: int) -> int:
+        return int(self._held[index][0][1])
+
+    def get_is_holding_right(self, index: int) -> bool:
+        return self._held[index][1][0] == 1
+
+    def get_held_right(self, index: int) -> int:
+        return int(self._held[index][1][1])
+
+    def get_is_collision(self, index: int, body_part_index: int, collision_index: int) -> bool:
+        return self._is_collisions[index][body_part_index][collision_index]
+
+    def get_collision_id(self, index: int, body_part_index: int, collision_index: int) -> int:
+        return int(self._collision_ids[index][body_part_index][collision_index])
+
+    def get_status(self, index: int) -> ActionStatus:
+        return ActionStatus(self._statuses[index])
diff --git a/Python/tdw/replicant/action_status.py b/Python/tdw/replicant/action_status.py
new file mode 100644
index 000000000..209b3d56a
--- /dev/null
+++ b/Python/tdw/replicant/action_status.py
@@ -0,0 +1,21 @@
+from enum import Enum
+
+
+class ActionStatus(Enum):
+    """
+    The status of the Replicant after doing an action.
+    """
+
+    ongoing = 0  # The action is ongoing.
+    failure = 1  # Generic failure code (useful for custom APIs).
+    success = 2  # The action was successful.
+    failed_to_move = 3  # Tried to move to a target position or object but failed.
+    failed_to_turn = 4  # Tried to turn but failed to align with the target angle, position, or object.
+    cannot_reach = 5  # Didn't try to reach for the target position because it can't.
+    failed_to_reach = 6  # Tried to reach for the target but failed; the magnet isn't close to the target.
+    not_holding = 7  # Didn't try to drop the object(s) because it isn't holding them.
+    collision = 8  # Tried to move or turn but failed because it collided with something.
+    detected_obstacle = 9  # Detected an obstacle in its path.
+    already_holding = 10  # Already holding the object.
+    still_dropping = 11  # Dropped an object but, after many `communicate()` calls, the object is still moving.
+    cannot_grasp = 12  # Didn't try to grasp the object because it's of an invalid type (e.g. a kinematic object).
diff --git a/Python/tdw/replicant/actions/action.py b/Python/tdw/replicant/actions/action.py
new file mode 100644
index 000000000..dd0f80e0b
--- /dev/null
+++ b/Python/tdw/replicant/actions/action.py
@@ -0,0 +1,118 @@
+from abc import ABC, abstractmethod
+from typing import List
+from overrides import final
+import numpy as np
+from tdw.output_data import OutputData, Transforms
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class Action(ABC):
+    """
+    An action that the Replicant can do. An action is first initialized, has an ongoing state, and an end state.
+    An action also has a status indicating whether it's ongoing, succeeded, or failed; and if it failed, why.
+    """
+
+    def __init__(self):
+        """
+        (no parameters)
+        """
+
+        """:field
+        [The current status of the action.](../action_status.md) By default, this is `ongoing` (the action isn't done).
+        """
+        self.status: ActionStatus = ActionStatus.ongoing
+        """:field
+        If True, the action has initialized. If False, the action will try to send `get_initialization_commands(resp)` on this frame.
+        """
+        self.initialized: bool = False
+        """:field
+        If True, this action is done and won't send any more commands.
+        """
+        self.done: bool = False
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        """
+        :param resp: The response from the build.
+        :param static: The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized.
+        :param dynamic: The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call.
+        :param image_frequency: An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured.
+
+        :return: A list of commands to initialize this action.
+        """
+
+        # If we only want images at the start of the action or never, disable the camera now.
+        if image_frequency == ImageFrequency.once or image_frequency == ImageFrequency.never:
+            commands = [{"$type": "enable_image_sensor",
+                         "enable": False,
+                         "avatar_id": static.avatar_id}]
+        # If we want images per frame, enable image capture now.
+        elif image_frequency == ImageFrequency.always:
+            commands = [{"$type": "enable_image_sensor",
+                         "enable": True,
+                         "avatar_id": static.avatar_id},
+                        {"$type": "send_images",
+                         "frequency": "always"},
+                        {"$type": "send_camera_matrices",
+                         "frequency": "always"}]
+        else:
+            raise Exception(f"Invalid image capture option: {image_frequency}")
+        return commands
+
+    @abstractmethod
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        """
+        Evaluate an action per-frame to determine whether it's done.
+
+        :param resp: The response from the build.
+        :param static: The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized.
+        :param dynamic: The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call.
+
+        :return: A list of commands to send to the build to continue the action.
+        """
+
+        raise Exception()
+
+    def get_end_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                         image_frequency: ImageFrequency) -> List[dict]:
+        """
+        :param resp: The response from the build.
+        :param static: The [`ReplicantStatic`](../replicant_static.md) data that doesn't change after the Replicant is initialized.
+        :param dynamic: The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call.
+        :param image_frequency: An [`ImageFrequency`](../image_frequency.md) value describing how often image data will be captured.
+
+        :return: A list of commands that must be sent to end any action.
+        """
+
+        commands: List[dict] = list()
+        # Enable image capture on this frame only.
+        if image_frequency == ImageFrequency.once:
+            commands.extend([{"$type": "enable_image_sensor",
+                              "enable": True,
+                              "avatar_id": static.avatar_id},
+                             {"$type": "send_images",
+                              "frequency": "once"},
+                             {"$type": "send_camera_matrices",
+                              "frequency": "once"}])
+        return commands
+
+    @final
+    def _get_object_position(self, object_id: int, resp: List[bytes]) -> np.ndarray:
+        """
+        :param object_id: The object ID.
+        :param resp: The response from the build.
+
+        :return: The position of the object.
+        """
+
+        for i in range(len(resp) - 1):
+            r_id = OutputData.get_data_type_id(resp[i])
+            if r_id == "tran":
+                transforms = Transforms(resp[i])
+                for j in range(transforms.get_num()):
+                    if transforms.get_id(j) == object_id:
+                        return transforms.get_position(j)
+        raise Exception(f"Transform data not found for: {object_id}")
diff --git a/Python/tdw/replicant/actions/animate.py b/Python/tdw/replicant/actions/animate.py
new file mode 100644
index 000000000..d55807484
--- /dev/null
+++ b/Python/tdw/replicant/actions/animate.py
@@ -0,0 +1,93 @@
+from __future__ import annotations
+from typing import List, Optional
+from tdw.replicant.actions.action import Action
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.image_frequency import ImageFrequency
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.librarian import HumanoidAnimationLibrarian, HumanoidAnimationRecord
+
+
+class Animate(Action):
+    """
+    Play an animation.
+
+    The animation will end either when the animation clip is finished or if the Replicant collides with something (see `self.collision_detection`).
+
+    - The collision detection will respond normally to walls, objects, obstacle avoidance, etc.
+    - If `self.collision_detection.previous_was_same == True`, and it was the same animation, and it ended in a collision, this action ends immediately.
+    """
+
+    def __init__(self, animation: str, collision_detection: CollisionDetection, forward: bool, library: str,
+                 previous: Optional[Action]):
+        """
+        :param animation: The name of the animation.
+        :param collision_detection: The [`CollisionDetection`](../collision_detection.md) rules.
+        :param forward: If True, play the animation forwards. If False, play the animation backwards.
+        :param library: The name animation library.
+        :param previous: The previous action. Can be None.
+        """
+
+        super().__init__()
+        # Add the animation library.
+        if library not in Controller.HUMANOID_ANIMATION_LIBRARIANS:
+            Controller.HUMANOID_ANIMATION_LIBRARIANS[library] = HumanoidAnimationLibrarian(library)
+        """:field
+        The `HumanoidAnimationRecord` of the animation.
+        """
+        self.record: HumanoidAnimationRecord = Controller.HUMANOID_ANIMATION_LIBRARIANS[library].get_record(animation)
+        """:field
+        The [`CollisionDetection`](../collision_detection.md) rules.
+        """
+        self.collision_detection: CollisionDetection = collision_detection
+        # Don't try to play the same animation twice if the first one ended in a collision.
+        if self.collision_detection.previous_was_same and previous is not None and isinstance(previous, Animate) and \
+                previous.status == ActionStatus.collision and previous.record.name == self.record.name:
+            self.status = ActionStatus.collision
+        """:field
+        If True, play the animation forwards. If False, play the animation backwards.
+        """
+        self.forward: bool = forward
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        # Download the animation if needed. Play the animation.
+        commands.extend([{"$type": "add_humanoid_animation",
+                          "name": self.record.name,
+                          "url": self.record.get_url()},
+                         {"$type": "play_humanoid_animation",
+                          "name": self.record.name,
+                          "id": static.replicant_id,
+                          "framerate": self.record.framerate,
+                          "forward": self.forward}])
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # If there was a collision, stop the animation.
+        if len(dynamic.get_collision_enters(collision_detection=self.collision_detection)) > 0:
+            self.status = ActionStatus.collision
+        # Check if the animation is done.
+        elif dynamic.output_data_status != ActionStatus.ongoing:
+            self.status = dynamic.output_data_status
+        # Try to resolve collider intersections.
+        commands = []
+        collider_intersections_direction = dynamic.transform.forward
+        if not self.forward:
+            collider_intersections_direction = -collider_intersections_direction
+        commands.append({"$type": "replicant_resolve_collider_intersections",
+                         "id": static.replicant_id,
+                         "direction": TDWUtils.array_to_vector3(collider_intersections_direction)})
+        return commands
+
+    def get_end_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                         image_frequency: ImageFrequency) -> List[dict]:
+        commands = super().get_end_commands(resp=resp, static=static, dynamic=dynamic, image_frequency=image_frequency)
+        # Stop the animation.
+        commands.append({"$type": "stop_humanoid_animation",
+                         "id": static.replicant_id})
+        return commands
diff --git a/Python/tdw/replicant/actions/arm_motion.py b/Python/tdw/replicant/actions/arm_motion.py
new file mode 100644
index 000000000..e1b5ee50c
--- /dev/null
+++ b/Python/tdw/replicant/actions/arm_motion.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+from typing import List, Optional
+from abc import ABC
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.actions.action import Action
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.arm import Arm
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class ArmMotion(Action, ABC):
+    """
+    Abstract base class for actions related to Replicant arm motion.
+
+    Duration an arm motion, the Replicant's arm(s) will continuously over multiple `communicate()` calls move until either the motion is complete or the arm collides with something (see `self.collision_detection`).
+
+    - The collision detection will respond normally to walls, objects, obstacle avoidance, etc.
+    - If `self.collision_detection.previous_was_same == True`, and if the previous action was a subclass of `ArmMotion`, and it ended in a collision, this action ends immediately.
+    """
+
+    def __init__(self, arms: List[Arm], dynamic: ReplicantDynamic, collision_detection: CollisionDetection,
+                 duration: float, previous: Optional[Action]):
+        """
+        :param arms: A list of [`Arm`](../arm.md) values that will reach for the `target`. Example: `[Arm.left, Arm.right]`.
+        :param dynamic: The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call.
+        :param duration: The duration of the motion in seconds.
+        :param collision_detection: The [`CollisionDetection`](../collision_detection.md) rules.
+        :param previous: The previous action. Can be None.
+        """
+
+        super().__init__()
+        """:field
+        A list of [`Arm`](../arm.md) values that will reach for the `target`. Example: `[Arm.left, Arm.right]`.
+        """
+        self.arms: List[Arm] = arms
+        """:field
+        The [`CollisionDetection`](../collision_detection.md) rules.
+        """
+        self.collision_detection: CollisionDetection = collision_detection
+        # Ignore collision detection for held items.
+        self.__held_objects: List[int] = [v for v in dynamic.held_objects.values() if v not in self.collision_detection.exclude_objects]
+        self.collision_detection.exclude_objects.extend(self.__held_objects)
+        """:field
+        The duration of the motion in seconds.
+        """
+        self.duration: float = duration
+        """:field
+        If the action fails in a collision, this is a list of arms that collided with something.
+        """
+        self.collisions: List[Arm] = list()
+        # Immediately end the action if the previous action was the same motion and it ended with a collision.
+        if self.collision_detection.previous_was_same and previous is not None and isinstance(previous, ArmMotion):
+            for arm in self.arms:
+                if arm in previous.collisions:
+                    self.status = ActionStatus.collision
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        if len(dynamic.get_collision_enters(collision_detection=self.collision_detection)) > 0:
+            self.status = ActionStatus.collision
+        # Check if the motion is done.
+        elif dynamic.output_data_status != ActionStatus.ongoing:
+            self.status = dynamic.output_data_status
+        return []
+
+    def get_end_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                         image_frequency: ImageFrequency) -> List[dict]:
+        # Ignore held objects.
+        for object_id in self.__held_objects:
+            self.collision_detection.exclude_objects.remove(object_id)
+        return super().get_end_commands(resp=resp, static=static, dynamic=dynamic, image_frequency=image_frequency)
diff --git a/Python/tdw/replicant/actions/drop.py b/Python/tdw/replicant/actions/drop.py
new file mode 100644
index 000000000..44fe8fb32
--- /dev/null
+++ b/Python/tdw/replicant/actions/drop.py
@@ -0,0 +1,80 @@
+from typing import List
+import numpy as np
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.actions.action import Action
+from tdw.replicant.arm import Arm
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class Drop(Action):
+    """
+    Drop a held object.
+
+    The action ends when the object stops moving or the number of consecutive `communicate()` calls since dropping the object exceeds `self.max_num_frames`.
+
+    When an object is dropped, it is made non-kinematic. Any objects contained by the object are parented to it and also made non-kinematic. For more information regarding containment in TDW, [read this](../../../lessons/semantic_states/containment.md).
+    """
+
+    def __init__(self, arm: Arm, dynamic: ReplicantDynamic, max_num_frames: int):
+        """
+        :param arm: The [`Arm`](../arm.md) holding the object.
+        :param dynamic: The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call.
+        :param max_num_frames: Wait this number of `communicate()` calls maximum for the object to stop moving before ending the action.
+        """
+
+        super().__init__()
+        """:field
+        The [`Arm`](../arm.md) holding the object.
+        """
+        self.arm: Arm = arm
+        if arm not in dynamic.held_objects:
+            self.status = ActionStatus.not_holding
+            """:field
+            The ID of the held object.
+            """
+            self.object_id: int = 0
+        else:
+            self.object_id = dynamic.held_objects[arm]
+        """:field
+        The current position of the object.
+        """
+        self.object_position: np.ndarray = np.zeros(shape=3)
+        """:field
+        Wait this number of `communicate()` calls maximum for the object to stop moving before ending the action.
+        """
+        self.max_num_frames: int = max_num_frames
+        """:field
+        The current frame.
+        """
+        self.frame_count: int = 0
+        self._first_frame: bool = True
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        commands.append({"$type": "replicant_drop_object",
+                         "id": static.replicant_id,
+                         "arm": self.arm.name})
+        self.object_position = self._get_object_position(object_id=self.object_id, resp=resp)
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # The build might've signaled that the action ended in failure.
+        if dynamic.output_data_status != ActionStatus.ongoing or self._first_frame:
+            self._first_frame = False
+            self.status = dynamic.output_data_status
+            return []
+        position = self._get_object_position(object_id=self.object_id, resp=resp)
+        # The object stopped moving or fell through the floor.
+        if np.linalg.norm(position - self.object_position) < 0.001 or position[1] < -0.1:
+            self.status = ActionStatus.success
+        else:
+            # Update the current position.
+            self.object_position = position
+            self.frame_count += 1
+            if self.frame_count >= self.max_num_frames:
+                self.status = ActionStatus.still_dropping
+        return []
diff --git a/Python/tdw/replicant/actions/grasp.py b/Python/tdw/replicant/actions/grasp.py
new file mode 100644
index 000000000..e04c4b5e1
--- /dev/null
+++ b/Python/tdw/replicant/actions/grasp.py
@@ -0,0 +1,91 @@
+from typing import List, Optional
+from tdw.replicant.actions.action import Action
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.arm import Arm
+from tdw.replicant.image_frequency import ImageFrequency
+from tdw.output_data import OutputData, Containment
+
+
+class Grasp(Action):
+    """
+    Grasp a target object.
+
+    The action fails if the hand is already holding an object. Otherwise, the action succeeds.
+
+    When an object is grasped, it is made kinematic. Any objects contained by the object are parented to it and also made kinematic. For more information regarding containment in TDW, [read this](../../../lessons/semantic_states/containment.md).
+    """
+
+    def __init__(self, target: int, arm: Arm, dynamic: ReplicantDynamic, angle: Optional[float], axis: Optional[str]):
+        """
+        :param target: The target object ID.
+        :param arm: The [`Arm`](../arm.md) value for the hand that will grasp the target object.
+        :param dynamic: The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call.
+        :param angle: Continuously (per `communicate()` call, including after this action ends), rotate the the grasped object by this many degrees relative to the hand. If None, the grasped object will maintain its initial rotation.
+        :param axis: Continuously (per `communicate()` call, including after this action ends) rotate the grasped object around this axis relative to the hand. Options: `"pitch"`, `"yaw"`, `"roll"`. If None, the grasped object will maintain its initial rotation.
+        """
+
+        super().__init__()
+        """:field
+        The target object ID.
+        """
+        self.target: int = target
+        """:field
+        The [`Arm`](../arm.md) value for the hand that will grasp the target object.
+        """
+        self.arm: Arm = arm
+        """:field
+        Continuously (per `communicate()` call, including after this action ends), rotate the grasped object by this many degrees. If None, the grasped object will maintain its initial rotation.
+        """
+        self.angle: Optional[float] = angle
+        """:field
+        Continuously (per `communicate()` call, including after this action ends), rotate the grasped object around this axis. Options: `"pitch"`, `"yaw"`, `"roll"`. If None, the grasped object will maintain its initial rotation.
+        """
+        self.axis: Optional[str] = axis
+        # We're already holding an object.
+        if self.arm in dynamic.held_objects:
+            self.status = ActionStatus.already_holding
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        # Get all of the objects contained by the grasped object. Parent them to the container and make them kinematic.
+        for i in range(len(resp) - 1):
+            r_id = OutputData.get_data_type_id(resp[i])
+            if r_id == "cont":
+                containment = Containment(resp[i])
+                object_id = containment.get_object_id()
+                if object_id == self.target:
+                    overlap_ids = containment.get_overlap_ids()
+                    for overlap_id in overlap_ids:
+                        commands.extend([{"$type": "parent_object_to_object",
+                                          "parent_id": self.target,
+                                          "id": int(overlap_id)},
+                                         {"$type": "set_kinematic_state",
+                                          "id": int(overlap_id),
+                                          "is_kinematic": True,
+                                          "use_gravity": False}])
+        # Grasp the object.
+        commands.append({"$type": "replicant_grasp_object",
+                         "id": static.replicant_id,
+                         "arm": self.arm.name,
+                         "object_id": self.target})
+        # Set the object's rotation.
+        if self.angle is not None and self.axis is not None:
+            commands.append({"$type": "replicant_set_grasped_object_rotation",
+                             "id": static.replicant_id,
+                             "arm": self.arm.name,
+                             "angle": self.angle,
+                             "axis": self.axis})
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # The build might've signaled that the action ended in failure.
+        if dynamic.output_data_status != ActionStatus.ongoing:
+            self.status = dynamic.output_data_status
+        # If the build didn't signal anything, assume that we grasped the object.
+        else:
+            self.status = ActionStatus.success
+        return []
diff --git a/Python/tdw/replicant/actions/head_motion.py b/Python/tdw/replicant/actions/head_motion.py
new file mode 100644
index 000000000..fb68f4b55
--- /dev/null
+++ b/Python/tdw/replicant/actions/head_motion.py
@@ -0,0 +1,33 @@
+from abc import ABC
+from typing import List
+from overrides import final
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.actions.action import Action
+from tdw.replicant.action_status import ActionStatus
+
+
+class HeadMotion(Action, ABC):
+    """
+    Abstract base class for actions that rotate the Replicant's head.
+
+    Duration an head motion, the Replicant's head will continuously move over multiple `communicate()` calls until either the motion is complete.
+    """
+
+    def __init__(self, duration: float):
+        """
+        :param duration: The duration of the motion in seconds.
+        """
+
+        super().__init__()
+        """:field
+        The duration of the motion in seconds.
+        """
+        self.duration: float = duration
+
+    @final
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Check if the motion is done.
+        if dynamic.output_data_status != ActionStatus.ongoing:
+            self.status = dynamic.output_data_status
+        return []
diff --git a/Python/tdw/replicant/actions/look_at.py b/Python/tdw/replicant/actions/look_at.py
new file mode 100644
index 000000000..8ef38e5c9
--- /dev/null
+++ b/Python/tdw/replicant/actions/look_at.py
@@ -0,0 +1,54 @@
+from typing import List, Dict, Union
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.actions.head_motion import HeadMotion
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class LookAt(HeadMotion):
+    """
+    Look at a target object or position.
+
+    The head will continuously move over multiple `communicate()` calls until it is looking at the target.
+    """
+
+    def __init__(self, target: Union[int, np.ndarray, Dict[str,  float]], duration: float):
+        """
+        :param target: The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array.
+        :param duration: The duration of the motion in seconds.
+        """
+
+        super().__init__(duration=duration)
+        """:field
+        The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array.
+        """
+        self.target: Union[int, np.ndarray, Dict[str,  float]] = target
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        # Look at a target position.
+        if isinstance(self.target, np.ndarray):
+            commands.append({"$type": "replicant_look_at_position",
+                             "id": static.replicant_id,
+                             "position": TDWUtils.array_to_vector3(self.target),
+                             "duration": self.duration})
+        # Reach for a target position.
+        elif isinstance(self.target, dict):
+            commands.append({"$type": "replicant_look_at_position",
+                             "id": static.replicant_id,
+                             "position": self.target,
+                             "duration": self.duration})
+        # Reach for a target object.
+        elif isinstance(self.target, int):
+            commands.append({"$type": "replicant_look_at_object",
+                             "id": static.replicant_id,
+                             "object_id": self.target,
+                             "use_centroid": True,
+                             "duration": self.duration})
+        else:
+            raise Exception(f"Invalid target: {self.target}")
+        return commands
diff --git a/Python/tdw/replicant/actions/move_by.py b/Python/tdw/replicant/actions/move_by.py
new file mode 100644
index 000000000..583fe0804
--- /dev/null
+++ b/Python/tdw/replicant/actions/move_by.py
@@ -0,0 +1,172 @@
+from __future__ import annotations
+from typing import Dict, List, Optional
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.replicant.actions.animate import Animate
+from tdw.replicant.actions.action import Action
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.image_frequency import ImageFrequency
+from tdw.replicant.arm import Arm
+from tdw.output_data import OutputData, Overlap
+
+
+class MoveBy(Animate):
+    """
+    Walk a given distance.
+
+    The Replicant will continuously play a walk cycle animation until the action ends.
+
+    The action can end for several reasons depending on the collision detection rules (see [`self.collision_detection`](../collision_detection.md).
+
+    - If the Replicant walks the target distance, the action succeeds.
+    - If `self.collision_detection.previous_was_same == True`, and the previous action was `MoveBy` or `MoveTo`, and it was in the same direction (forwards/backwards), and the previous action ended in failure, this action ends immediately.
+    - If `self.collision_detection.avoid_obstacles == True` and the Replicant encounters a wall or object in its path:
+      - If the object is in `self.collision_detection.exclude_objects`, the Replicant ignores it.
+      - Otherwise, the action ends in failure.
+    - If the Replicant collides with an object or a wall and `self.collision_detection.objects == True` and/or `self.collision_detection.walls == True` respectively:
+      - If the object is in `self.collision_detection.exclude_objects`, the Replicant ignores it.
+      - Otherwise, the action ends in failure.
+    - If the Replicant takes too long to reach the target distance, the action ends in failure (see `self.max_walk_cycles`).
+    """
+
+    """:class_var
+    While walking, the Replicant will cast an overlap shape in front of or behind it, depending on whether it is walking forwards or backwards. The overlap is used to detect object prior to collision (see `self.collision_detection.avoid_obstacles`). These are the half-extents of the overlap shape.
+    """
+    OVERLAP_HALF_EXTENTS: Dict[str, float] = {"x": 0.31875, "y": 0.8814, "z": 0.0875}
+
+    def __init__(self, distance: float, dynamic: ReplicantDynamic, collision_detection: CollisionDetection,
+                 previous: Optional[Action], reset_arms: bool, reset_arms_duration: float, arrived_at: float, max_walk_cycles: int):
+        """
+        :param distance: The target distance. If less than 0, the Replicant will walk backwards.
+        :param dynamic: The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call.
+        :param collision_detection: The [`CollisionDetection`](../collision_detection.md) rules.
+        :param previous: The previous action, if any.
+        :param reset_arms: If True, reset the arms to their neutral positions while beginning the walk cycle.
+        :param reset_arms_duration: The speed at which the arms are reset in seconds.
+        :param arrived_at: If at any point during the action the difference between the target distance and distance traversed is less than this, then the action is successful.
+        :param max_walk_cycles: The walk animation will loop this many times maximum. If by that point the Replicant hasn't reached its destination, the action fails.
+        """
+
+        """:field
+        The target distance. If less than 0, the Replicant will walk backwards.
+        """
+        self.distance: float = distance
+        """:field
+        If True, reset the arms to their neutral positions while beginning the walk cycle.
+        """
+        self.reset_arms: bool = reset_arms
+        """:field
+        The speed at which the arms are reset in seconds.
+        """
+        self.reset_arms_duration: float = reset_arms_duration
+        """:field
+        If at any point during the action the difference between the target distance and distance traversed is less than this, then the action is successful.
+        """
+        self.arrived_at: float = arrived_at
+        super().__init__(animation="walking_2",
+                         collision_detection=collision_detection,
+                         library="humanoid_animations.json",
+                         previous=previous,
+                         forward=self.distance > 0)
+        self._destination: np.ndarray = dynamic.transform.position + (dynamic.transform.forward * distance)
+        """:field
+        The walk animation will loop this many times maximum. If by that point the Replicant hasn't reached its destination, the action fails.
+        """
+        self.max_walk_cycles: int = max_walk_cycles
+        """:field
+        The current walk cycle.
+        """
+        self.walk_cycle: int = 0
+        # Don't try to walk in the same direction twice.
+        if self.collision_detection.previous_was_same and previous is not None and isinstance(previous, MoveBy) and \
+                previous.status == ActionStatus.collision and np.sign(previous.distance) == np.sign(self.distance):
+            self.status = ActionStatus.collision
+        # Ignore collision detection for held items.
+        self.__held_objects: List[int] = [v for v in dynamic.held_objects.values() if v not in self.collision_detection.exclude_objects]
+        self.collision_detection.exclude_objects.extend(self.__held_objects)
+        self._initial_position: np.ndarray = np.zeros(shape=3)
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        self._initial_position = dynamic.transform.position
+        # Reset the arms.
+        if self.reset_arms:
+            commands.extend([{"$type": "replicant_reset_arm",
+                              "id": static.replicant_id,
+                              "duration": self.reset_arms_duration,
+                              "arm": arm.name} for arm in Arm])
+        # Reset the head.
+        commands.append({"$type": "replicant_reset_head",
+                         "id": static.replicant_id})
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        commands = super().get_ongoing_commands(resp=resp, static=static, dynamic=dynamic)
+        # Reset the action status because we want to loop the animation.
+        if self.status == ActionStatus.success:
+            self.status = ActionStatus.ongoing
+        if self.status != ActionStatus.ongoing:
+            return commands
+        else:
+            distance_to_target = np.linalg.norm(dynamic.transform.position - self._destination)
+            distance_traversed = np.linalg.norm(dynamic.transform.position - self._initial_position)
+            # We arrived at the target.
+            if distance_to_target < self.arrived_at or distance_traversed > abs(self.distance):
+                self.status = ActionStatus.success
+            # Stop walking if there is a collision.
+            elif len(dynamic.get_collision_enters(collision_detection=self.collision_detection)) > 0:
+                self.status = ActionStatus.collision
+            else:
+                # Try to avoid obstacles by detecting them ahead of time with the Replicant's trigger collider.
+                if self.collision_detection.avoid:
+                    # Check for overlap.
+                    overlap_z = 0.5
+                    if self.distance < 0:
+                        overlap_z *= -1
+                    overlap_position = dynamic.transform.position + (dynamic.transform.forward * overlap_z)
+                    overlap_position[1] += 1
+                    # Send the next overlap command.
+                    commands.append({"$type": "send_overlap_box",
+                                     "id": static.replicant_id,
+                                     "half_extents": MoveBy.OVERLAP_HALF_EXTENTS,
+                                     "rotation": TDWUtils.array_to_vector4(dynamic.transform.rotation),
+                                     "position": TDWUtils.array_to_vector3(overlap_position)})
+                    for i in range(len(resp) - 1):
+                        r_id = OutputData.get_data_type_id(resp[i])
+                        if r_id == "over":
+                            overlap = Overlap(resp[i])
+                            if overlap.get_id() == static.replicant_id:
+                                # We detected a wall.
+                                if overlap.get_env() and overlap.get_walls():
+                                    self.status = ActionStatus.detected_obstacle
+                                    return commands
+                                object_ids = overlap.get_object_ids()
+                                for object_id in object_ids:
+                                    # We detected an object.
+                                    if object_id != static.replicant_id and object_id not in self.collision_detection.exclude_objects:
+                                        self.status = ActionStatus.detected_obstacle
+                                        return commands
+                # We're at the end of the walk cycle. Continue the animation.
+                if dynamic.output_data_status == ActionStatus.success:
+                    commands.append({"$type": "play_humanoid_animation",
+                                     "name": self.record.name,
+                                     "id": static.replicant_id,
+                                     "framerate": self.record.framerate,
+                                     "forward": self.distance > 0})
+                    # Too many walk cycles. End the action.
+                    self.walk_cycle += 1
+                    if self.walk_cycle >= self.max_walk_cycles:
+                        self.status = ActionStatus.failed_to_move
+            return commands
+
+    def get_end_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                         image_frequency: ImageFrequency) -> List[dict]:
+        # Stop excluding held objects.
+        for object_id in self.__held_objects:
+            self.collision_detection.exclude_objects.remove(object_id)
+        return super().get_end_commands(resp=resp, static=static, dynamic=dynamic, image_frequency=image_frequency)
diff --git a/Python/tdw/replicant/actions/move_to.py b/Python/tdw/replicant/actions/move_to.py
new file mode 100644
index 000000000..bc7cd99a3
--- /dev/null
+++ b/Python/tdw/replicant/actions/move_to.py
@@ -0,0 +1,147 @@
+from __future__ import annotations
+from typing import Union, Dict, List, Optional
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.output_data import OutputData, Bounds
+from tdw.replicant.actions.action import Action
+from tdw.replicant.actions.turn_to import TurnTo
+from tdw.replicant.actions.move_by import MoveBy
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class MoveTo(Action):
+    """
+    Turn the Replicant to a target position or object and then walk to it.
+
+    While walking, the Replicant will continuously play a walk cycle animation until the action ends.
+
+    The action can end for several reasons depending on the collision detection rules (see [`self.collision_detection`](../collision_detection.md).
+
+    - If the Replicant walks the target distance (i.e. it reaches its target), the action succeeds.
+    - If `self.collision_detection.previous_was_same == True`, and the previous action was `MoveBy` or `MoveTo`, and it was in the same direction (forwards/backwards), and the previous action ended in failure, this action ends immediately.
+    - If `self.collision_detection.avoid_obstacles == True` and the Replicant encounters a wall or object in its path:
+      - If the object is in `self.collision_detection.exclude_objects`, the Replicant ignores it.
+      - Otherwise, the action ends in failure.
+    - If the Replicant collides with an object or a wall and `self.collision_detection.objects == True` and/or `self.collision_detection.walls == True` respectively:
+      - If the object is in `self.collision_detection.exclude_objects`, the Replicant ignores it.
+      - Otherwise, the action ends in failure.
+    - If the Replicant takes too long to reach the target distance, the action ends in failure (see `self.max_walk_cycles`).
+    """
+
+    def __init__(self, target: Union[int, Dict[str, float], np.ndarray], collision_detection: CollisionDetection,
+                 previous: Optional[Action], reset_arms: bool, reset_arms_duration: float, arrived_at: float,
+                 max_walk_cycles: int, bounds_position: str):
+        """
+        :param target: The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array.
+        :param collision_detection: The [`CollisionDetection`](../collision_detection.md) rules.
+        :param previous: The previous action, if any.
+        :param reset_arms: If True, reset the arms to their neutral positions while beginning the walk cycle.
+        :param reset_arms_duration: The speed at which the arms are reset in seconds.
+        :param arrived_at: If at any point during the action the difference between the target distance and distance traversed is less than this, then the action is successful.
+        :param max_walk_cycles: The walk animation will loop this many times maximum. If by that point the Replicant hasn't reached its destination, the action fails.
+        :param bounds_position: If `target` is an integer object ID, move towards this bounds point of the object. Options: `"center"`, `"top`", `"bottom"`, `"left"`, `"right"`, `"front"`, `"back"`.
+        """
+
+        """:field
+        The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array.
+        """
+        self.target: Union[int, Dict[str, float], np.ndarray] = target
+        """:field
+        The [`CollisionDetection`](../collision_detection.md) rules.
+        """
+        self.collision_detection: CollisionDetection = collision_detection
+        """:field
+        If True, reset the arms to their neutral positions while beginning the walk cycle.
+        """
+        self.reset_arms: bool = reset_arms
+        """:field
+        The speed at which the arms are reset in seconds.
+        """
+        self.reset_arms_duration: float = reset_arms_duration
+        """:field
+        If at any point during the action the difference between the target distance and distance traversed is less than this, then the action is successful.
+        """
+        self.arrived_at: float = arrived_at
+        """:field
+        The walk animation will loop this many times maximum. If by that point the Replicant hasn't reached its destination, the action fails.
+        """
+        self.max_walk_cycles: int = max_walk_cycles
+        """:field
+        If `target` is an integer object ID, move towards this bounds point of the object. Options: `"center"`, `"top`", `"bottom"`, `"left"`, `"right"`, `"front"`, `"back"`.
+        """
+        self.bounds_position: str = bounds_position
+        self._turning: bool = True
+        self._image_frequency: ImageFrequency = ImageFrequency.once
+        self._move_by: Optional[MoveBy] = None
+        self._previous_action: Optional[Action] = previous
+        super().__init__()
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        # Remember the image frequency for both the turn and move sub-actions.
+        self._image_frequency = image_frequency
+        # Turn to the target.
+        return TurnTo(target=self.target).get_initialization_commands(resp=resp,
+                                                                      static=static,
+                                                                      dynamic=dynamic,
+                                                                      image_frequency=image_frequency)
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Turning requires only one `communicate()` call. Now, it's time to start walking.
+        if self._turning:
+            self._turning = False
+            # Get the target position.
+            if isinstance(self.target, np.ndarray):
+                target_position: np.ndarray = self.target
+            elif isinstance(self.target, dict):
+                target_position = TDWUtils.vector3_to_array(self.target)
+            # If the target is and object ID, the target position is a bounds position.
+            elif isinstance(self.target, int):
+                target_position = np.zeros(shape=3)
+                for i in range(len(resp) - 1):
+                    # Get the output data ID.
+                    r_id = OutputData.get_data_type_id(resp[i])
+                    # Get the bounds data.
+                    if r_id == "boun":
+                        bounds = Bounds(resp[i])
+                        for j in range(bounds.get_num()):
+                            if bounds.get_id(j) == self.target:
+                                bound = TDWUtils.get_bounds_dict(bounds, j)
+                                target_position = bound[self.bounds_position]
+                                break
+                        break
+            else:
+                raise Exception(f"Invalid target: {self.target}")
+            # Get the distance to the target. The distance is positive because we already turned to the target.
+            distance = np.linalg.norm(dynamic.transform.position - target_position)
+            # Start walking.
+            self._move_by = MoveBy(distance=float(distance),
+                                   dynamic=dynamic,
+                                   collision_detection=self.collision_detection,
+                                   previous=self._previous_action,
+                                   reset_arms=self.reset_arms,
+                                   reset_arms_duration=self.reset_arms_duration,
+                                   arrived_at=self.arrived_at,
+                                   max_walk_cycles=self.max_walk_cycles)
+            commands = self._move_by.get_initialization_commands(resp=resp,
+                                                                 static=static,
+                                                                 dynamic=dynamic,
+                                                                 image_frequency=self._image_frequency)
+            self.status = self._move_by.status
+            return commands
+        # Keep walking.
+        commands = self._move_by.get_ongoing_commands(resp=resp,
+                                                      static=static,
+                                                      dynamic=dynamic)
+        self.status = self._move_by.status
+        return commands
+
+    def get_end_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                         image_frequency: ImageFrequency) -> List[dict]:
+        if self._move_by is not None:
+            return self._move_by.get_end_commands(resp=resp, static=static, dynamic=dynamic, image_frequency=image_frequency)
+        else:
+            return super().get_end_commands(resp=resp, static=static, dynamic=dynamic, image_frequency=image_frequency)
diff --git a/Python/tdw/replicant/actions/reach_for.py b/Python/tdw/replicant/actions/reach_for.py
new file mode 100644
index 000000000..e6f3f17f6
--- /dev/null
+++ b/Python/tdw/replicant/actions/reach_for.py
@@ -0,0 +1,121 @@
+from __future__ import annotations
+from typing import List, Dict, Union, Optional
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.actions.arm_motion import ArmMotion
+from tdw.replicant.actions.action import Action
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.arm import Arm
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class ReachFor(ArmMotion):
+    """
+    Reach for a target object or position. One or both hands can reach for the target at the same time.
+
+    If target is an object, the target position is a point on the object.
+    If the object has affordance points, the target position is the affordance point closest to the hand.
+    Otherwise, the target position is the bounds position closest to the hand.
+
+    The Replicant's arm(s) will continuously over multiple `communicate()` calls move until either the motion is complete or the arm collides with something (see `self.collision_detection`).
+
+    - If the hand is near the target at the end of the action, the action succeeds.
+    - If the target is too far away at the start of the action, the action fails.
+    - The collision detection will respond normally to walls, objects, obstacle avoidance, etc.
+    - If `self.collision_detection.previous_was_same == True`, and if the previous action was a subclass of `ArmMotion`, and it ended in a collision, this action ends immediately.
+    """
+
+    def __init__(self, target: Union[int, np.ndarray, Dict[str,  float]], offhand_follows: bool,
+                 arrived_at: float, max_distance: float, arms: List[Arm], dynamic: ReplicantDynamic,
+                 collision_detection: CollisionDetection, duration: float, previous: Optional[Action]):
+        """
+        :param target: The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array.
+        :param offhand_follows: If True, the offhand will follow the primary hand, meaning that it will maintain the same relative position. Ignored if `len(arms) > 1` or if `target` is an object ID.
+        :param arrived_at: If the motion ends and the hand is this distance or less from the target, the action succeeds.
+        :param max_distance: If the target is further away from this distance at the start of the action, the action fails.
+        :param arms: A list of [`Arm`](../arm.md) values that will reach for the `target`. Example: `[Arm.left, Arm.right]`.
+        :param dynamic: The [`ReplicantDynamic`](../replicant_dynamic.md) data that changes per `communicate()` call.
+        :param collision_detection: The [`CollisionDetection`](../collision_detection.md) rules.
+        :param duration: The duration of the motion in seconds.
+        :param previous: The previous action. Can be None.
+        """
+
+        super().__init__(arms=arms, dynamic=dynamic, collision_detection=collision_detection, previous=previous,
+                         duration=duration)
+        """:field
+        The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array.
+        """
+        self.target: Union[int, np.ndarray, Dict[str,  float]] = target
+        """:field
+        If the motion ends and the hand is this distance or less from the target, the action succeeds.
+        """
+        self.arrived_at: float = arrived_at
+        """:field
+        If the target is further away from this distance at the start of the action, the action fails.
+        """
+        self.max_distance: float = max_distance
+        """:field
+        If True, the offhand will follow the primary hand, meaning that it will maintain the same relative position. Ignored if `len(arms) > 1` or if `target` is an object ID.
+        """
+        self.offhand_follows: bool = offhand_follows
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        # Reach for a target position.
+        if isinstance(self.target, np.ndarray):
+            commands.extend(self._get_reach_for_position(target=TDWUtils.array_to_vector3(self.target),
+                                                         static=static,
+                                                         dynamic=dynamic))
+        # Reach for a target position.
+        elif isinstance(self.target, dict):
+            commands.extend(self._get_reach_for_position(target=self.target,
+                                                         static=static,
+                                                         dynamic=dynamic))
+        # Reach for a target object.
+        elif isinstance(self.target, int):
+            commands.extend([{"$type": "replicant_reach_for_object",
+                              "id": static.replicant_id,
+                              "object_id": int(self.target),
+                              "duration": self.duration,
+                              "arm": arm.name,
+                              "max_distance": self.max_distance,
+                              "arrived_at": self.arrived_at} for arm in self.arms])
+        else:
+            raise Exception(f"Invalid target: {self.target}")
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        # Check if we can't reach the target or if the action is done.
+        self.status = dynamic.output_data_status
+        # Continue the action, checking for collisions.
+        return super().get_ongoing_commands(resp=resp, static=static, dynamic=dynamic)
+
+    def _get_reach_for_position(self, target: Dict[str, float], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        commands = [{"$type": "replicant_reach_for_position",
+                     "id": static.replicant_id,
+                     "position": target,
+                     "duration": self.duration,
+                     "arm": arm.name,
+                     "max_distance": self.max_distance,
+                     "arrived_at": self.arrived_at} for arm in self.arms]
+        # Tell the offhand to follow.
+        if self.offhand_follows and len(self.arms) == 1:
+            # Get the offset to the target.
+            offset = TDWUtils.vector3_to_array(target) - dynamic.body_parts[static.hands[self.arms[0]]].position
+            # Get the offhand.
+            offhand = Arm.right if self.arms[0] == Arm.left else Arm.left
+            # Get the position.
+            position = dynamic.body_parts[static.hands[offhand]].position + offset
+            # Set the target of the offhand.
+            commands.append({"$type": "replicant_reach_for_position",
+                             "id": static.replicant_id,
+                             "position": TDWUtils.array_to_vector3(position),
+                             "duration": self.duration,
+                             "arm": offhand.name,
+                             "max_distance": self.max_distance,
+                             "arrived_at": self.arrived_at})
+        return commands
diff --git a/Python/tdw/replicant/actions/reset_arm.py b/Python/tdw/replicant/actions/reset_arm.py
new file mode 100644
index 000000000..a6d769bdb
--- /dev/null
+++ b/Python/tdw/replicant/actions/reset_arm.py
@@ -0,0 +1,26 @@
+from typing import List
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.image_frequency import ImageFrequency
+from tdw.replicant.actions.arm_motion import ArmMotion
+
+
+class ResetArm(ArmMotion):
+    """
+    Move arm(s) back to rest position(s). One or both arms can be reset at the same time.
+
+    The Replicant's arm(s) will continuously over multiple `communicate()` calls move until either the motion is complete or the arm collides with something (see `self.collision_detection`).
+
+    - The collision detection will respond normally to walls, objects, obstacle avoidance, etc.
+    - If `self.collision_detection.previous_was_same == True`, and if the previous action was a subclass of `ArmMotion`, and it ended in a collision, this action ends immediately.
+    """
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        commands.extend([{"$type": "replicant_reset_arm",
+                          "id": static.replicant_id,
+                          "duration": self.duration,
+                          "arm": arm.name} for arm in self.arms])
+        return commands
diff --git a/Python/tdw/replicant/actions/reset_head.py b/Python/tdw/replicant/actions/reset_head.py
new file mode 100644
index 000000000..c92db416b
--- /dev/null
+++ b/Python/tdw/replicant/actions/reset_head.py
@@ -0,0 +1,29 @@
+from typing import List
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.actions.head_motion import HeadMotion
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class ResetHead(HeadMotion):
+    """
+    Reset the head to its neutral rotation.
+
+    The head will continuously move over multiple `communicate()` calls until it is at its neutral rotation.
+    """
+
+    def __init__(self, duration: float):
+        """
+        :param duration: The duration of the motion in seconds.
+        """
+
+        super().__init__(duration=duration)
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        commands.append({"$type": "replicant_reset_head",
+                         "id": static.replicant_id,
+                         "duration": self.duration})
+        return commands
diff --git a/Python/tdw/replicant/actions/turn_by.py b/Python/tdw/replicant/actions/turn_by.py
new file mode 100644
index 000000000..086fb7bbb
--- /dev/null
+++ b/Python/tdw/replicant/actions/turn_by.py
@@ -0,0 +1,38 @@
+from typing import List
+from tdw.replicant.actions.action import Action
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class TurnBy(Action):
+    """
+    Turn by an angle.
+
+    This is a non-animated action, meaning that the Replicant will immediately snap to the angle.
+    """
+
+    def __init__(self, angle: float):
+        """
+        :param: The angle to turn by, in degrees.
+        """
+
+        super().__init__()
+        self._angle: float = angle
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        commands.append({"$type": "rotate_object_by",
+                         "angle": self._angle,
+                         "id": static.replicant_id,
+                         "axis": "yaw",
+                         "is_world": True,
+                         "use_centroid": False})
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        self.status = ActionStatus.success
+        return []
diff --git a/Python/tdw/replicant/actions/turn_to.py b/Python/tdw/replicant/actions/turn_to.py
new file mode 100644
index 000000000..7713566c8
--- /dev/null
+++ b/Python/tdw/replicant/actions/turn_to.py
@@ -0,0 +1,47 @@
+from typing import Union, Dict, List
+import numpy as np
+from tdw.tdw_utils import TDWUtils
+from tdw.replicant.actions.action import Action
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.replicant_static import ReplicantStatic
+from tdw.replicant.replicant_dynamic import ReplicantDynamic
+from tdw.replicant.image_frequency import ImageFrequency
+
+
+class TurnTo(Action):
+    """
+    Turn to a target object or position.
+
+    This is a non-animated action, meaning that the Replicant will immediately snap to the angle.
+    """
+
+    def __init__(self, target: Union[int, Dict[str, float], np.ndarray]):
+        """
+        :param target: The target. If int: An object ID. If dict: A position as an x, y, z dictionary. If numpy array: A position as an [x, y, z] numpy array.
+        """
+
+        super().__init__()
+        self._target: Union[int, Dict[str, float], np.ndarray] = target
+
+    def get_initialization_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic,
+                                    image_frequency: ImageFrequency) -> List[dict]:
+        commands = super().get_initialization_commands(resp=resp, static=static, dynamic=dynamic,
+                                                       image_frequency=image_frequency)
+        if isinstance(self._target, int):
+            position = TDWUtils.array_to_vector3(self._get_object_position(object_id=self._target, resp=resp))
+        elif isinstance(self._target, np.ndarray):
+            position = TDWUtils.array_to_vector3(self._target)
+        elif isinstance(self._target, dict):
+            position = {k: v for k, v in self._target.items()}
+        else:
+            raise Exception(f"Invalid target: {self._target}")
+        # Set the y value to match the Replicant's y value.
+        position["y"] = float(dynamic.transform.position[1])
+        commands.append({"$type": "object_look_at_position",
+                         "position": position,
+                         "id": static.replicant_id})
+        return commands
+
+    def get_ongoing_commands(self, resp: List[bytes], static: ReplicantStatic, dynamic: ReplicantDynamic) -> List[dict]:
+        self.status = ActionStatus.success
+        return []
diff --git a/Python/tdw/replicant/arm.py b/Python/tdw/replicant/arm.py
new file mode 100644
index 000000000..dab04bd34
--- /dev/null
+++ b/Python/tdw/replicant/arm.py
@@ -0,0 +1,10 @@
+from enum import Enum
+
+
+class Arm(Enum):
+    """
+    An enum that defines the side that an arm is on.
+    """
+
+    left = 0  # The left arm.
+    right = 1  # The right arm.
diff --git a/Python/tdw/replicant/collision_detection.py b/Python/tdw/replicant/collision_detection.py
new file mode 100644
index 000000000..0f9fc06cd
--- /dev/null
+++ b/Python/tdw/replicant/collision_detection.py
@@ -0,0 +1,41 @@
+from typing import List
+
+
+class CollisionDetection:
+    """
+    Parameters for how a Replicant handles collision detection.
+    """
+
+    def __init__(self, objects: bool = True, avoid: bool = True, held: bool = True, exclude_objects: List[int] = None,
+                 previous_was_same: bool = True):
+        """
+        :param objects: If True, the Replicant will stop when it collides with an object unless the is in the `exclude_objects`.
+        :param avoid: If True, while walking, the Replicant will try to stop *before* colliding with objects.
+        :param held: If True, ignore collisions between a held object and hand + lower arm holding the object.
+        :param exclude_objects: The Replicant will ignore a collision with any object in this list, *regardless* of whether or not `objects == True`. Can be None.
+        :param previous_was_same: If True, the Replicant will stop if the previous action resulted in a collision and was the same sort of action as the current one.
+        """
+
+        """:field
+        If True, the Replicant will stop when it collides with an object unless the is in the `exclude_objects`.
+        """
+        self.objects: bool = objects
+        """:field
+        If True, while walking, the Replicant will try to stop *before* colliding with objects.
+        """
+        self.avoid: bool = avoid
+        """:field
+        If True, ignore collisions between a held object and hand + lower arm holding the object.
+        """
+        self.held: bool = held
+        if exclude_objects is None:
+            """:field
+            The Replicant will ignore a collision with any object in this list, *regardless* of whether or not `objects == True` or the mass of the object. Can be None.
+            """
+            self.exclude_objects: List[int] = list()
+        else:
+            self.exclude_objects: List[int] = exclude_objects
+        """:field
+        If True, the Replicant will stop if the previous action resulted in a collision and was the same sort of action as the current one.
+        """
+        self.previous_was_same: bool = previous_was_same
diff --git a/Python/tdw/replicant/image_frequency.py b/Python/tdw/replicant/image_frequency.py
new file mode 100644
index 000000000..069cfaf87
--- /dev/null
+++ b/Python/tdw/replicant/image_frequency.py
@@ -0,0 +1,11 @@
+from enum import Enum
+
+
+class ImageFrequency(Enum):
+    """
+    The per-frame frequency of image capture.
+    """
+
+    once = 1  # Capture an image only on this `communicate()` call.
+    always = 2  # Capture images on every `communicate()` call.
+    never = 4  # Don't capture images on any `communicate()` call.
diff --git a/Python/tdw/replicant/replicant_body_part.py b/Python/tdw/replicant/replicant_body_part.py
new file mode 100644
index 000000000..56069f637
--- /dev/null
+++ b/Python/tdw/replicant/replicant_body_part.py
@@ -0,0 +1,27 @@
+from enum import IntEnum
+from typing import List
+
+
+class ReplicantBodyPart(IntEnum):
+    """
+    The name of each Replicant body part.
+    """
+
+    hand_l = 0
+    hand_r = 1
+    head = 2
+    lowerarm_l = 3
+    lowerarm_r = 4
+    lowerleg_l = 5
+    lowerleg_r = 6
+    spine_01 = 7
+    spine_02 = 8
+    spine_03 = 9
+    upperarm_l = 10
+    upperarm_r = 11
+    upperleg_l = 12
+    upperleg_r = 13
+
+
+# The body part enum values in a list.
+BODY_PARTS: List[ReplicantBodyPart] = [r for r in ReplicantBodyPart]
diff --git a/Python/tdw/replicant/replicant_dynamic.py b/Python/tdw/replicant/replicant_dynamic.py
new file mode 100644
index 000000000..7935f2f64
--- /dev/null
+++ b/Python/tdw/replicant/replicant_dynamic.py
@@ -0,0 +1,181 @@
+from typing import List, Dict, Optional, Union
+import numpy as np
+from pathlib import Path
+from tdw.tdw_utils import TDWUtils
+from PIL import Image
+from tdw.output_data import OutputData, Images, CameraMatrices, Replicants
+from tdw.object_data.transform import Transform
+from tdw.replicant.replicant_body_part import BODY_PARTS
+from tdw.replicant.collision_detection import CollisionDetection
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.arm import Arm
+
+
+class ReplicantDynamic:
+    """
+    Dynamic data for a replicant that can change per `communicate()` call (such as the position of the Replicant).
+    """
+
+    def __init__(self, resp: List[bytes], replicant_id: int, frame_count: int):
+        """
+        :param resp: The response from the build, which we assume contains `replicant` output data.
+        :param replicant_id: The ID of this replicant.
+        :param frame_count: The current frame count.
+        """
+
+        """:field
+        The [`Transform`](../object_data/transform.md) of the Replicant.
+        """
+        self.transform: Transform = Transform(np.zeros(shape=3), np.zeros(shape=4), np.zeros(shape=3))
+        """:field
+        A dictionary of objects held in each hand. Key = [`Arm`](arm.md). Value = Object ID.
+        """
+        self.held_objects: Dict[Arm, int] = dict()
+        """:field
+        The images rendered by the robot as dictionary. Key = the name of the pass. Value = the pass as a numpy array.
+        """
+        self.images: Dict[str, np.array] = dict()
+        """:field
+        The [camera projection matrix](../../api/output_data.md#cameramatrices) of the Replicant's camera as a numpy array.
+        """
+        self.projection_matrix: Optional[np.array] = None
+        """:field
+        The [camera matrix](../../api/output_data.md#cameramatrices) of the Replicant's camera as a numpy array.
+        """
+        self.camera_matrix: Optional[np.array] = None
+        # File extensions per pass.
+        self.__image_extensions: Dict[str, str] = dict()
+        """:field
+        If True, we got images from the output data.
+        """
+        self.got_images: bool = False
+        """:field
+        Transform data for each body part. Key = Body part ID. Value = [`Transform`](../object_data/transform.md).
+        """
+        self.body_parts: Dict[int, Transform] = dict()
+        """:field
+        Collision data per body part. Key = Body part ID. Value = A list of object IDs that the body part collided with.
+        """
+        self.collisions: Dict[int, List[int]] = dict()
+        """:field
+        This is meant for internal use only. For certain actions, the build will update the Replicant's `ActionStatus`. *Do not use this field to check the Replicant's status.* Always check `replicant.action.status` instead. 
+        """
+        self.output_data_status: ActionStatus = ActionStatus.ongoing
+        self._frame_count: int = frame_count
+        self._replicant_id: int = replicant_id
+        avatar_id = str(replicant_id)
+        got_data = False
+        for i in range(len(resp) - 1):
+            r_id = OutputData.get_data_type_id(resp[i])
+            # Get replicant's data.
+            if r_id == "repl":
+                replicants = Replicants(resp[i])
+                for j in range(replicants.get_num()):
+                    object_id = replicants.get_id(j)
+                    # We found the ID of this replicant.
+                    if object_id == replicant_id:
+                        # Get the held objects.
+                        if replicants.get_is_holding_left(j):
+                            self.held_objects[Arm.left] = replicants.get_held_left(j)
+                        if replicants.get_is_holding_right(j):
+                            self.held_objects[Arm.right] = replicants.get_held_right(j)
+                        # Get the body part transforms.
+                        for k in range(len(BODY_PARTS)):
+                            # Cache the transform.
+                            body_part_id = replicants.get_body_part_id(j, k)
+                            self.body_parts[body_part_id] = Transform(position=replicants.get_body_part_position(j, k),
+                                                                      forward=replicants.get_body_part_forward(j, k),
+                                                                      rotation=replicants.get_body_part_rotation(j, k))
+                            # Get collisions.
+                            self.collisions[body_part_id] = list()
+                            for m in range(10):
+                                if replicants.get_is_collision(j, k, m):
+                                    self.collisions[body_part_id].append(replicants.get_collision_id(j, k, m))
+                        self.transform = Transform(position=replicants.get_position(j),
+                                                   rotation=replicants.get_rotation(j),
+                                                   forward=replicants.get_forward(j))
+                        self.output_data_status = replicants.get_status(j)
+                        # Get collision data.
+                        got_data = True
+                        break
+            if got_data:
+                break
+        # Now that we have the body part IDs, iterate through the output data a second time to get collisions.
+        for i in range(len(resp) - 1):
+            r_id = OutputData.get_data_type_id(resp[i])
+            # Get the images captured by the avatar's camera.
+            if r_id == "imag":
+                images = Images(resp[i])
+                # Get this robot's avatar and save the images.
+                if images.get_avatar_id() == avatar_id:
+                    self.got_images = True
+                    for j in range(images.get_num_passes()):
+                        image_data = images.get_image(j)
+                        pass_mask = images.get_pass_mask(j)
+                        if pass_mask == "_depth":
+                            image_data = TDWUtils.get_shaped_depth_pass(images=images, index=j)
+                        # Remove the underscore from the pass mask such as: _img -> img
+                        pass_name = pass_mask[1:]
+                        # Save the image data.
+                        self.images[pass_name] = image_data
+                        # Record the file extension.
+                        self.__image_extensions[pass_name] = images.get_extension(j)
+            # Get the camera matrices for the avatar's camera.
+            elif r_id == "cama":
+                camera_matrices = CameraMatrices(resp[i])
+                if camera_matrices.get_avatar_id() == avatar_id:
+                    self.projection_matrix = camera_matrices.get_projection_matrix()
+                    self.camera_matrix = camera_matrices.get_camera_matrix()
+
+    def save_images(self, output_directory: Union[str, Path]) -> None:
+        """
+        Save the ID pass (segmentation colors) and the depth pass to disk.
+        Images will be named: `[frame_number]_[pass_name].[extension]`
+        For example, the depth pass on the first frame will be named: `00000000_depth.png`
+
+        The `img` pass is either a .jpg. The `id` and `depth` passes are .png files.
+
+        :param output_directory: The directory that the images will be saved to.
+        """
+
+        if isinstance(output_directory, str):
+            output_directory = Path(output_directory)
+        if not output_directory.exists():
+            output_directory.mkdir(parents=True)
+        # The prefix is a zero-padded integer to ensure sequential images.
+        prefix = TDWUtils.zero_padding(self._frame_count, 8)
+        # Save each image.
+        for pass_name in self.images:
+            if self.images[pass_name] is None:
+                continue
+            # Get the filename, such as: `00000000_img.png`
+            p = output_directory.joinpath(f"{prefix}_{pass_name}.{self.__image_extensions[pass_name]}")
+            if pass_name == "depth":
+                Image.fromarray(self.images[pass_name]).save(str(p.resolve()))
+            else:
+                with p.open("wb") as f:
+                    f.write(self.images[pass_name])
+
+    def get_collision_enters(self, collision_detection: CollisionDetection) -> List[int]:
+        """
+        :param collision_detection: The [`CollisionDetection`](collision_detection.md) rules.
+
+        :return: A list of body IDs that entered a collision on this frame, filtered by the collision detection rules.
+        """
+
+        if not collision_detection.objects:
+            return []
+        enters: List[int] = list()
+        for body_part_id in self.collisions:
+            if body_part_id not in self.body_parts:
+                continue
+            for object_id in self.collisions[body_part_id]:
+                if object_id in collision_detection.exclude_objects or object_id in self.body_parts:
+                    continue
+                # Ignore held objects.
+                if collision_detection.held and (
+                        (Arm.left in self.held_objects and self.held_objects[Arm.left] == object_id) or
+                        (Arm.right in self.held_objects and self.held_objects[Arm.right] == object_id)):
+                    continue
+                enters.append(object_id)
+        return enters
diff --git a/Python/tdw/replicant/replicant_static.py b/Python/tdw/replicant/replicant_static.py
new file mode 100644
index 000000000..62237438d
--- /dev/null
+++ b/Python/tdw/replicant/replicant_static.py
@@ -0,0 +1,61 @@
+from typing import Dict, List
+from tdw.output_data import OutputData, Replicants
+from tdw.replicant.arm import Arm
+from tdw.replicant.replicant_body_part import ReplicantBodyPart, BODY_PARTS
+
+
+class ReplicantStatic:
+    """
+    Static data for the Replicant.
+    """
+
+    """:class_var
+    A dictionary of arms and their constituent joints.
+    """
+    ARM_JOINTS: Dict[Arm, List[ReplicantBodyPart]] = {Arm.left: [__b for __b in ReplicantBodyPart if __b.name.endswith("_l")],
+                                                      Arm.right: [__b for __b in ReplicantBodyPart if __b.name.endswith("_r")]}
+
+    def __init__(self, replicant_id: int, resp: List[bytes]):
+        """
+        :param replicant_id: The ID of the Replicant.
+        :param resp: The response from the build.
+        """
+
+        """:field
+        The ID of the Replicant.
+        """
+        self.replicant_id: int = replicant_id
+        """:field
+        The ID of the Replicant's avatar (camera). This is used internally for API calls.
+        """
+        self.avatar_id: str = str(replicant_id)
+        """:field
+        Body parts by name. Key = [`ReplicantBodyPart`](replicant_body_part.md). Value = Object ID.
+        """
+        self.body_parts: Dict[ReplicantBodyPart, int] = dict()
+        got_data = False
+        for i in range(len(resp) - 1):
+            r_id = OutputData.get_data_type_id(resp[i])
+            # Get Replicants data.
+            if r_id == "repl":
+                replicants = Replicants(resp[i])
+                for j in range(replicants.get_num()):
+                    object_id = replicants.get_id(j)
+                    # We found the ID of this replicant.
+                    if object_id == self.replicant_id:
+                        for k in range(len(BODY_PARTS)):
+                            # Cache the ID.
+                            self.body_parts[BODY_PARTS[k]] = replicants.get_body_part_id(j, k)
+                        got_data = True
+                        break
+            if got_data:
+                break
+        """:field
+        The Replicant's hands. Key = [`Arm`](arm.md). Value = Hand ID.
+        """
+        self.hands: Dict[Arm, int] = {Arm.left: self.body_parts[ReplicantBodyPart.hand_l],
+                                      Arm.right: self.body_parts[ReplicantBodyPart.hand_r]}
+        """:field
+        Body parts by ID. Key = Object ID. Value = [`ReplicantBodyPart`](replicant_body_part.md).
+        """
+        self.body_parts_by_id: Dict[int, ReplicantBodyPart] = {v: k for k, v in self.body_parts.items()}
diff --git a/Python/tdw/tdw_utils.py b/Python/tdw/tdw_utils.py
index add7c07a6..233505495 100644
--- a/Python/tdw/tdw_utils.py
+++ b/Python/tdw/tdw_utils.py
@@ -613,7 +613,7 @@ def hashable_to_color(hashable: int) -> np.array:
         return np.array([(hashable >> 16) & 255, (hashable >> 8) & 255, hashable & 255], dtype=int)
 
     @staticmethod
-    def get_bounds_dict(bounds: Bounds, index: int) -> Dict[str, np.array]:
+    def get_bounds_dict(bounds: Bounds, index: int) -> Dict[str, np.ndarray]:
         """
         :param bounds: Bounds output data.
         :param index: The index in `bounds` of the target object.
diff --git a/Python/tdw/version.py b/Python/tdw/version.py
index b45c1e4dc..5f5754b22 100644
--- a/Python/tdw/version.py
+++ b/Python/tdw/version.py
@@ -1,1 +1,1 @@
-__version__ = "1.10.9"
+__version__ = "1.11.0"
diff --git a/README.md b/README.md
index 011656852..c8c33c30b 100644
--- a/README.md
+++ b/README.md
@@ -212,19 +212,35 @@ High-level API: [tdw_physics](https://github.com/alters-mit/tdw_physics)
 
 1. [Magnebot API (external repo)](https://github.com/alters-mit/magnebot)
 
-### 11.3 Virtual Reality (VR)
+### 11.3 Replicants
+
+1. [Overview](Documentation/lessons/replicants/overview.md)
+2. [Actions](Documentation/lessons/replicants/actions.md)
+3. [Output data](Documentation/lessons/replicants/output_data.md)
+4. [Collision detection](Documentation/lessons/replicants/collision_detection.md)
+5. [Movement](Documentation/lessons/replicants/movement.md)
+6. [Animations](Documentation/lessons/replicants/animations.md)
+7. [Arm articulation](Documentation/lessons/replicants/arm_articulation.md)
+8. [Grasp and drop objects](Documentation/lessons/replicants/grasp_drop.md)
+9. [Head rotation](Documentation/lessons/replicants/head_rotation.md)
+10. [Navigation](Documentation/lessons/replicants/navigation.md)
+11. [Custom actions](Documentation/lessons/replicants/custom_actions.md)
+12. [Multiple Replicants](Documentation/lessons/replicants/multiple_replicants.md)
+13. [Reset](Documentation/lessons/replicants/reset.md)
+
+### 11.4 Virtual Reality (VR)
 
 1. [Overview](Documentation/lessons/vr/overview.md)
 2. [Oculus Touch rigs](Documentation/lessons/vr/oculus_touch.md)
 
-### 11.4 Keyboard and Mouse
+### 11.5 Keyboard and Mouse
 
 1. [Overview](Documentation/lessons/keyboard_and_mouse/overview.md)
 2. [Mouse input](Documentation/lessons/keyboard_and_mouse/mouse.md)
 3. [The `FirstPersonAvatar`](Documentation/lessons/keyboard_and_mouse/first_person_avatar.md)
 4. [Keyboard input](Documentation/lessons/keyboard_and_mouse/keyboard.md)
 
-### 11.5 Embodied avatars
+### 11.6 Embodied avatars
 
 1. [The `EmbodiedAvatar`](Documentation/lessons/embodied_avatars/embodied_avatar.md)
 
@@ -320,6 +336,7 @@ High-level API: [tdw_physics](https://github.com/alters-mit/tdw_physics)
 - [PhysicsAudioRecorder](Documentation/python/add_ons/physics_audio_recorder.md)
 - [ProcGenKitchen](Documentation/python/add_ons/proc_gen_kitchen.md)
 - [PyImpact](Documentation/python/add_ons/py_impact.md)
+- [Replicant](Documentation/python/add_ons/replicant.md)
 - [ResonanceAudioInitializer](Documentation/python/add_ons/resonance_audio_initializer.md)
 - [Robot](Documentation/python/add_ons/robot.md)
 - [RobotArm](Documentation/python/add_ons/robot_arm.md)
@@ -493,6 +510,33 @@ High-level API: [tdw_physics](https://github.com/alters-mit/tdw_physics)
 - [Build](Documentation/python/release/build.md)
 - [PyPi](Documentation/python/release/pypi.md)
 
+**tdw.replicant**
+
+- [ActionStatus](Documentation/python/replicant/action_status.md)
+- [Arm](Documentation/python/replicant/arm.md)
+- [CollisionDetection](Documentation/python/replicant/collision_detection.md)
+- [ImageFrequency](Documentation/python/replicant/image_frequency.md)
+- [ReplicantBodyPart](Documentation/python/replicant/replicant_body_part.md)
+- [ReplicantDynamic](Documentation/python/replicant/replicant_dynamic.md)
+- [ReplicantStatic](Documentation/python/replicant/replicant_static.md)
+
+**tdw.replicant.actions**
+
+- [Action](Documentation/python/replicant/actions/action.md)
+- [Animate](Documentation/python/replicant/actions/animate.md)
+- [ArmMotion](Documentation/python/replicant/actions/arm_motion.md)
+- [Drop](Documentation/python/replicant/actions/drop.md)
+- [Grasp](Documentation/python/replicant/actions/grasp.md)
+- [HeadMotion](Documentation/python/replicant/actions/head_motion.md)
+- [LookAt](Documentation/python/replicant/actions/look_at.md)
+- [MoveBy](Documentation/python/replicant/actions/move_by.md)
+- [MoveTo](Documentation/python/replicant/actions/move_to.md)
+- [ReachFor](Documentation/python/replicant/actions/reach_for.md)
+- [ResetArm](Documentation/python/replicant/actions/reset_arm.md)
+- [ResetHead](Documentation/python/replicant/actions/reset_head.md)
+- [TurnBy](Documentation/python/replicant/actions/turn_by.md)
+- [TurnTo](Documentation/python/replicant/actions/turn_to.md)
+
 **tdw.robot_data**
 
 - [Drive](Documentation/python/robot_data/drive.md)


Test patch:
diff --git a/Python/example_controllers/semantic_states/composite_objects/Darwin/test_composite_object b/Python/example_controllers/composite_objects/composite_objects/Darwin/test_composite_object
similarity index 100%
rename from Python/example_controllers/semantic_states/composite_objects/Darwin/test_composite_object
rename to Python/example_controllers/composite_objects/composite_objects/Darwin/test_composite_object
diff --git a/Python/example_controllers/semantic_states/composite_objects/Linux/test_composite_object b/Python/example_controllers/composite_objects/composite_objects/Linux/test_composite_object
similarity index 100%
rename from Python/example_controllers/semantic_states/composite_objects/Linux/test_composite_object
rename to Python/example_controllers/composite_objects/composite_objects/Linux/test_composite_object
diff --git a/Python/example_controllers/semantic_states/composite_objects/Windows/test_composite_object b/Python/example_controllers/composite_objects/composite_objects/Windows/test_composite_object
similarity index 100%
rename from Python/example_controllers/semantic_states/composite_objects/Windows/test_composite_object
rename to Python/example_controllers/composite_objects/composite_objects/Windows/test_composite_object
diff --git a/Python/example_controllers/replicant/collision_detection_tests.py b/Python/example_controllers/replicant/collision_detection_tests.py
new file mode 100644
index 000000000..98a7d7d2d
--- /dev/null
+++ b/Python/example_controllers/replicant/collision_detection_tests.py
@@ -0,0 +1,57 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.replicant.action_status import ActionStatus
+
+
+class CollisionDetectionTests(Controller):
+    """
+    Test Replicant collision detection.
+    """
+
+    OBJECT_ID: int = 1
+
+    def test(self, status: ActionStatus, kinematic: bool = False, avoid: bool = True,
+             exclude: bool = False, distance: float = 4) -> None:
+        """
+        Test collision detection.
+
+        :param status: The expected action status at the end of the move_by(distance) action.
+        :param kinematic: If True, the object is kinematic.
+        :param avoid: If True, try to avoid obstacles and walls.
+        :param exclude: If True, ignore collisions with the object.
+        :param distance: The distance to move by.
+        """
+
+        self.add_ons.clear()
+        replicant = Replicant()
+        camera = ThirdPersonCamera(position={"x": 2, "y": 3, "z": 0.3},
+                                   look_at=replicant.replicant_id)
+        self.add_ons.extend([replicant, camera])
+        replicant.collision_detection.avoid = avoid
+        if exclude:
+            replicant.collision_detection.exclude_objects.append(CollisionDetectionTests.OBJECT_ID)
+        commands = [{"$type": "load_scene",
+                     "scene_name": "ProcGenScene"},
+                    TDWUtils.create_empty_room(12, 12)]
+        commands.extend(Controller.get_add_physics_object(model_name="trunck",
+                                                          object_id=CollisionDetectionTests.OBJECT_ID,
+                                                          position={"x": 0, "y": 0, "z": 3},
+                                                          kinematic=kinematic))
+        self.communicate(commands)
+        replicant.move_by(distance)
+        while replicant.action.status == ActionStatus.ongoing:
+            self.communicate([])
+        assert replicant.action.status == status, replicant.action.status
+
+
+if __name__ == "__main__":
+    c = CollisionDetectionTests()
+    c.test(status=ActionStatus.detected_obstacle)
+    c.test(avoid=False, status=ActionStatus.collision)
+    c.test(avoid=False, exclude=True, status=ActionStatus.success)
+    c.test(distance=-12, status=ActionStatus.detected_obstacle)
+    c.test(kinematic=True, status=ActionStatus.detected_obstacle)
+    c.test(kinematic=True, avoid=False, status=ActionStatus.collision)
+    c.communicate({"$type": "terminate"})
diff --git a/Python/example_controllers/replicant/physics_test.py b/Python/example_controllers/replicant/physics_test.py
new file mode 100644
index 000000000..a03d6fa20
--- /dev/null
+++ b/Python/example_controllers/replicant/physics_test.py
@@ -0,0 +1,113 @@
+from tdw.controller import Controller
+from tdw.tdw_utils import TDWUtils
+from tdw.add_ons.replicant import Replicant
+from tdw.add_ons.third_person_camera import ThirdPersonCamera
+from tdw.add_ons.image_capture import ImageCapture
+from tdw.replicant.action_status import ActionStatus
+from tdw.replicant.image_frequency import ImageFrequency
+from tdw.replicant.arm import Arm
+from tdw.backend.paths import EXAMPLE_CONTROLLER_OUTPUT_PATH
+
+
+class PhysicsTest(Controller):
+    """
+    Pick up a table and walk around.
+    """
+
+    def __init__(self, port: int = 1071, check_version: bool = True, launch_build: bool = True):
+        super().__init__(port=port, check_version=check_version, launch_build=launch_build)
+        # Set the Replicant in the constructor so that it can be referenced by multiple functions.
+        self.replicant = Replicant(position={"x": -5, "y": 0, "z": -5},
+                                   image_frequency=ImageFrequency.never)
+        # Likewise, we'll need to reference two object IDs, so we'll set them here.
+        # We'll set the other objects' IDs in `initialize_scene()` because we won't need to reference them again.
+        self.table_id = Controller.get_unique_id()
+        self.box_id = Controller.get_unique_id()
+        self.ball_id1 = Controller.get_unique_id()
+        self.ball_id2 = Controller.get_unique_id()
+
+    def initialize_scene(self):
+        # The camera and the image capture add-ons are instantiated here, rather than in the constructor, because we won't need to reference them again.
+        camera = ThirdPersonCamera(position={"x": 2, "y": 3, "z": 0.3},
+                                   look_at=self.replicant.replicant_id,
+                                   avatar_id="a")
+        path = EXAMPLE_CONTROLLER_OUTPUT_PATH.joinpath("replicant_physics_test")
+        print(f"Images will be saved to: {path}")
+        capture = ImageCapture(avatar_ids=[camera.avatar_id], path=path)
+        self.add_ons.extend([self.replicant, camera, capture])
+        # Create the room. Set a target framerate.
+        commands = [TDWUtils.create_empty_room(12, 20),
+                    {"$type": "set_target_framerate",
+                     "framerate": 60}]
+        # Add some objects.
+        commands.extend(Controller.get_add_physics_object(model_name="iron_box",
+                                                          object_id=self.box_id,
+                                                          position={"x": -1.575, "y": 0.35, "z": 2.25},
+                                                          rotation={"x": 0, "y": 0, "z": 0}))
+        commands.extend(Controller.get_add_physics_object(model_name="prim_sphere",
+                                                          object_id=self.ball_id1,
+                                                          position={"x": 3.5, "y": 0, "z": -3.5},
+                                                          rotation={"x": 0, "y": 0, "z": 0},
+                                                          scale_factor={"x": 0.2, "y": 0.2, "z": 0.2},
+                                                          kinematic=True,
+                                                          gravity=False,
+                                                          library="models_special.json"))
+        commands.extend(Controller.get_add_physics_object(model_name="prim_sphere",
+                                                          object_id=self.ball_id2,
+                                                          position={"x": -1, "y": 0, "z": -7.5},
+                                                          rotation={"x": 0, "y": 0, "z": 0},
+                                                          scale_factor={"x": 0.2, "y": 0.2, "z": 0.2},
+                                                          kinematic=True,
+                                                          gravity=False,
+                                                          library="models_special.json"))
+        commands.extend(Controller.get_add_physics_object(model_name="live_edge_coffee_table",
+                                                          object_id=self.table_id,
+                                                          position={"x": -1, "y": 0, "z": 2},
+                                                          rotation={"x": 0, "y": 20, "z": 0},
+                                                          library="models_core.json"))
+        commands.extend(Controller.get_add_physics_object(model_name="zenblocks",
+                                                          object_id=Controller.get_unique_id(),
+                                                          position={"x": -0.38, "y": 0.35, "z": 1.885},
+                                                          scale_factor={"x": 0.5, "y": 0.5, "z": 0.5},
+                                                          rotation={"x": 0, "y": 0, "z": 0}))
+        commands.extend(Controller.get_add_physics_object(model_name="basket_18inx18inx12iin_wicker",
+                                                          object_id=Controller.get_unique_id(),
+                                                          position={"x": -0.8, "y": 0.35, "z": 2},
+                                                          rotation={"x": 0, "y": 90, "z": 0}))
+        self.communicate(commands)
+
+    def do_action(self) -> None:
+        while self.replicant.action.status == ActionStatus.ongoing:
+            self.communicate([])
+
+    def run(self) -> None:
+        # Initialize the scene.
+        self.initialize_scene()
+        # Move to the table.
+        self.replicant.move_to(target=self.table_id)
+        self.do_action()
+        # Reach for the table.
+        self.replicant.reach_for(target=self.table_id, arm=Arm.left)
+        self.do_action()
+        # Grasp the table.
+        self.replicant.grasp(target=self.table_id, arm=Arm.left)
+        self.do_action()
+        # Move to the first ball.
+        self.replicant.move_to(target=self.ball_id1)
+        self.do_action()
+        # Drop the table.
+        self.replicant.drop(arm=Arm.left)
+        self.do_action()
+        # Reset the arm.
+        self.replicant.reset_arm(arm=Arm.left)
+        self.do_action()
+        # Walk to the other ball.
+        self.replicant.move_to(target=self.ball_id2)
+        self.do_action()
+        # End the simulation.
+        self.communicate({"$type": "terminate"})
+
+
+if __name__ == "__main__":
+    c = PhysicsTest()
+    c.run()