diff --git a/lib/kalevala/cache.ex b/lib/kalevala/cache.ex index b987331..5623abd 100644 --- a/lib/kalevala/cache.ex +++ b/lib/kalevala/cache.ex @@ -72,6 +72,13 @@ defmodule Kalevala.Cache do Kalevala.Cache.get(__MODULE__, key) end + @doc """ + Delete a value from the cache + """ + def delete(key) do + Kalevala.Cache.delete(__MODULE__, key) + end + @doc """ Get a value from the cache @@ -118,6 +125,13 @@ defmodule Kalevala.Cache do end end + @doc """ + Delete a value from the cache + """ + def delete(name, key) do + :ets.delete(name, key) + end + @doc """ Get a list of all keys in a table """ diff --git a/lib/kalevala/character/action.ex b/lib/kalevala/character/action.ex index 7d4e148..fdb5f9f 100644 --- a/lib/kalevala/character/action.ex +++ b/lib/kalevala/character/action.ex @@ -6,7 +6,7 @@ defmodule Kalevala.Character.Action do flee in a random direction. """ - @callback run(Conn.t(), map()) :: :ok + @callback run(Conn.t(), map()) :: Conn.t() defstruct [:request_id, :type, delay: 0, params: %{}] diff --git a/lib/kalevala/character/conn.ex b/lib/kalevala/character/conn.ex index 29fc811..4fe10f1 100644 --- a/lib/kalevala/character/conn.ex +++ b/lib/kalevala/character/conn.ex @@ -94,7 +94,7 @@ defmodule Kalevala.Character.Conn.Text do new text. """ - defstruct [:data, newline: false, go_ahead: false] + defstruct [:data, newline: false, go_ahead: true] end defmodule Kalevala.Character.Conn.Option do @@ -116,6 +116,7 @@ defmodule Kalevala.Character.Conn do alias Kalevala.Meta defstruct [ + :controller, :character, :params, assigns: %{}, @@ -159,10 +160,9 @@ defmodule Kalevala.Character.Conn do end defp merge_assigns(conn, assigns) do - conn.session - |> Map.put(:character, Private.character(conn)) + Map.new() + |> Map.put(:self, Private.character(conn)) |> Map.merge(conn.assigns) - |> Map.merge(conn.flash) |> Map.merge(assigns) end @@ -307,9 +307,11 @@ defmodule Kalevala.Character.Conn do @doc """ Creates an even to move from one room to another """ - def move(conn, direction, room_id, view, template, assigns) do - assigns = merge_assigns(conn, assigns) - data = view.render(template, assigns) + def move(conn, direction, room_id, view, template, data \\ %{}) do + data = Map.put(data, :character, Private.character(conn)) + assigns = merge_assigns(conn, data) + + reason = view.render(template, assigns) event = %Kalevala.Event{ acting_character: Private.character(conn), @@ -318,8 +320,9 @@ defmodule Kalevala.Character.Conn do data: %Kalevala.Event.Movement{ character: Private.character(conn), direction: direction, - reason: data, - room_id: room_id + reason: reason, + room_id: room_id, + data: data } } @@ -449,6 +452,14 @@ defmodule Kalevala.Character.Conn do put_character(conn, character) end + @doc """ + Get values in a character's meta map + """ + def get_meta(conn, key) do + character = character(conn) + Meta.get(character.meta, key) + end + defp put_private(conn, key, value) do private = Map.put(conn.private, key, value) Map.put(conn, :private, private) diff --git a/lib/kalevala/character/foreman.ex b/lib/kalevala/character/foreman.ex index 111768b..d142ad4 100644 --- a/lib/kalevala/character/foreman.ex +++ b/lib/kalevala/character/foreman.ex @@ -73,6 +73,7 @@ defmodule Kalevala.Character.Foreman do @doc false def new_conn(state) do %Conn{ + controller: state.controller, character: state.character, session: state.session, flash: state.flash, diff --git a/lib/kalevala/communication.ex b/lib/kalevala/communication.ex index d8e5cb3..b1ba7b6 100644 --- a/lib/kalevala/communication.ex +++ b/lib/kalevala/communication.ex @@ -106,7 +106,7 @@ defmodule Kalevala.Communication do Channel.subscribe(pid, channel_name, subscriber_pid, options) _ -> - :error + {:error, :channel_not_found} end end @@ -119,7 +119,7 @@ defmodule Kalevala.Communication do Channel.unsubscribe(pid, channel_name, subscriber_pid, options) _ -> - :error + {:error, :channel_not_found} end end @@ -142,6 +142,6 @@ defmodule Kalevala.Communication do Only events with a topic of #{__MODULE__} allowed. """) - :error + {:error, :channel_not_found} end end diff --git a/lib/kalevala/event/movement.ex b/lib/kalevala/event/movement.ex index e911fe4..96a9fa1 100644 --- a/lib/kalevala/event/movement.ex +++ b/lib/kalevala/event/movement.ex @@ -3,7 +3,7 @@ defmodule Kalevala.Event.Movement do An event to move from one room to another """ - defstruct [:character, :direction, :reason, :room_id] + defstruct [:character, :direction, :reason, :room_id, :data] @typedoc """ A movement event @@ -22,7 +22,7 @@ defmodule Kalevala.Event.Movement.Notice do Event to send a notice to other characters in the room """ - defstruct [:character, :direction, :reason] + defstruct [:character, :direction, :reason, :data] end defmodule Kalevala.Event.Movement.Commit do @@ -30,7 +30,7 @@ defmodule Kalevala.Event.Movement.Commit do Struct for committing movement between two rooms """ - defstruct [:character, :to, :from, :exit_name] + defstruct [:character, :to, :from, :exit_name, :entrance_name] end defmodule Kalevala.Event.Movement.Abort do @@ -57,6 +57,7 @@ defmodule Kalevala.Event.Movement.Voting do :to, :from, :exit_name, + :entrance_name, :reason, aborted: false ] @@ -86,7 +87,8 @@ defmodule Kalevala.Event.Movement.Voting do character: event.data.character, to: event.data.to, from: event.data.from, - exit_name: event.data.exit_name + exit_name: event.data.exit_name, + entrance_name: event.data.entrance_name } } end diff --git a/lib/kalevala/world/room.ex b/lib/kalevala/world/room.ex index 06eb518..28b10a3 100644 --- a/lib/kalevala/world/room.ex +++ b/lib/kalevala/world/room.ex @@ -89,7 +89,8 @@ defmodule Kalevala.World.Room do @impl true def handle_continue(:initialized, state) do - Callbacks.initialized(state.data) + data = Callbacks.initialized(state.data) + state = %{state | data: data} {:noreply, state} end @@ -114,6 +115,10 @@ defmodule Kalevala.World.Room do {:reply, :ok, state} end + def handle_call(:dump, _from, state) do + {:reply, state.data, state} + end + @impl true def handle_info(event = %Event{}, state) do Events.handle_event(event, state) diff --git a/lib/kalevala/world/room/context.ex b/lib/kalevala/world/room/context.ex index f9dd7e4..a1d95f7 100644 --- a/lib/kalevala/world/room/context.ex +++ b/lib/kalevala/world/room/context.ex @@ -67,7 +67,7 @@ defmodule Kalevala.World.Room.Context do @doc """ Render a prompt back to a pid """ - def prompt(context, to_pid, view, template, assigns) do + def prompt(context, to_pid, view, template, assigns \\ %{}) do assigns = Map.merge(context.assigns, assigns) data = view.render(template, assigns) push(context, to_pid, data, true) @@ -94,6 +94,45 @@ defmodule Kalevala.World.Room.Context do Map.put(context, :events, context.events ++ [{to_pid, event}]) end + @doc """ + Update the context data + """ + def put_data(context, key, val) do + data = Map.put(context.data, key, val) + Map.put(context, :data, data) + end + + def update_character(context, character) do + %Kalevala.Character{id: id} = character + + characters = + Enum.map(context.characters, fn + %{id: ^id} -> character + no_change -> no_change + end) + + %{context | characters: characters} + end + + @doc """ + Broadcast an event to multiple characters in the room + Options: + - `to`: pre-filtered list of characters + """ + def broadcast(context, topic, data, opts \\ []) do + recipients = Keyword.get(opts, :to, context.characters) + + recipients = + case !is_list(recipients) do + true -> List.wrap(recipients) + false -> recipients + end + + Enum.reduce(recipients, context, fn character, acc -> + event(acc, character.pid, self(), topic, data) + end) + end + @doc """ Handle context after processing an event """ diff --git a/lib/kalevala/world/room/events.ex b/lib/kalevala/world/room/events.ex index f39377a..b21a286 100644 --- a/lib/kalevala/world/room/events.ex +++ b/lib/kalevala/world/room/events.ex @@ -31,7 +31,23 @@ defmodule Kalevala.World.Room.Events do case event.data.room_id == state.data.id do true -> state = Movement.handle_event(state, event) - {:noreply, state} + + extra_data = with nil <- event.data.data, do: %{} + extra_data = Map.delete(extra_data, :character) + + event = %Kalevala.Event{ + acting_character: event.data.character, + from_pid: event.from_pid, + topic: Kalevala.Event.Movement.Notice, + data: %Kalevala.Event.Movement.Notice{ + character: event.data.character, + direction: event.data.direction, + data: extra_data, + reason: event.data.reason + } + } + + handle_event(event, state) false -> Room.global_name(event.data.room_id) @@ -76,7 +92,12 @@ defmodule Kalevala.World.Room.Events do |> Handler.event(event) |> Context.handle_context() - state = Map.put(state, :data, context.data) + private = + state.private + |> Map.put(:characters, context.characters) + |> Map.put(:item_instances, context.item_instances) + + state = %{state | data: context.data, private: private} {:noreply, state} end @@ -264,6 +285,9 @@ defmodule Kalevala.World.Room.Movement do Broadcast the event to characters in the room """ def broadcast(state, event) do + data = event.data.data + data = if is_map(data), do: Map.delete(event.data.data, :character) + event = %Kalevala.Event{ acting_character: event.data.character, from_pid: event.from_pid, @@ -271,7 +295,8 @@ defmodule Kalevala.World.Room.Movement do data: %Kalevala.Event.Movement.Notice{ character: event.data.character, direction: event.data.direction, - reason: event.data.reason + reason: event.data.reason, + data: data } } diff --git a/lib/kalevala/world/zone.ex b/lib/kalevala/world/zone.ex index 23eae94..63bc17f 100644 --- a/lib/kalevala/world/zone.ex +++ b/lib/kalevala/world/zone.ex @@ -9,14 +9,12 @@ defmodule Kalevala.World.Zone do alias Kalevala.Event alias Kalevala.World.Zone.Movement + alias Kalevala.World.Zone.Handler + alias Kalevala.World.Zone.Callbacks + alias Kalevala.World.Zone.Context @type t() :: map() - @doc """ - Called when the zone is initializing - """ - @callback init(zone :: t()) :: t() - @doc """ Replace internal zone state """ @@ -42,7 +40,7 @@ defmodule Kalevala.World.Zone do Logger.info("Zone starting - #{options.zone.id}") config = options.config - zone = config.callback_module.init(options.zone) + zone = Callbacks.init(options.zone) state = %{ data: zone, @@ -50,7 +48,14 @@ defmodule Kalevala.World.Zone do callback_module: config.callback_module } - {:ok, state} + {:ok, state, {:continue, :initialized}} + end + + @impl true + def handle_continue(:initialized, state) do + data = Callbacks.initialized(state.data) + state = %{state | data: data} + {:noreply, state} end @impl true @@ -65,6 +70,62 @@ defmodule Kalevala.World.Zone do {:noreply, state} end + + @impl true + def handle_info(event = %Event{}, state) do + context = + state + |> Handler.event(event) + |> Context.handle_context() + + state = Map.put(state, :data, context.data) + + {:noreply, state} + end + + @impl true + def handle_info(event = %Event.Delayed{}, state) do + event = Event.Delayed.to_event(event) + + context = + state + |> Handler.event(event) + |> Context.handle_context() + + state = Map.put(state, :data, context.data) + + {:noreply, state} + end +end + +defmodule Kalevala.World.Zone.Handler do + @moduledoc false + + alias Kalevala.World.Zone.Callbacks + alias Kalevala.World.Zone.Context + + def event(state, event) do + Callbacks.event(state.data, Context.new(state), event) + end +end + +defprotocol Kalevala.World.Zone.Callbacks do + @doc """ + Called when the zone is initializing + """ + def init(zone) + + @doc """ + Called after the zone process is started + + Directly after `init` is completed. + """ + def initialized(zone) + + @doc """ + Callback for when a new event is received + """ + def event(zone, context, event) end defmodule Kalevala.World.BasicZone do diff --git a/lib/kalevala/world/zone/context.ex b/lib/kalevala/world/zone/context.ex new file mode 100644 index 0000000..2d228ec --- /dev/null +++ b/lib/kalevala/world/zone/context.ex @@ -0,0 +1,149 @@ +defmodule Kalevala.World.Zone.Context do + @moduledoc """ + Context for performing work for an event in a room + """ + + alias Kalevala.Event + + @type t() :: %__MODULE__{} + + defstruct [:data, events: [], output: [], assigns: %{}] + + @doc """ + Create a new context struct from room state + """ + def new(state) do + %__MODULE__{ + data: state.data + } + end + + defp push(context, to_pid, event = %Kalevala.Character.Conn.Event{}, _newline) do + Map.put(context, :output, context.output ++ [{to_pid, event}]) + end + + defp push(context, to_pid, event = %Kalevala.Character.Conn.EventText{}, newline) do + text = %Kalevala.Character.Conn.Text{ + data: event.text, + newline: newline + } + + event = %{event | text: text} + + Map.put(context, :output, context.output ++ [{to_pid, event}]) + end + + defp push(context, to_pid, data, newline) do + text = %Kalevala.Character.Conn.Text{ + data: data, + newline: newline + } + + Map.put(context, :output, context.output ++ [{to_pid, text}]) + end + + @doc """ + Render text back to a pid + """ + def render(context, to_pid, view, template, assigns \\ %{}) do + assigns = Map.merge(context.assigns, assigns) + data = view.render(template, assigns) + push(context, to_pid, data, false) + end + + @doc """ + Render a prompt back to a pid + """ + def prompt(context, to_pid, view, template, assigns) do + assigns = Map.merge(context.assigns, assigns) + data = view.render(template, assigns) + push(context, to_pid, data, true) + end + + @doc """ + Add to the assignment map on the context + """ + def assign(context, key, value) do + assigns = Map.put(context.assigns, key, value) + Map.put(context, :assigns, assigns) + end + + @doc """ + Send an event back to a pid + """ + def event(context, to_pid, from_pid, topic, data) do + event = %Kalevala.Event{ + from_pid: from_pid, + topic: topic, + data: data + } + + Map.put(context, :events, context.events ++ [{to_pid, event}]) + end + + @doc """ + Send an event to a pid on a delay + """ + + def delay_event(context, delay, to_pid, topic, data) do + event = %Kalevala.Event.Delayed{ + delay: delay, + from_pid: self(), + topic: topic, + data: data + } + + Map.put(context, :events, context.events ++ [{self(), event}]) + end + + @doc """ + Update the context data + """ + def put_data(context, key, val) do + data = Map.put(context.data, key, val) + Map.put(context, :data, data) + end + + @doc """ + Handle context after processing an event + """ + def handle_context(context) do + context + |> send_events() + |> send_output() + end + + defp send_events(context) do + {events, delayed_events} = + Enum.split_with(context.events, fn {_, event} -> + match?(%Kalevala.Event{}, event) + end) + + Enum.each(events, fn {to_pid, event} -> + send(to_pid, event) + end) + + Enum.each(delayed_events, fn {to_pid, delayed_event} -> + Process.send_after(to_pid, delayed_event, delayed_event.delay) + end) + + context + end + + defp send_output(context) do + context.output + |> Enum.group_by( + fn {to_pid, _text} -> + to_pid + end, + fn {_to_pid, text} -> + text + end + ) + |> Enum.each(fn {to_pid, text} -> + send(to_pid, %Event.Display{output: text}) + end) + + context + end +end