diff --git a/lib/account/public/account.ex b/lib/account/public/account.ex index 94e8428b..0cce6c0c 100644 --- a/lib/account/public/account.ex +++ b/lib/account/public/account.ex @@ -1,6 +1,7 @@ defmodule Helix.Account.Public.Account do alias Helix.Entity.Model.Entity + alias Helix.Entity.Query.Entity, as: EntityQuery alias Helix.Server.Public.Index, as: ServerIndex alias Helix.Story.Public.Index, as: StoryIndex alias Helix.Account.Public.Index, as: AccountIndex @@ -28,9 +29,20 @@ defmodule Helix.Account.Public.Account do @spec bootstrap(Entity.id) :: bootstrap def bootstrap(entity_id) do + entity = EntityQuery.fetch(entity_id) + + # OPTIMIZE: On `AccoutIndex`, more specifically on `InventoryIndex`, we + # fetch all of player's components, including the motherboards. This query + # (which is relatively expensive) is performed again on `ServerIndex`. This + # can be optimized by passing the queried motherboards on `AccountIndex` to + # `ServerIndex`. This would make the code a bit harder to read though, and + # that's the reason the queries are repeated. + # If my future self, or anyone else, ever wander through these dark lands + # looking for a low hanging fruit to optimize the bootstrap, start here! + %{ - account: AccountIndex.index(entity_id), - servers: ServerIndex.index(entity_id), + account: AccountIndex.index(entity), + servers: ServerIndex.index(entity), storyline: StoryIndex.index(entity_id) } end diff --git a/lib/account/public/index.ex b/lib/account/public/index.ex index da45b1df..656b0213 100644 --- a/lib/account/public/index.ex +++ b/lib/account/public/index.ex @@ -3,36 +3,40 @@ defmodule Helix.Account.Public.Index do alias Helix.Entity.Model.Entity alias Helix.Entity.Query.Entity, as: EntityQuery alias Helix.Server.Model.Server + alias Helix.Account.Public.Index.Inventory, as: InventoryIndex @type index :: %{ - mainframe: Server.id + mainframe: Server.id, + inventory: InventoryIndex.index } @type rendered_index :: %{ - mainframe: String.t + mainframe: String.t, + inventory: InventoryIndex.rendered_index } - @spec index(Entity.id) :: + @spec index(Entity.t) :: index - def index(entity_id) do - mainframe = - entity_id - |> EntityQuery.fetch() - |> EntityQuery.get_servers() - |> List.first() + def index(entity) do + mainframe = + entity + |> EntityQuery.get_servers() + |> List.first() - %{ - mainframe: mainframe - } + %{ + mainframe: mainframe, + inventory: InventoryIndex.index(entity) + } end @spec render_index(index) :: rendered_index def render_index(index) do - %{ - mainframe: to_string(index.mainframe) - } + %{ + mainframe: to_string(index.mainframe), + inventory: InventoryIndex.render_index(index.inventory) + } end end diff --git a/lib/account/public/index/inventory.ex b/lib/account/public/index/inventory.ex new file mode 100644 index 00000000..b7067794 --- /dev/null +++ b/lib/account/public/index/inventory.ex @@ -0,0 +1,165 @@ +defmodule Helix.Account.Public.Index.Inventory do + + import HELL.Macros + + alias HELL.Utils + alias Helix.Entity.Model.Entity + alias Helix.Entity.Query.Entity, as: EntityQuery + alias Helix.Network.Model.Network + alias Helix.Server.Model.Component + alias Helix.Server.Query.Component, as: ComponentQuery + alias Helix.Server.Query.Motherboard, as: MotherboardQuery + + @type index :: + %{ + components: component_index, + network_connections: network_connection_index + } + + @type rendered_index :: + %{ + components: rendered_component_index, + network_connections: rendered_network_connection_index + } + + @typep component_index :: [Component.t] + @typep network_connection_index :: [Network.Connection.t] + + @typep rendered_component_index :: + %{component_id :: String.t => rendered_component} + + @typep rendered_component :: + %{ + spec_id: String.t, + durability: float, + used?: boolean, + type: String.t, + custom: %{term => String.t} + } + + @typep rendered_network_connection_index :: + [rendered_network_connection] + + @typep rendered_network_connection :: + %{ + network_id: String.t, + ip: String.t, + name: String.t, + used?: boolean + } + + @spec index(Entity.t) :: + index + def index(entity = %Entity{}) do + %{ + components: get_components(entity), + network_connections: get_network_connections(entity) + } + end + + @spec get_components(Entity.t) :: + component_index + defp get_components(entity) do + entity + |> EntityQuery.get_components() + |> Enum.map(&(ComponentQuery.fetch(&1.component_id))) + end + + @spec get_network_connections(Entity.t) :: + network_connection_index + defp get_network_connections(entity) do + entity.entity_id + |> EntityQuery.get_network_connections() + end + + @spec render_index(index) :: + rendered_index + def render_index(index) do + %{ + components: render_components(index.components), + network_connections: render_network_connections(index.network_connections) + } + end + + @spec render_components(component_index) :: + rendered_component_index + defp render_components(components) do + linked_components = get_linked_components(components) + + Enum.reduce(components, %{}, fn component, acc -> + component_id = component.component_id |> to_string() + + %{} + |> Map.put(component_id, render_component(component, linked_components)) + |> Map.merge(acc) + end) + end + + @spec render_component(Component.t, [Component.t]) :: + rendered_component + defp render_component(component, linked_components) do + %{ + spec_id: to_string(component.spec_id), + type: to_string(component.type), + custom: Utils.stringify_map(component.custom), + used?: component in linked_components, + durability: 1.0 # TODO: #339 + } + end + + @spec render_network_connections(network_connection_index) :: + rendered_network_connection_index + defp render_network_connections(network_connections) do + Enum.reduce(network_connections, [], fn nc, acc -> + acc ++ [render_network_connection(nc)] + end) + end + + @spec render_network_connection(Network.Connection.t) :: + rendered_network_connection + defp render_network_connection(nc) do + %{ + network_id: nc.network_id |> to_string(), + ip: nc.ip, + name: "Internet", + used?: not is_nil(nc.nic_id) + } + end + + docp """ + Iterates over all components owned by the user and figures out which ones are + used (i.e. are linked to a motherboard). + + Note that, for our indexing purpose, a motherboard with linked components to + it is marked as a linked component itself, so the motherboard entry returned + to the client also tells whether it is being used or not. + """ + defp get_linked_components(components) do + components + + # Fetches all motherboards + |> Enum.reduce([], fn component, acc -> + if component.type == :mobo do + motherboard = MotherboardQuery.fetch(component.component_id) + + # Fetching a motherboard may return empty if there are no components + # linked to it, hence this check. + if motherboard do + acc ++ [{MotherboardQuery.fetch(component.component_id), component}] + else + acc + end + else + acc + end + end) + + # From these mobos, accumulates the linked components (and the mobo itself) + |> Enum.reduce([], fn {motherboard, mobo}, acc -> + acc ++ MotherboardQuery.get_components(motherboard) ++ [mobo] + end) + + # Flatten earth + |> List.flatten() + end +end diff --git a/lib/entity/action/entity.ex b/lib/entity/action/entity.ex index 8cd59818..9b629f02 100644 --- a/lib/entity/action/entity.ex +++ b/lib/entity/action/entity.ex @@ -6,6 +6,7 @@ defmodule Helix.Entity.Action.Entity do alias Helix.Universe.NPC.Model.NPC alias Helix.Entity.Internal.Entity, as: EntityInternal alias Helix.Entity.Model.Entity + alias Helix.Entity.Model.EntityComponent @spec create_from_specialization(struct) :: {:ok, Entity.t} @@ -46,7 +47,7 @@ defmodule Helix.Entity.Action.Entity do to: EntityInternal @spec link_component(Entity.t, Component.idt) :: - :ok + {:ok, EntityComponent.t} | {:error, Ecto.Changeset.t} @doc """ Links `component` to `entity` effectively making entity the owner of the diff --git a/lib/entity/query/entity.ex b/lib/entity/query/entity.ex index 42e497fb..c93ea93e 100644 --- a/lib/entity/query/entity.ex +++ b/lib/entity/query/entity.ex @@ -1,11 +1,11 @@ defmodule Helix.Entity.Query.Entity do - alias Helix.Universe.NPC.Model.NPC alias Helix.Account.Model.Account + alias Helix.Network.Query.Network, as: NetworkQuery alias Helix.Server.Model.Server + alias Helix.Universe.NPC.Model.NPC alias Helix.Entity.Internal.Entity, as: EntityInternal alias Helix.Entity.Model.Entity - alias Helix.Universe.NPC.Model.NPC @spec fetch(Entity.id) :: Entity.t @@ -58,6 +58,13 @@ defmodule Helix.Entity.Query.Entity do defdelegate get_components(entity), to: EntityInternal + @doc """ + Returns all network connections owned by the entity. + """ + defdelegate get_network_connections(entity), + to: NetworkQuery.Connection, + as: :get_by_entity + @spec get_entity_id(struct) :: Entity.id @doc """ @@ -67,12 +74,16 @@ defmodule Helix.Entity.Query.Entity do case entity do %Account{account_id: %Account.ID{id: id}} -> %Entity.ID{id: id} + %Account.ID{id: id} -> %Entity.ID{id: id} + %NPC{npc_id: %NPC.ID{id: id}} -> %Entity.ID{id: id} + %NPC.ID{id: id} -> %Entity.ID{id: id} + value -> Entity.ID.cast!(value) end diff --git a/lib/network/internal/network/connection.ex b/lib/network/internal/network/connection.ex index dde790cf..b6f88a2c 100644 --- a/lib/network/internal/network/connection.ex +++ b/lib/network/internal/network/connection.ex @@ -28,6 +28,14 @@ defmodule Helix.Network.Internal.Network.Connection do |> Repo.one() end + @spec get_by_entity(Entity.id) :: + [Network.Connection.t] + def get_by_entity(entity_id) do + entity_id + |> Network.Connection.Query.by_entity() + |> Repo.all() + end + @spec create(Network.idt, Network.ip, Entity.idt, Component.idt | nil) :: repo_result @doc """ diff --git a/lib/network/model/network/connection.ex b/lib/network/model/network/connection.ex index 7bd68caa..f7866765 100644 --- a/lib/network/model/network/connection.ex +++ b/lib/network/model/network/connection.ex @@ -85,6 +85,7 @@ defmodule Helix.Network.Model.Network.Connection do query do + alias Helix.Entity.Model.Entity alias Helix.Server.Model.Component alias Helix.Network.Model.Network @@ -97,5 +98,10 @@ defmodule Helix.Network.Model.Network.Connection do Queryable.t def by_nic(query \\ Network.Connection, nic_id), do: where(query, [nc], nc.nic_id == ^nic_id) + + @spec by_entity(Queryable.t, Entity.id) :: + Queryable.t + def by_entity(query \\ Network.Connection, entity_id), + do: where(query, [nc], nc.entity_id == ^entity_id) end end diff --git a/lib/network/query/network/connection.ex b/lib/network/query/network/connection.ex index c797b1bc..3ae788fd 100644 --- a/lib/network/query/network/connection.ex +++ b/lib/network/query/network/connection.ex @@ -7,4 +7,7 @@ defmodule Helix.Network.Query.Network.Connection do defdelegate fetch_by_nic(nic), to: NetworkInternal.Connection + + defdelegate get_by_entity(entity_id), + to: NetworkInternal.Connection end diff --git a/lib/server/internal/motherboard.ex b/lib/server/internal/motherboard.ex index a09576dc..59dcfffa 100644 --- a/lib/server/internal/motherboard.ex +++ b/lib/server/internal/motherboard.ex @@ -68,6 +68,17 @@ defmodule Helix.Server.Internal.Motherboard do Motherboard.get_free_slots(motherboard, mobo.spec_id) end + @spec get_components(Motherboard.t) :: + [Component.pluggable] + @doc """ + Returns all components linked to the motherboard. + """ + def get_components(motherboard = %Motherboard{}) do + Enum.reduce(motherboard.slots, [], fn {_slot_id, component}, acc -> + acc ++ [component] + end) + end + @spec get_cpus(Motherboard.t) :: [Component.cpu] @doc """ diff --git a/lib/server/public/index.ex b/lib/server/public/index.ex index e69ce29f..55cbb2ea 100644 --- a/lib/server/public/index.ex +++ b/lib/server/public/index.ex @@ -108,7 +108,7 @@ defmodule Helix.Server.Public.Index do tunnels: NetworkIndex.rendered_index } - @spec index(Entity.id) :: + @spec index(Entity.t) :: index @doc """ Returns the server index, which encompasses all other indexes residing under @@ -117,11 +117,8 @@ defmodule Helix.Server.Public.Index do Called on Account bootstrap (as opposed to `remote_server_index`, which is used after a player logs into a remote server) """ - def index(entity_id) do - player_servers = - entity_id - |> EntityQuery.fetch() - |> EntityQuery.get_servers() + def index(entity = %Entity{}) do + player_servers = EntityQuery.get_servers(entity) # Get all endpoints (any remote server the player is SSH-ed to) endpoints = TunnelQuery.get_remote_endpoints(player_servers) diff --git a/lib/server/query/motherboard.ex b/lib/server/query/motherboard.ex index d82267df..78bbd36e 100644 --- a/lib/server/query/motherboard.ex +++ b/lib/server/query/motherboard.ex @@ -22,6 +22,8 @@ defmodule Helix.Server.Query.Motherboard do |> MotherboardInternal.get_resources() end + defdelegate get_components(motherboard), + to: MotherboardInternal defdelegate get_cpus(motherboard), to: MotherboardInternal defdelegate get_hdds(motherboard), diff --git a/lib/server/websocket/channel/server/join.ex b/lib/server/websocket/channel/server/join.ex index fec23e24..294bedd9 100644 --- a/lib/server/websocket/channel/server/join.ex +++ b/lib/server/websocket/channel/server/join.ex @@ -362,8 +362,7 @@ join Helix.Server.Websocket.Channel.Server.Join do # If counter was not specified, set it as `nil`. Later, the request # will figure out what is the next counter expected to be. else - ip = topic - {ip, nil} + {topic, nil} end data = diff --git a/test/account/public/index/inventory_test.exs b/test/account/public/index/inventory_test.exs new file mode 100644 index 00000000..1e39a0a3 --- /dev/null +++ b/test/account/public/index/inventory_test.exs @@ -0,0 +1,102 @@ +defmodule Helix.Account.Public.Index.InventoryTest do + + use Helix.Test.Case.Integration + + alias Helix.Cache.Query.Cache, as: CacheQuery + alias Helix.Entity.Action.Entity, as: EntityAction + alias Helix.Account.Public.Index.Inventory, as: InventoryIndex + + alias Helix.Test.Server.Setup, as: ServerSetup + alias Helix.Test.Server.Component.Setup, as: ComponentSetup + alias Helix.Test.Network.Setup, as: NetworkSetup + + describe "index/1" do + test "indexes the entire inventory" do + {server, %{entity: entity}} = ServerSetup.server() + + index = InventoryIndex.index(entity) + + # There are 5 components (initial hardware) + assert length(index.components) == 5 + + [nc] = index.network_connections + + # The NIC used for the NetworkConnection is among the player's components + assert Enum.find(index.components, &(&1.component_id == nc.nic_id)) + + # The NIP described by the NetworkConnection points to the player's server + assert {:ok, server.server_id} == + CacheQuery.from_nip_get_server(nc.network_id, nc.ip) + end + end + + describe "render_index/1" do + test "renders the index to a JSON-friendly format" do + {_, %{entity: entity}} = ServerSetup.server() + + rendered = + entity + |> InventoryIndex.index() + |> InventoryIndex.render_index() + + assert is_map(rendered.components) + assert is_list(rendered.network_connections) + + Enum.each(rendered.components, fn {comp_id, component} -> + assert is_binary(comp_id) + + assert component.custom + assert is_binary(component.spec_id) + assert is_binary(component.type) + assert is_float(component.durability) + assert is_boolean(component.used?) + end) + + Enum.each(rendered.network_connections, fn nc -> + assert is_binary(nc.network_id) + assert is_binary(nc.ip) + assert is_binary(nc.name) + assert is_boolean(nc.used?) + end) + end + + test "correctly marks components and connections as used / not used" do + {_, %{entity: entity}} = ServerSetup.server() + + # Create two components that are not used + {mobo, _} = ComponentSetup.component(type: :mobo) + {cpu, _} = ComponentSetup.component(type: :cpu) + + # Link those components to the entity + assert {:ok, _} = EntityAction.link_component(entity, mobo) + assert {:ok, _} = EntityAction.link_component(entity, cpu) + + # Create a NetworkConnection that is unused + {nc, _} = NetworkSetup.Connection.connection(entity_id: entity.entity_id) + + rendered = + entity + |> InventoryIndex.index() + |> InventoryIndex.render_index() + + Enum.each(rendered.components, fn {component_id, component} -> + if \ + component_id == to_string(mobo.component_id) or + component_id == to_string(cpu.component_id) + do + refute component.used? + else + assert component.used? + end + end) + + Enum.each(rendered.network_connections, fn rendered_nc -> + if rendered_nc.ip == nc.ip do + refute rendered_nc.used? + else + assert rendered_nc.used? + end + end) + end + end +end diff --git a/test/server/public/index_test.exs b/test/server/public/index_test.exs index 53ad1aae..ea6462b3 100644 --- a/test/server/public/index_test.exs +++ b/test/server/public/index_test.exs @@ -13,7 +13,7 @@ defmodule Helix.Server.Public.IndexTest do test "indexes player server correctly" do {server, %{entity: entity}} = ServerSetup.server() - index = ServerIndex.index(entity.entity_id) + index = ServerIndex.index(entity) assert length(index.player) == 1 assert Enum.empty?(index.remote) @@ -47,7 +47,7 @@ defmodule Helix.Server.Public.IndexTest do bounces: tunnel2_bounce] NetworkSetup.connection([tunnel_opts: tunnel2_opts, type: :ssh]) - index = ServerIndex.index(entity.entity_id) + index = ServerIndex.index(entity) result_gateway = Enum.find(index.player, &(&1.server_id == player.server_id)) @@ -97,7 +97,7 @@ defmodule Helix.Server.Public.IndexTest do [gateway_id: gateway2.server_id, destination_id: target2.server_id] NetworkSetup.connection([tunnel_opts: g2t2_opts, type: :ssh]) - index = ServerIndex.index(entity.entity_id) + index = ServerIndex.index(entity) # 2 gateway servers, 2 remote servers assert length(index.player) == 2 @@ -135,7 +135,7 @@ defmodule Helix.Server.Public.IndexTest do bounces: tunnel_bounce] NetworkSetup.connection([tunnel_opts: tunnel_opts, type: :ssh]) - index = ServerIndex.index(entity.entity_id) + index = ServerIndex.index(entity) rendered = ServerIndex.render_index(index) rendered_gateway =