defmodule Ecspanse.Command do @moduledoc """ The `Ecspanse.Command` module provides a set of functions for managing entities, components and resources in the `Ecspanse` engine. Commands are the only way to change the state of components and resources in `Ecspanse`. These commands can only be run from systems, otherwise an error will be thrown. The `Ecspanse.Command` module includes functions for managing relationships between entities, such as adding and removing children and parents. All entity and component related commands can run for batches (lists) for performance reasons. All commands raise an error if the command fails. #### Entity Relationships The `Ecspanse.Command` module provides functions for managing relationships between entities. This is aslo a powerful tool to manage different kind of collections. > #### Ecspanse entities relationships are **bidirectional associations** {: .info} > When adding or removing children or parents, they are automatically added or removed from the corresponding parent or children entities. > The same applies when despawning entities. """ require Logger require Ex2ms alias __MODULE__ alias Ecspanse.Component alias Ecspanse.Entity alias Ecspanse.Event alias Ecspanse.Query alias Ecspanse.Resource alias Ecspanse.Util defmodule Operation do @moduledoc false @type t :: %__MODULE__{ name: name(), system: module(), entities_components: list(%{(entity_id :: binary()) => list(component_module :: module())}), system_execution: atom(), locked_components: list() } @type name :: :run | :spawn_entities | :despawn_entities | :despawn_entities_and_descendants | :add_components | :remove_components | :update_components | :add_children | :remove_children | :add_parents | :remove_parents | :insert_resource | :update_resource | :delete_resource defstruct name: nil, system: nil, entities_components: %{}, system_execution: nil, locked_components: [] end defmodule Error do @moduledoc false defexception [:message] @impl true def exception({%Ecspanse.Command.Operation{} = operation, message}) do msg = """ System: #{inspect(operation.system)} Operation: #{inspect(operation.name)} Message: #{message} """ %Error{message: msg} end end @typedoc false @type t :: %Command{ return_result: any(), insert_components: list(component :: struct()), update_components: list(component :: struct()), delete_components: list(component :: struct()) } defstruct return_result: nil, insert_components: [], update_components: [], delete_components: [] @doc """ Spawns a new entity with the given components and relations provided by the Ecspanse.Entity.entity_spec() type. When creating a new entity, at least one of the `components:`, `children:` or `parents:` must be provided in the entity spec, otherwise the entity cannot be persisted. Due to the potentially large number of components that may be affected by this operation, it is recommended to run this function in a synchronous system (such as a `frame_start` or `frame_end` system) to avoid the need to lock all involved components. ## Examples ```elixir %Ecspanse.Entity{} = Ecspanse.Command.spawn_entity!( { Ecspanse.Entity, id: "my_custom_id", components: [Demo.Components.Hero, {Demo.Components.Position, [x: 5, y: 3], [:hero, :map]}], children: [potion_entity, sword_entity], parents: [map_entity] } ) ``` """ @doc group: :entities @spec spawn_entity!(Entity.entity_spec()) :: Entity.t() def spawn_entity!(spec) do [entity] = spawn_entities!([spec]) entity end @doc """ The same as `spawn_entity!/1` but spawns multiple entities at once. It takes a list of entity specs as argument and returns a list of Ecspanse.Entity structs. See `spawn_entity!/1` for more details. """ @doc group: :entities @spec spawn_entities!(list(Entity.entity_spec())) :: list(Entity.t()) def spawn_entities!([]), do: [] def spawn_entities!(list) do operation = build_operation(:spawn_entities) command = apply_operation(operation, %Command{}, list) commit(command) command.return_result end @doc """ Despawns the specified entity and removes all of its components. It also removes the despawned entity from its parent and child entities, if any. Due to the potentially large number of components that may be affected by this operation, it is recommended to run this function in a synchronous system (such as a `frame_start` or `frame_end` system) to avoid the need to lock all involved components. ## Examples ```elixir :ok = Ecspanse.Command.despawn_entity!(hero_entity) ``` """ @doc group: :entities @spec despawn_entity!(Entity.t()) :: :ok def despawn_entity!(entity) do despawn_entities!([entity]) end @doc """ The same as `despawn_entity!/1` but despawns multiple entities at once. It takes a list of entities as argument and returns `:ok`. See `despawn_entity!/1` for more details. """ @doc group: :entities @spec despawn_entities!(list(Entity.t())) :: :ok def despawn_entities!([]), do: :ok def despawn_entities!(list) do operation = build_operation(:despawn_entities) command = apply_operation(operation, %Command{}, list) commit(command) command.return_result end @doc """ The same as `despawn_entity!/1` but recursively despawns also all descendant tree of the entity. This means that it will despawn the children of the entity, and their children, and so on. It is an efficient way to remove an entire entity tree with just one operation. Extra attention required for entities with shared children. See `despawn_entity!/1` for more details. """ @doc group: :entities @spec despawn_entity_and_descendants!(Entity.t()) :: :ok def despawn_entity_and_descendants!(entity) do despawn_entities_and_descendants!([entity]) end @doc """ The same as `despawn_entity_and_descendants!/1` but despawns multiple entities and their descendants at once. It takes a list of entities as argument and returns `:ok`. """ @doc group: :entities @spec despawn_entities_and_descendants!(list(Entity.t())) :: :ok def despawn_entities_and_descendants!([]), do: :ok def despawn_entities_and_descendants!(entities_list) do descendants_list = entities_descendants(entities_list) (entities_list ++ descendants_list) |> List.flatten() |> Enum.uniq() |> despawn_entities!() end @doc """ Adds a new component to the specified entity. > #### Info {: .info} > An entity cannot have multiple components of the same type. > If an attempt is made to insert a component that already exists for the entity, an error will be raised. ## Examples ```elixir :ok = Ecspanse.Command.add_component!(hero_entity, Demo.Components.Gold) :ok = Ecspanse.Command.add_component!(hero_entity, {Demo.Components.Gold, [amount: 5], [:resource, :available]}) ``` """ @doc group: :components @spec add_component!(Entity.t(), Component.component_spec()) :: :ok def add_component!(entity, component_spec) do add_components!([{entity, [component_spec]}]) end @doc """ The same as `add_component!/2` but adds multiple components to moultiple entities at once. It takes a list of two element tuples as argument, where the first element of the tuple is the entity and the second element is a list of component specs. ## Examples ```elixir :ok = Ecspanse.Command.add_components!([ {inventory_item_entity, [Demo.Components.Sword]}, {hero_entity, [Demo.Components.Position, Demo.Components.Hero]} ]) ``` """ @doc group: :components @spec add_components!(list({Entity.t(), list(Component.component_spec())})) :: :ok def add_components!([]), do: :ok def add_components!(list) do operation = build_operation(:add_components) command = apply_operation(operation, %Command{}, list) commit(command) command.return_result end @doc """ Updates the state of an existing component. The function takes two arguments: the component struct to update and a keyword list of changes to apply. ## Examples ```elixir :ok = Ecspanse.Command.update_component!(position_component, x: :12) ``` """ @doc group: :components @spec update_component!(current_component :: struct(), state_changes :: keyword()) :: :ok def update_component!(component, changes_keyword) do update_components!([{component, changes_keyword}]) end @doc """ The same as `update_component!/2` but updates multiple components at once. It takes a list of two element tuples as argument, where the first element of the tuple is the component struct and the second element is a keyword list of changes to apply. ## Examples ```elixir :ok = Ecspanse.Command.update_components!([ {position_component, x: 7, y: 9}, {gold_component, amount: 12} ]) ``` """ @doc group: :components @spec update_components!(list({current_component :: struct(), state_changes :: keyword()})) :: :ok def update_components!([]), do: :ok def update_components!(list) do operation = build_operation(:update_components) command = apply_operation(operation, %Command{}, list) commit(command) command.return_result end @doc """ Removes an existing component from its entity. The components is destroyed. ## Examples ```elixir :ok = Ecspanse.Command.remove_component!(invisibility_component) ``` """ @doc group: :components @spec remove_component!(component :: struct()) :: :ok def remove_component!(component) do remove_components!([component]) end @doc """ The same as `remove_component!/1` but removes multiple components at once. """ @doc group: :components @spec remove_components!(list(component :: struct())) :: :ok def remove_components!([]), do: :ok def remove_components!(components) do operation = build_operation(:remove_components) command = apply_operation(operation, %Command{}, components) commit(command) command.return_result end @doc """ Adds an entity as child to a parent entity. ## Examples ```elixir :ok = Ecspanse.Command.add_child!(hero_entity, sword_entity) ``` """ @doc group: :relationships @spec add_child!(Entity.t(), child :: Entity.t()) :: :ok def add_child!(entity, child) do add_children!([{entity, [child]}]) end @doc """ The same as `add_child!/2` but can perform multiple operations at once. For example, adding multiple children to multiple parents. It takes a list of two element tuples as argument, where the first element of the tuple is the parent entity and the second element is a list of children entities. ## Examples ```elixir :ok = Ecspanse.Command.add_children!([ {hero_entity, [sword_entity]}, {market_entity, [map_entity, potion_entity]} ]) ``` """ @doc group: :relationships @spec add_children!(list({Entity.t(), children :: list(Entity.t())})) :: :ok def add_children!([]), do: :ok def add_children!(list) do operation = build_operation(:add_children) command = apply_operation(operation, %Command{}, list) commit(command) command.return_result end @doc """ Adds a parent entity to a child entity. ## Examples ```elixir :ok = Ecspanse.Command.add_parent!(sowrd_entity, hero_entity) ``` """ @doc group: :relationships @spec add_parent!(Entity.t(), parent :: Entity.t()) :: :ok def add_parent!(entity, parent) do add_parents!([{entity, [parent]}]) end @doc """ The same as `add_parent!/2` but can perform multiple operations at once. For example, adding multiple parents to multiple children. It takes a list of two element tuples as argument, where the first element of the tuple is the child entity and the second element is a list of parent entities. ## Examples ```elixir :ok = Ecspanse.Command.add_parents!([ {sword_entity, [hero_entity]}, {map_entity, [market_entity, vendor_entity]} ]) ``` """ @doc group: :relationships @spec add_parents!(list({Entity.t(), parents :: list(Entity.t())})) :: :ok def add_parents!([]), do: :ok def add_parents!(list) do operation = build_operation(:add_parents) command = apply_operation(operation, %Command{}, list) commit(command) command.return_result end @doc """ Removes a child entity from a parent entity. ## Examples ```elixir :ok = Ecspanse.Command.remove_child!(hero_entity, sword_entity) ``` """ @doc group: :relationships @spec remove_child!(Entity.t(), child :: Entity.t()) :: :ok def remove_child!(entity, child) do remove_children!([{entity, [child]}]) end @doc """ The same as `remove_child!/2` but can perform multiple operations at once. For example, removing multiple children from multiple parents. It takes a list of two element tuples as argument, where the first element of the tuple is the parent entity and the second element is a list of children entities. ## Examples ```elixir :ok = Ecspanse.Command.remove_children!([ {hero_entity, [sword_entity]}, {market_entity, [map_entity, potion_entity]} ]) ``` """ @doc group: :relationships @spec remove_children!(list({Entity.t(), children :: list(Entity.t())})) :: :ok def remove_children!([]), do: :ok def remove_children!(list) do operation = build_operation(:remove_children) command = apply_operation(operation, %Command{}, list) commit(command) command.return_result end @doc """ Removes a parent entity from a child entity. ## Examples ```elixir :ok = Ecspanse.Command.remove_parent!(sword_entity, hero_entity) ``` """ @doc group: :relationships @spec remove_parent!(Entity.t(), parent :: Entity.t()) :: :ok def remove_parent!(entity, parent) do remove_parents!([{entity, [parent]}]) end @doc """ The same as `remove_parent!/2` but can perform multiple operations at once. For example, removing multiple parents from multiple children. It takes a list of two element tuples as argument, where the first element of the tuple is the child entity and the second element is a list of parent entities. ## Examples ```elixir :ok = Ecspanse.Command.remove_parents!([ {sword_entity, [hero_entity]}, {map_entity, [market_entity, vendor_entity]} ]) ``` """ @doc group: :relationships @spec remove_parents!(list({Entity.t(), parents :: list(Entity.t())})) :: :ok def remove_parents!([]), do: :ok def remove_parents!(list) do operation = build_operation(:remove_parents) command = apply_operation(operation, %Command{}, list) commit(command) command.return_result end @doc """ Inserts a new global resource. > #### Info {: .info} > An Ecspanse instance can only hold one resource of each type at a time. > If an attempt is made to insert a resource that already exists, an error will be raised. ## Examples ```elixir :ok = Ecspanse.Command.insert_resource!({Demo.Resources.Lobby, player_count: 0}) ``` """ @doc group: :resources @spec insert_resource!(resource_spec :: Resource.resource_spec()) :: resource :: struct() def insert_resource!(resource_spec) do operation = build_operation(:insert_resource) :ok = validate_payload(operation, resource_spec) command = apply_operation(operation, %Command{}, resource_spec) command.return_result end @doc """ Updates an existing global resource. ## Examples ```elixir :ok = Ecspanse.Command.update_resource!(lobby_resource, player_count: 1) ``` """ @doc group: :resources @spec update_resource!(resource :: struct(), state_changes :: keyword()) :: updated_resource :: struct() def update_resource!(resource, state_changes) do operation = build_operation(:update_resource) :ok = validate_payload(operation, {resource, state_changes}) command = apply_operation(operation, %Command{}, {resource, state_changes}) command.return_result end @doc """ Deletes an existing global resource. ## Examples ```elixir :ok = Ecspanse.Command.delete_resource!(lobby_resource) ``` """ @doc group: :resources @spec delete_resource!(resource :: struct()) :: deleted_resource :: struct() def delete_resource!(resource) do operation = build_operation(:delete_resource) :ok = validate_payload(operation, resource) command = apply_operation(operation, %Command{}, resource) command.return_result end ######## defp build_operation(operation_name) do unless Process.get(:ecs_process_type) == :system do raise "Commands can only be executed from a System." end # Find the entities_components only once per Command and store it in the Operation entities_components = Util.list_entities_components() %Operation{ name: operation_name, system: Process.get(:system_module), entities_components: entities_components, system_execution: Process.get(:system_execution), locked_components: Process.get(:locked_components) } end # resource payload validation defp validate_payload(%Operation{name: :insert_resource}, resource_module) when is_atom(resource_module), do: :ok defp validate_payload(%Operation{name: :insert_resource}, {resource_module, state}) when is_atom(resource_module) and is_list(state), do: :ok defp validate_payload(%Operation{name: :insert_resource} = operation, value), do: raise( Error, {operation, "Expected type `Ecspanse.Resource.resource_spec()` , got: `#{inspect(value)}`"} ) defp validate_payload(%Operation{name: :update_resource}, {resource, state_changes}) when is_struct(resource) and is_list(state_changes), do: :ok defp validate_payload(%Operation{name: :update_resource} = operation, value), do: raise( Error, {operation, "Expected a resource state `struct()` and `keyword()` type args, got: `#{inspect(value)}`"} ) defp validate_payload(%Operation{name: :delete_resource}, resource) when is_struct(resource), do: :ok # Create, Update, Delete components # recieves [{Entity, opts}] defp apply_operation( %Operation{name: :spawn_entities} = operation, command, entity_spec_list ) do entity_spec_list = Enum.map(entity_spec_list, fn {_, opts} -> entity_id = Keyword.get(opts, :id, UUID.uuid4()) component_specs = Keyword.get(opts, :components, []) component_modules = Enum.map(component_specs, fn module when is_atom(module) -> module {module, _} when is_atom(module) -> module {module, _, _} when is_atom(module) -> module end) children_entities = Keyword.get(opts, :children, []) parents_entities = Keyword.get(opts, :parents, []) :ok = validate_required_opts(operation, component_specs, children_entities, parents_entities) %{ entity: Util.build_entity(entity_id), component_specs: component_specs, component_modules: component_modules ++ [Component.Children, Component.Parents], children_entities: children_entities, parents_entities: parents_entities } end) v1 = Task.async(fn -> entity_ids = Enum.map(entity_spec_list, fn %{entity: entity} -> entity.id end) :ok = validate_binary_entity_names(operation, entity_ids) :ok = validate_unique_entity_names(operation, entity_ids) end) v2 = Task.async(fn -> component_specs = Enum.map(entity_spec_list, fn %{component_specs: component_specs} -> component_specs end) |> List.flatten() :ok = validate_no_relation(operation, component_specs) end) v3 = Task.async(fn -> relation_entities = Enum.map(entity_spec_list, fn %{children_entities: children, parents_entities: parents} -> children ++ parents end) |> List.flatten() :ok = validate_entities(operation, relation_entities) :ok = validate_entities_exist(operation, relation_entities) end) custom_components = Task.async(fn -> for %{ entity: entity, component_specs: component_specs } <- entity_spec_list do # Force the creation of the children and parents components on entity creation component_specs = component_specs ++ [Component.Children, Component.Parents] upsert_components(operation, entity, component_specs, []) end end) children_and_parents_components = Task.async(fn -> for %{ entity: entity, children_entities: children_entities } <- entity_spec_list do create_children_and_parents( operation, entity, children_entities ) end end) parents_and_children_components = Task.async(fn -> for %{ entity: entity, parents_entities: parents_entities } <- entity_spec_list do create_parents_and_children( operation, entity, parents_entities ) end end) Task.await_many([v1, v2, v3]) components = Task.await(custom_components) ungrouped_relations = List.flatten( Task.await(children_and_parents_components) ++ Task.await(parents_and_children_components) ) relations = group_added_relations(operation, ungrouped_relations) # Relation components are always updated, not inserted %{ command | return_result: Enum.map(entity_spec_list, fn %{entity: entity} -> entity end), insert_components: List.flatten(components), update_components: relations } end defp apply_operation( %Operation{name: :despawn_entities} = operation, command, entities ) do # Entity relations (children, parents) need to be handled before removing the entity's components :ok = validate_entities(operation, entities) table = Util.components_state_ets_table() children_and_parents_components = Task.async(fn -> for entity <- entities do # it is possible that the children component was already removed case :ets.lookup(table, {entity.id, Component.Children}) do [{{_id, Component.Children}, _, %Component.Children{entities: children_entities}}] -> remove_children_and_parents(operation, entity, children_entities) _ -> [] end end end) parents_and_children_components = Task.async(fn -> for entity <- entities do case :ets.lookup(table, {entity.id, Component.Parents}) do [{{_id, Component.Parents}, _, %Component.Parents{entities: parents_entities}}] -> remove_parents_and_children(operation, entity, parents_entities) _ -> [] end end end) deleted_components = for %Entity{id: id} <- entities do f = Ex2ms.fun do {{entity_id, _component_module}, _component_tags, component_state} when entity_id == ^id -> component_state end deleted_components_state = :ets.select(table, f) delete_components(operation, deleted_components_state) end # It is possible that more relations for the same entity are updated in the same command. # If there are more, they need to be grouped, leaving only the relations that are not deleted ungrouped_relations = List.flatten( Task.await(children_and_parents_components) ++ Task.await(parents_and_children_components) ) relations = group_removed_relations(operation, ungrouped_relations) %{ command | return_result: :ok, update_components: relations, delete_components: List.flatten(deleted_components) } end # receives a list of {%Entity{} = entity, component_specs} defp apply_operation( %Operation{name: :add_components} = operation, command, list ) do entities = Enum.map(list, fn {entity, _component_specs} -> entity end) v1 = Task.async(fn -> :ok = validate_entities(operation, entities) :ok = validate_entities_exist(operation, entities) end) v2 = Task.async(fn -> Enum.each(list, fn {entity, component_specs} -> :ok = validate_components_do_not_exist(operation, entity, component_specs) end) end) v3 = Task.async(fn -> component_specs = Enum.map(list, fn {_entity, component_specs} -> component_specs end) |> List.flatten() :ok = validate_no_relation(operation, component_specs) end) components = for {entity, component_specs} <- list do upsert_components(operation, entity, component_specs, []) end Task.await_many([v1, v2, v3]) %{ command | return_result: :ok, insert_components: List.flatten(components) } end # Receives a list of updates: [ {%Component{}, state_changes :: keyword()}] defp apply_operation( %Operation{name: :update_components} = operation, command, updates ) do v1 = Task.async(fn -> component_modules = Enum.map(updates, fn {component, _state_changes} -> component.__meta__.module end) :ok = validate_no_relation(operation, component_modules) end) v2 = Task.async(fn -> Enum.each(updates, fn {component, _state_changes} -> :ok = validate_is_component(operation, component.__meta__.module) :ok = validate_component_exists(operation, component) end) end) v3 = Task.async(fn -> Enum.each(updates, fn {component, _state_changes} -> :ok = validate_locked_component( operation, operation.system_execution, component.__meta__.module ) end) end) components = for {component, state_changes} <- updates do state_changes = Keyword.delete(state_changes, :__meta__) new_component_state = struct(component, state_changes) :ok = validate_component_state(operation, new_component_state) { {component.__meta__.entity.id, component.__meta__.module}, component.__meta__.tags, new_component_state } end Task.await_many([v1, v2, v3]) %{ command | return_result: :ok, update_components: components } end defp apply_operation( %Operation{name: :remove_components} = operation, command, components_state ) do :ok = validate_no_relation(operation, Enum.map(components_state, & &1.__meta__.module)) deleted_components = delete_components(operation, components_state) %{ command | return_result: :ok, delete_components: deleted_components } end # receives a list of [{%Entity{}, [%ChildrenEntity{}]}] defp apply_operation( %Operation{name: :add_children} = operation, command, list ) do v1 = Task.async(fn -> entities = Enum.map(list, fn {entity, children_entities} -> [entity | children_entities] end) |> List.flatten() :ok = validate_entities(operation, entities) :ok = validate_entities_exist(operation, entities) end) children_and_parents_components = for {entity, children_entities} <- list do create_children_and_parents( operation, entity, children_entities ) end Task.await(v1) relations = group_added_relations(operation, List.flatten(children_and_parents_components)) %{ command | return_result: :ok, update_components: relations } end # receives a list of [{%Entity{}, [%ParentEntity{}]}] defp apply_operation( %Operation{name: :add_parents} = operation, command, list ) do v1 = Task.async(fn -> entities = Enum.map(list, fn {entity, parents_entities} -> [entity | parents_entities] end) |> List.flatten() :ok = validate_entities(operation, entities) :ok = validate_entities_exist(operation, entities) end) parents_and_children_components = for {entity, parents_entities} <- list do create_parents_and_children( operation, entity, parents_entities ) end Task.await(v1) relations = group_added_relations(operation, List.flatten(parents_and_children_components)) %{ command | return_result: :ok, update_components: relations } end # receives a list of [{%Entity{}, [%ChildrenEntity{}]}] defp apply_operation( %Operation{name: :remove_children} = operation, command, list ) do v1 = Task.async(fn -> entities = Enum.map(list, fn {entity, children_entities} -> [entity | children_entities] end) |> List.flatten() :ok = validate_entities(operation, entities) end) v2 = Task.async(fn -> entities = Enum.map(list, fn {entity, _children_entities} -> entity end) |> List.flatten() # not checking if the children entities exist because they might have been deleted :ok = validate_entities_exist(operation, entities) end) children_and_parents_components = for {entity, children_entities} <- list do remove_children_and_parents( operation, entity, children_entities ) end Task.await_many([v1, v2]) relations = group_removed_relations(operation, List.flatten(children_and_parents_components)) %{ command | return_result: :ok, update_components: relations } end # receives a list of [{%Entity{}, [%ParentEntity{}]}] defp apply_operation( %Operation{name: :remove_parents} = operation, command, list ) do v1 = Task.async(fn -> entities = Enum.map(list, fn {entity, parents_entities} -> [entity | parents_entities] end) |> List.flatten() :ok = validate_entities(operation, entities) end) v2 = Task.async(fn -> entities = Enum.map(list, fn {entity, _parents_entities} -> entity end) |> List.flatten() # not checking if the parents entities exist because they might have been deleted :ok = validate_entities_exist(operation, entities) end) parents_and_children_components = for {entity, parents_entities} <- list do remove_parents_and_children( operation, entity, parents_entities ) end Task.await_many([v1, v2]) relations = group_removed_relations(operation, List.flatten(parents_and_children_components)) %{ command | return_result: :ok, update_components: relations } end defp apply_operation(%Operation{name: :insert_resource} = operation, command, resource_spec) do :ok = validate_resource_does_not_exist(operation, resource_spec) resource_state = upsert_resource(operation, resource_spec) resource_created_event(resource_state) %{ command | return_result: resource_state } end defp apply_operation( %Operation{name: :update_resource} = operation, command, {resource_state, state_changes} ) do resource_module = resource_state.__meta__.module :ok = validate_resource_exists(operation, resource_state) state_changes = Keyword.delete(state_changes, :__meta__) state = resource_state |> Map.from_struct() |> Map.to_list() |> Keyword.merge(state_changes) resource_state = upsert_resource(operation, {resource_module, state}) resource_updated_event(resource_state) %{ command | return_result: resource_state } end defp apply_operation(%Operation{name: :delete_resource} = operation, command, resource_state) do resource_module = resource_state.__meta__.module :ok = validate_locked_resource( operation, operation.system_execution, resource_module ) table = Util.resources_state_ets_table() :ets.delete(table, resource_module) resource_deleted_event(resource_state) %{ command | return_result: resource_state } end defp upsert_resource(operation, resource_module) when is_atom(resource_module) do upsert_resource(operation, {resource_module, []}) end # composes the resource and inserts it into the ets table # the flow is different than the components, as the resources are managed one at a time defp upsert_resource(operation, {resource_module, state}) when is_atom(resource_module) and is_list(state) do :ok = validate_is_resource(operation, resource_module) :ok = validate_locked_resource( operation, operation.system_execution, resource_module ) resource_meta = struct!(Resource.Meta, %{ module: resource_module }) resource_state = struct!(resource_module, Keyword.put(state, :__meta__, resource_meta)) :ok = validate_resource_state(operation, resource_state) # this is stored in the ETS table resource_with_key = {resource_module, resource_state} table = Util.resources_state_ets_table() :ets.insert(table, resource_with_key) resource_state end defp upsert_components(_operation, _entity, [], components), do: components defp upsert_components( operation, entity, [component_spec | component_specs], components ) do component = upsert_component(operation, entity, component_spec) upsert_components(operation, entity, component_specs, [component | components]) end # Used also for children and parents. Validating children and parents should be done before this defp upsert_component(operation, entity, component_module) when is_atom(component_module) do upsert_component(operation, entity, {component_module, [], []}) end defp upsert_component(operation, entity, {component_module, state}) do upsert_component(operation, entity, {component_module, state, []}) end defp upsert_component(operation, entity, {component_module, state, tags}) when is_atom(component_module) and is_list(state) do :ok = validate_is_component(operation, component_module) :ok = validate_tags(operation, tags) # VALIDATE THE COMPOENENT IS LOCKED FOR CREATION :ok = validate_locked_component( operation, operation.system_execution, component_module ) # merging tags from compile time with tags from component spec tags = Enum.uniq(component_module.__component_tags__() ++ tags) component_meta = struct!(Component.Meta, %{ entity: entity, module: component_module, tags: tags }) component_state = struct!(component_module, Keyword.put(state, :__meta__, component_meta)) :ok = validate_component_state(operation, component_state) # this is stored in the ETS table {{entity.id, component_module}, tags, component_state} end # there is no guarantee that the components belong to the same entity # returns a list of {{entity_id, component_module, tags}, component_state} defp delete_components(operation, components_state) do Enum.map(components_state, fn component_state -> entity = component_state.__meta__.entity component_module = component_state.__meta__.module component_tags = component_state.__meta__.tags :ok = validate_locked_component( operation, operation.system_execution, component_module ) {{entity.id, component_module}, component_tags, component_state} end) end # Adds to, or creates the Entity's children and adds to or create the children's parents components defp create_children_and_parents(operation, entity, []) do # Create empty children component for entity empty_entity_children = upsert_children_for(operation, entity, []) [empty_entity_children] end defp create_children_and_parents(operation, entity, children) when is_list(children) do entity_children = upsert_children_for(operation, entity, children) entities_parents = Enum.map(children, fn child_entity -> upsert_parents_for(operation, child_entity, [entity]) end) [entity_children | entities_parents] end # Adds to, or creates the Entity's parents and adds to or create the parent's children components defp create_parents_and_children(operation, entity, []) do # Create empty parents component for entity empty_entity_parents = upsert_parents_for(operation, entity, []) [empty_entity_parents] end defp create_parents_and_children(operation, entity, parents) when is_list(parents) do entity_parents = upsert_parents_for(operation, entity, parents) entities_children = Enum.map(parents, fn parent_entity -> upsert_children_for(operation, parent_entity, [ entity ]) end) [entity_parents | entities_children] end # Returns Children component with its key # {{entity_id, Component.Children, []}, %Component.Children{entities: [entity_3, entity_2, entity_1]}} # it is calling upsert_component which will validate the component is locked for creation defp upsert_children_for(operation, entity, children) do table = Util.components_state_ets_table() case :ets.lookup(table, {entity.id, Component.Children}) do [{_key, _tags, %Component.Children{entities: existing_children}}] -> children = Enum.concat(existing_children, children) |> Enum.uniq() upsert_component( operation, entity, {Component.Children, [entities: children]} ) [] -> upsert_component( operation, entity, {Component.Children, [entities: children]} ) end end # Returns a Parents component with its key # {{entity_id, Component.Parents, []}, %Component.Parents{entities: [entity_3, entity_2, entity_1]}} # it is calling upsert_component which will validate the component is locked for creation defp upsert_parents_for(operation, entity, parents) do table = Util.components_state_ets_table() case :ets.lookup(table, {entity.id, Component.Parents}) do [{_key, _tags, %Component.Parents{entities: existing_parents}}] -> parents = Enum.concat(existing_parents, parents) |> Enum.uniq() upsert_component(operation, entity, {Component.Parents, [entities: parents]}) [] -> upsert_component(operation, entity, {Component.Parents, [entities: parents]}) end end # Mark for update: Remove children entities from target Entity # and parent entity from their parents defp remove_children_and_parents(_operation, _entity, []), do: [] defp remove_children_and_parents(operation, entity, children) when is_list(children) do table = Util.components_state_ets_table() entity_children = case :ets.lookup(table, {entity.id, Component.Children}) do [{_key, _tags, %Component.Children{entities: existing_children}}] -> upsert_component( operation, entity, {Component.Children, [entities: existing_children -- children]} ) [] -> nil end entities_parents = Enum.map(children, fn child_entity -> case :ets.lookup(table, {child_entity.id, Component.Parents}) do [{_key, _tags, %Component.Parents{entities: existing_parents}}] -> upsert_component( operation, child_entity, {Component.Parents, [entities: existing_parents -- [entity]]} ) [] -> nil end end) [entity_children | entities_parents] |> Enum.reject(&is_nil/1) end # Mark for update: Remove parent entities from target Entity # and children entity from their children defp remove_parents_and_children(_operation, _entity, []), do: [] defp remove_parents_and_children(operation, entity, parents) do table = Util.components_state_ets_table() entity_parents = case :ets.lookup(table, {entity.id, Component.Parents}) do [{_key, _tags, %Component.Parents{entities: existing_parents}}] -> upsert_component( operation, entity, {Component.Parents, [entities: existing_parents -- parents]} ) [] -> nil end entities_children = Enum.map(parents, fn parent_entity -> case :ets.lookup(table, {parent_entity.id, Component.Children}) do [{_key, _tags, %Component.Children{entities: existing_children}}] -> upsert_component( operation, parent_entity, {Component.Children, [entities: existing_children -- [entity]]} ) [] -> nil end end) [entity_parents | entities_children] |> Enum.reject(&is_nil/1) end # Grouping relations # It is possible that more relations for the same entity are updated in the same command. # If there are more, they need to be grouped defp group_added_relations(operation, relations) do relations |> Enum.group_by(fn {k, _tags, _v} -> k end, fn {_k, _tags, v} -> v end) |> Enum.map(fn {k, [v]} -> {k, [], v} {{entity_id, module}, values} -> list = Enum.map(values, fn value -> value.entities end) |> List.flatten() |> Enum.uniq() entity = Util.build_entity(entity_id) upsert_component(operation, entity, {module, entities: list}) end) end defp group_removed_relations(operation, relations) do relations |> Enum.group_by(fn {k, _tags, _v} -> k end, fn {_k, _tags, v} -> v end) |> Enum.map(fn {k, [v]} -> {k, [], v} {{entity_id, module}, values} -> list = Enum.map(values, fn value -> value.entities end) |> List.flatten() |> Enum.uniq() |> select_entities_present_in_all_relations(values) entity = Util.build_entity(entity_id) upsert_component(operation, entity, {module, entities: list}) end) end defp select_entities_present_in_all_relations(entity_list, relations) do Enum.filter( entity_list, fn entity -> Enum.all?( relations, fn r -> entity in r.entities end ) end ) end # Component CRUD Validations defp validate_required_opts(operation, [], [], []) do raise Error, {operation, "Expected at least one of the following options in the entity_spec when creating an entity: `components`, `children`, `parents`"} end defp validate_required_opts(_operation, _, _, _), do: :ok defp validate_entities(operation, entities) do non_enitites = Enum.reject(entities, &match?(%Entity{}, &1)) case non_enitites do [] -> :ok _ -> raise Error, {operation, "Expected a list of `Ecspanse.Entity.t()` types, got: `#{Kernel.inspect(non_enitites)}`"} end end defp validate_binary_entity_names(operation, entity_ids) do Enum.each(entity_ids, fn entity_id -> case is_binary(entity_id) do true -> :ok false -> raise Error, {operation, "Entity id `#{entity_id}` must be a binary. Entity ids must be unique."} end end) :ok end defp validate_unique_entity_names(operation, entity_ids) do table = Util.components_state_ets_table() Enum.each(entity_ids, fn entity_id -> f = Ex2ms.fun do {{^entity_id, _component_module}, _component_tags, _component_state} -> ^entity_id end result = :ets.select(table, f, 1) case result do {[], _} -> :ok {nil, _} -> :ok :"$end_of_table" -> :ok _ -> raise Error, {operation, "Entity id `#{entity_id}` already exists. Entity ids must be unique."} end end) :ok end defp validate_entities_exist(_operation, []), do: :ok defp validate_entities_exist(operation, entities) do table = Util.components_state_ets_table() entity_ids = Enum.map(entities, & &1.id) # All this, just because `when entity_id in ^entity_ids` doesn't work :( result = entity_ids |> Stream.map(fn target_entity_id -> f = Ex2ms.fun do {{entity_id, _component_module}, _tags, _component_state} when entity_id == ^target_entity_id -> entity_id end # limit to 1. We don't care about the result, just if it exists :ets.select(table, f, 1) end) |> Stream.map(fn {id_list, _} -> id_list :"$end_of_table" -> [] end) |> Enum.concat() case entity_ids -- result do [] -> :ok {nil, _} -> :ok :"$end_of_table" -> :ok missing_entity_ids -> raise Error, {operation, "Entity ids `#{inspect(missing_entity_ids)}` do not exist."} end end defp validate_is_component(operation, component_module) do Util.validate_ecs_type( component_module, :component, Error, {operation, "#{inspect(component_module)} is not a Component"} ) end defp validate_no_relation(operation, component_specs) do component_modules = Enum.map(component_specs, fn {component_module, _, _} when is_atom(component_module) -> component_module {component_module, _} when is_atom(component_module) -> component_module component_module when is_atom(component_module) -> component_module end) if component_modules -- [Component.Children, Component.Parent] == component_modules do :ok else raise Error, {operation, "Children or Parent relation not expected. Use the the dedicated `children` and `parents` options when creating a new entity. Or use the dedicated `add_child/3` and `add_parent/3` commands."} end end defp validate_component_state(operation, component_state_struct) do if function_exported?(component_state_struct.__meta__.module, :validate, 1) do case component_state_struct.__meta__.module.validate(component_state_struct) do :ok -> :ok {:error, error} -> raise Error, {operation, "#{inspect(component_state_struct)} state is invalid. Error: #{inspect(error)}"} end else :ok end end # There is no lock validation for sync systems defp validate_locked_component(operation, :sync, component_module) do unless Enum.empty?(operation.locked_components) do Logger.warning( "#{inspect(operation)}. Component: #{inspect(component_module)}. There is no need to lock components in Systems that execute synchronously. The values are ignored" ) end :ok end defp validate_locked_component(operation, :async, component_module) do locked_components = operation.locked_components if component_module in locked_components do :ok else raise Error, {operation, "#{inspect(component_module)} is not locked. It can not be created or updated in an async System"} end end defp validate_components_do_not_exist(operation, %Entity{id: entity_id}, component_specs) do entities_components = operation.entities_components case entities_components[entity_id] do nil -> :ok [] -> :ok existing_components when is_list(existing_components) -> component_modules = Enum.map(component_specs, fn {component_module, _, _} when is_atom(component_module) -> component_module {component_module, _} when is_atom(component_module) -> component_module component_module when is_atom(component_module) -> component_module end) if component_modules -- existing_components == component_modules do :ok else duplicates = component_modules -- component_modules -- existing_components raise Error, {operation, "Components #{inspect(duplicates)} already exist for the entity #{inspect(entity_id)}"} end end end defp validate_component_exists(operation, component) do table = Util.components_state_ets_table() case :ets.lookup( table, {component.__meta__.entity.id, component.__meta__.module} ) do [{_key, _tags, _val}] -> :ok _ -> raise Error, {operation, "#{inspect(component)} does not exist"} end end # Resources CRUD validations defp validate_is_resource(operation, {resource_module, _state}) do validate_is_resource(operation, resource_module) end defp validate_is_resource(operation, resource_module) do Util.validate_ecs_type( resource_module, :resource, Error, {operation, "#{inspect(resource_module)} is not a Resource"} ) end defp validate_resource_state(operation, resource_state_struct) do if function_exported?(resource_state_struct.__meta__.module, :validate, 1) do case resource_state_struct.__meta__.module.validate(resource_state_struct) do :ok -> :ok {:error, error} -> raise Error, {operation, "#{inspect(resource_state_struct)} state is invalid. Error: #{inspect(error)}"} end else :ok end end # for now Resources CRUD are supported only in sync Systems! defp validate_locked_resource(_operation, :sync, _resource_module) do :ok end defp validate_locked_resource(operation, :async, resource_module) do raise Error, {operation, "Resource commands are supported only in sync Systems (startup_systems, frame_start_system, frame_end_system). Resource: #{inspect(resource_module)} "} end defp validate_resource_does_not_exist(operation, resource_spec) do resource_module = case resource_spec do {resource_module, _} when is_atom(resource_module) -> resource_module resource_module when is_atom(resource_module) -> resource_module end table = Util.resources_state_ets_table() case :ets.lookup(table, resource_module) do [] -> :ok _ -> raise Error, {operation, "Resource #{inspect(resource_module)} already exists"} end end defp validate_resource_exists(operation, resource) do table = Util.resources_state_ets_table() case :ets.lookup(table, resource.__meta__.module) do [{_key, _val}] -> :ok _ -> raise Error, {operation, "#{inspect(resource)} does not exist"} end end defp validate_tags(operation, tags) do unless is_list(tags) do raise Error, {operation, "Expected tags to be a list of atoms, got: #{inspect(tags)}"} end non_tags = Enum.reject(tags, &is_atom/1) case non_tags do [] -> :ok _ -> raise Error, {operation, "Expected tags to be a list of atoms, got: #{inspect(non_tags)}"} end end # Commits # CRUD operations are handled as a bundle for each command defp commit(%Command{} = command) do :ok = commit_inserts(command.insert_components) :ok = commit_updates(command.update_components) :ok = commit_deletes(command.delete_components) end defp commit_inserts([]), do: :ok defp commit_inserts(components) do table = Util.components_state_ets_table() # do not allow multiple operations for the same component unique_components = Enum.uniq_by(components, fn {key, _tags, _val} -> key end) duplicates = components -- unique_components case duplicates do [] -> :ets.insert(table, components) invalidate = Task.async(fn -> # invalidate the cache when inserting components Util.invalidate_cache() end) component_created_events(components) Task.await(invalidate) :ok _ -> raise "Error inserting components. Duplicate components insert is not allowed in the same Command: #{inspect(duplicates)}" end end defp commit_updates([]), do: :ok defp commit_updates(components) do table = Util.components_state_ets_table() # do not allow multiple operations for the same component unique_components = Enum.uniq_by(components, fn {key, _tags, _val} -> key end) duplicates = components -- unique_components case duplicates do [] -> :ets.insert(table, components) invalidate = Task.async(fn -> maybe_invalidate_cache_on_relation_update(components) end) component_updated_events(components) Task.await(invalidate) :ok _ -> raise "Error updating components. Duplicate components update is not allowed in the same Command: #{inspect(duplicates)}" end end defp commit_deletes([]), do: :ok defp commit_deletes(components) do table = Util.components_state_ets_table() Enum.each(components, fn {key, _tags, _val} -> :ets.delete(table, key) end) invalidate = Task.async(fn -> # invalidate the cache when deleting components Util.invalidate_cache() end) component_deleted_events(components) Task.await(invalidate) :ok end defp maybe_invalidate_cache_on_relation_update(components) do relation_updates = Enum.any?(components, fn {{_entity_id, module}, _tags, _component} -> module in [Ecspanse.Component.Children, Ecspanse.Component.Parents] end) if relation_updates do # invalidate the cache when updating Children or Parents Util.invalidate_query_cache() end end # helper query functions defp entities_descendants(entities) do Query.select({Ecspanse.Entity}, for_descendants_of: entities) |> Query.stream() |> Stream.map(fn {entity} -> entity end) |> Enum.to_list() end ### Special Events defp component_created_events(components) do components |> Enum.map(fn {_key, _tags, component} -> {{Event.ComponentCreated, component.__meta__.entity.id}, struct!(Event.ComponentCreated, %{ component: component, inserted_at: System.os_time() })} end) |> add_events() end defp component_updated_events(components) do components |> Enum.map(fn {_key, _tags, component} -> {{Event.ComponentUpdated, component.__meta__.entity.id}, struct!(Event.ComponentUpdated, %{ component: component, inserted_at: System.os_time() })} end) |> add_events() end defp component_deleted_events(components) do components |> Enum.map(fn {_key, _tags, component} -> {{Event.ComponentDeleted, component.__meta__.entity.id}, struct!(Event.ComponentDeleted, %{ component: component, inserted_at: System.os_time() })} end) |> add_events() end defp resource_created_event(resource) do event = {{Event.ResourceCreated, resource.__meta__.module}, struct!(Event.ResourceCreated, %{ resource: resource, inserted_at: System.os_time() })} add_events([event]) end defp resource_updated_event(resource) do event = {{Event.ResourceUpdated, resource.__meta__.module}, struct!(Event.ResourceUpdated, %{ resource: resource, inserted_at: System.os_time() })} add_events([event]) end defp resource_deleted_event(resource) do event = {{Event.ResourceDeleted, resource.__meta__.module}, struct!(Event.ResourceDeleted, %{ resource: resource, inserted_at: System.os_time() })} add_events([event]) end defp add_events(events) when is_list(events) do table = Util.events_ets_table() :ets.insert(table, events) end end