Skip to content

Commit

Permalink
23 fetch commands dont have a nice error if the application isnt star…
Browse files Browse the repository at this point in the history
…ted (#25)

* Correct tutorial function name

* Update Credo

* Update getting started guide with an example

* Raise with special message with ETS tables do not exist

* Update the tutorial

* Version number and changelog
  • Loading branch information
iacobson authored Dec 22, 2023
1 parent 2f78651 commit 2eb2f0e
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 23 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## v0.8.1 (2023-12-22)

### Improvements

- returns an explicit error message when trying to run queries or create events and the Ecspanse server is not running.
- documentation improvements.

## v0.8.0 (2023-11-14)

### Improvements
Expand Down
20 changes: 19 additions & 1 deletion guides/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,25 @@ The `c:Ecspanse.setup/1` callback is mandatory. It will be used later on to sche

## Starting the Ecspanse server

The module implementing `Ecspanse` needs to be added to the supervision tree.
The module invoking `use Ecspanse` needs to be added to the aplication supervision tree.

```elixir
defmodule Demo.Application do
@moduledoc false

use Application

@impl true
def start(_type, _args) do
children = [
Demo
]

opts = [strategy: :one_for_one, name: Demo.Supervisor]
Supervisor.start_link(children, opts)
end
end
```

The Ecspanse server loop will start together with the application:

Expand Down
31 changes: 26 additions & 5 deletions guides/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,11 @@ defmodule Demo.API do
@spec fetch_hero_details() :: {:ok, map()} | {:error, :not_found}
def fetch_hero_details do
Ecspanse.Query.select(
{Demo.Components.Hero, Demo.Components.Energy, Demo.Components.Position}
{Ecspanse.Entity, Demo.Components.Hero, Demo.Components.Energy, Demo.Components.Position}
)
|> Ecspanse.Query.one()
|> case do
{hero, energy, position} ->
{hero_entity, hero, energy, position} ->
%{name: hero.name, energy: energy.current, max_energy: energy.max, pos_x: position.x, pos_y: position.y}
_ ->
{:error, :not_found}
Expand Down Expand Up @@ -444,12 +444,12 @@ defmodule Demo do
def setup(data) do
data
|> Ecspanse.add_startup_system(Systems.SpawnHero)
|> Ecspanse.add_system(Systems.RestoreEnergy, run_if: [{__MODULE__, :energy_not_max}])
|> Ecspanse.add_system(Systems.RestoreEnergy, run_if: [{__MODULE__, :energy_not_max?}])
|> Ecspanse.add_system(Systems.MoveHero, run_after: [Systems.RestoreEnergy])
|> Ecspanse.add_frame_end_system(Ecspanse.System.Timer)
end

def energy_not_max do
def energy_not_max? do
Ecspanse.Query.select({Demo.Components.Energy}, with: [Demo.Components.Hero])
|> Ecspanse.Query.one()
|> case do
Expand All @@ -466,7 +466,7 @@ end

#### The Conditional System Execution

By using the `:run_if` option, the `RestoreEnergy` system will run only if the current energy is below the max energy. The `energy_not_max/0` function must always return a boolean value. Please note, this is not an efficient implementation. The `energy_not_max/0` function will be called every frame. If the check would happen in the `RestoreEnergy` system, it would run only once every 3 seconds. But we took the opportunity to exemplify conditionally running systems.
By using the `:run_if` option, the `RestoreEnergy` system will run only if the current energy is below the max energy. The `energy_not_max?/0` function must always return a boolean value. Please note, this is not an efficient implementation. The `energy_not_max?/0` function will be called every frame. If the check would happen in the `RestoreEnergy` system, it would run only once every 3 seconds. But we took the opportunity to exemplify conditionally running systems.

#### The System Execution Order

Expand Down Expand Up @@ -637,6 +637,16 @@ The last step of the current section is to expose the resources in the `fetch_he

Here we use the `Ecspanse.Query.list_tagged_components_for_entity/2` function to get all the components tagged with `:resource` and `:available` for the hero entity.

The map returned by `fetch_hero_details/0` function should be updated with the new resources field:

```elixir
%{
# ...
pos_y: position.y,
resources: list_hero_resources(hero_entity),
}
```

Starting the application and moving the hero around will now start to accumulate resources:

```iex
Expand Down Expand Up @@ -819,6 +829,17 @@ defmodule Demo.API do
end
```

The map returned by `fetch_hero_details/0` function should be updated with the new inventory field:

```elixir
%{
# ...
pos_y: position.y,
resources: list_hero_resources(hero_entity),
inventory: list_hero_inventory(hero_entity)
}
```

We will now display the hero's inventory and the market items with their respective prices.

We can test the new functions in the `iex` console:
Expand Down
11 changes: 10 additions & 1 deletion lib/ecspanse.ex
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,16 @@ defmodule Ecspanse do

event = prepare_event(event_spec, batch_key)

:ets.insert(Util.events_ets_table(), event)
try do
:ets.insert(Util.events_ets_table(), event)
rescue
e ->
case :ets.info(Util.events_ets_table()) do
:undefined -> reraise Util.server_not_started_error(), __STACKTRACE__
_ -> reraise e, __STACKTRACE__
end
end

:ok
end

Expand Down
125 changes: 115 additions & 10 deletions lib/ecspanse/query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,16 @@ defmodule Ecspanse.Query do
{{^entity_id, _component_module}, _component_tags, _component_state} -> ^entity_id
end

result = :ets.select(Util.components_state_ets_table(), f, 1)
result =
try do
:ets.select(Util.components_state_ets_table(), f, 1)
rescue
e ->
case :ets.info(Util.components_state_ets_table()) do
:undefined -> reraise Util.server_not_started_error(), __STACKTRACE__
_ -> reraise e, __STACKTRACE__
end
end

case result do
{[^entity_id], _} ->
Expand Down Expand Up @@ -305,7 +314,18 @@ defmodule Ecspanse.Query do

@doc false
defmemo memo_list_children(%Entity{id: entity_id}), max_waiter: 1000, waiter_sleep_ms: 0 do
case :ets.lookup(Util.components_state_ets_table(), {entity_id, Component.Children}) do
result =
try do
:ets.lookup(Util.components_state_ets_table(), {entity_id, Component.Children})
rescue
e ->
case :ets.info(Util.components_state_ets_table()) do
:undefined -> reraise Util.server_not_started_error(), __STACKTRACE__
_ -> reraise e, __STACKTRACE__
end
end

case result do
[{_key, _tags, %Component.Children{entities: children_entities}}] -> children_entities
[] -> []
end
Expand Down Expand Up @@ -349,7 +369,18 @@ defmodule Ecspanse.Query do

@doc false
defmemo memo_list_parents(%Entity{id: entity_id}), max_waiter: 1000, waiter_sleep_ms: 0 do
case :ets.lookup(Util.components_state_ets_table(), {entity_id, Component.Parents}) do
result =
try do
:ets.lookup(Util.components_state_ets_table(), {entity_id, Component.Parents})
rescue
e ->
case :ets.info(Util.components_state_ets_table()) do
:undefined -> reraise Util.server_not_started_error(), __STACKTRACE__
_ -> reraise e, __STACKTRACE__
end
end

case result do
[{_key, _tags, %Component.Parents{entities: parents_entities}}] -> parents_entities
[] -> []
end
Expand Down Expand Up @@ -454,7 +485,18 @@ defmodule Ecspanse.Query do

filtered_entities_components_tags
|> Stream.map(fn {entity_id, comp_module, _tags_set} ->
case :ets.lookup(table, {entity_id, comp_module}) do
result =
try do
:ets.lookup(table, {entity_id, comp_module})
rescue
e ->
case :ets.info(table) do
:undefined -> reraise Util.server_not_started_error(), __STACKTRACE__
_ -> reraise e, __STACKTRACE__
end
end

case result do
[{_key, _tags, comp_state}] -> comp_state
# checking for race conditions when a required component is removed during the query
# the whole entity should be filtered out
Expand Down Expand Up @@ -519,7 +561,18 @@ defmodule Ecspanse.Query do
entity_id in entity_ids
end)
|> Stream.map(fn {entity_id, comp_module, _tags_set} ->
case :ets.lookup(table, {entity_id, comp_module}) do
result =
try do
:ets.lookup(table, {entity_id, comp_module})
rescue
e ->
case :ets.info(table) do
:undefined -> reraise Util.server_not_started_error(), __STACKTRACE__
_ -> reraise e, __STACKTRACE__
end
end

case result do
[{_key, _tags, comp_state}] -> comp_state
# checking for race conditions when a required component is removed during the query
# the whole entity should be filtered out
Expand Down Expand Up @@ -631,7 +684,18 @@ defmodule Ecspanse.Query do
@spec fetch_component(Ecspanse.Entity.t(), module()) ::
{:ok, component_state :: struct()} | {:error, :not_found}
def fetch_component(%Entity{id: entity_id}, component_module) do
case :ets.lookup(Util.components_state_ets_table(), {entity_id, component_module}) do
result =
try do
:ets.lookup(Util.components_state_ets_table(), {entity_id, component_module})
rescue
e ->
case :ets.info(Util.components_state_ets_table()) do
:undefined -> reraise Util.server_not_started_error(), __STACKTRACE__
_ -> reraise e, __STACKTRACE__
end
end

case result do
[{_key, _tags, component}] -> {:ok, component}
[] -> {:error, :not_found}
end
Expand Down Expand Up @@ -693,7 +757,15 @@ defmodule Ecspanse.Query do
component_state
end

:ets.select(table, f)
try do
:ets.select(table, f)
rescue
e ->
case :ets.info(table) do
:undefined -> reraise Util.server_not_started_error(), __STACKTRACE__
_ -> reraise e, __STACKTRACE__
end
end
end

@doc """
Expand Down Expand Up @@ -860,7 +932,18 @@ defmodule Ecspanse.Query do
@spec fetch_resource(resource_module :: module()) ::
{:ok, resource_state :: struct()} | {:error, :not_found}
def fetch_resource(resource_module) do
case :ets.lookup(Util.resources_state_ets_table(), resource_module) do
result =
try do
:ets.lookup(Util.resources_state_ets_table(), resource_module)
rescue
e ->
case :ets.info(Util.resources_state_ets_table()) do
:undefined -> reraise Util.server_not_started_error(), __STACKTRACE__
_ -> reraise e, __STACKTRACE__
end
end

case result do
[{_key, resource}] -> {:ok, resource}
[] -> {:error, :not_found}
end
Expand Down Expand Up @@ -1086,7 +1169,18 @@ defmodule Ecspanse.Query do
# add mandatory components to the select tuple
defp add_select_components(select_tuple, comp_modules, entity_id, components_state_ets_table) do
Enum.reduce(comp_modules, select_tuple, fn comp_module, acc ->
case :ets.lookup(components_state_ets_table, {entity_id, comp_module}) do
result =
try do
:ets.lookup(components_state_ets_table, {entity_id, comp_module})
rescue
e ->
case :ets.info(components_state_ets_table) do
:undefined -> reraise Util.server_not_started_error(), __STACKTRACE__
_ -> reraise e, __STACKTRACE__
end
end

case result do
[{_key, _tags, comp_state}] -> Tuple.append(acc, comp_state)
# checking for race conditions when a required component is removed during the query
# the whole entity should be filtered out
Expand All @@ -1103,7 +1197,18 @@ defmodule Ecspanse.Query do
components_state_ets_table
) do
Enum.reduce(comp_modules, select_tuple, fn comp_module, acc ->
case :ets.lookup(components_state_ets_table, {entity_id, comp_module}) do
result =
try do
:ets.lookup(components_state_ets_table, {entity_id, comp_module})
rescue
e ->
case :ets.info(components_state_ets_table) do
:undefined -> reraise Util.server_not_started_error(), __STACKTRACE__
_ -> reraise e, __STACKTRACE__
end
end

case result do
[{_key, _tags, comp_state}] -> Tuple.append(acc, comp_state)
[] -> Tuple.append(acc, nil)
end
Expand Down
37 changes: 33 additions & 4 deletions lib/ecspanse/util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,16 @@ defmodule Ecspanse.Util do
{entity_id, component_module}
end

:ets.select(components_state_ets_table(), f)
|> Enum.group_by(fn {k, _v} -> k end, fn {_k, v} -> v end)
try do
:ets.select(components_state_ets_table(), f)
|> Enum.group_by(fn {k, _v} -> k end, fn {_k, v} -> v end)
rescue
e ->
case :ets.info(components_state_ets_table()) do
:undefined -> reraise server_not_started_error(), __STACKTRACE__
_ -> reraise e, __STACKTRACE__
end
end
end

@doc false
Expand Down Expand Up @@ -105,7 +113,15 @@ defmodule Ecspanse.Util do
{entity_id, component_module, component_tags_set}
end

:ets.select(components_state_ets_table(), f)
try do
:ets.select(components_state_ets_table(), f)
rescue
e ->
case :ets.info(components_state_ets_table()) do
:undefined -> reraise server_not_started_error(), __STACKTRACE__
_ -> reraise e, __STACKTRACE__
end
end
end

@doc false
Expand All @@ -122,7 +138,15 @@ defmodule Ecspanse.Util do
{id, component_tags_set, component_state}
end

:ets.select(components_state_ets_table(), f)
try do
:ets.select(components_state_ets_table(), f)
rescue
e ->
case :ets.info(components_state_ets_table()) do
:undefined -> reraise server_not_started_error(), __STACKTRACE__
_ -> reraise e, __STACKTRACE__
end
end
end

@doc false
Expand Down Expand Up @@ -167,6 +191,11 @@ defmodule Ecspanse.Util do
end
end

@doc false
def server_not_started_error do
"Ecspanse Server not started. The module invoking `use Ecspanse` needs to be added to the aplication supervision tree."
end

@doc false
def invalidate_cache do
Memoize.invalidate(Ecspanse.Query)
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Ecspanse.MixProject do
use Mix.Project

@version "0.8.0"
@version "0.8.1"
@source_url "https://github.com/iacobson/ecspanse"

def project do
Expand Down
Loading

0 comments on commit 2eb2f0e

Please sign in to comment.