# `GameServer.Hooks`
[🔗](https://github.com/appsinacup/game_server/blob/v1.0.26/lib/game_server/hooks.ex#L1)

Behaviour for GameServer hooks/callbacks.

Implement this behaviour in your hooks module to receive lifecycle events
from the GameServer and run custom game logic.

## Setup

1. Create a module implementing this behaviour
2. Configure it in your GameServer instance

## Example

    defmodule MyGame.Hooks do
      @behaviour GameServer.Hooks

      @impl true
      def after_user_register(user) do
        # Give new users starting coins
        GameServer.Accounts.update_user(user, %{
          metadata: Map.put(user.metadata || %{}, "coins", 100)
        })
      end

      @impl true
      def after_user_login(user) do
        # Log login
        :ok
      end

      @impl true
      def after_user_updated(user) do
        # React to user profile changes (e.g., sync display name to external system)
        :ok
      end

      @impl true
      def before_user_update(_user, attrs) do
        # Validate or modify user update attributes before they are applied
        # Return {:ok, attrs} to allow, {:error, reason} to block
        {:ok, attrs}
      end

      # Lobby hooks
      @impl true
      def before_lobby_create(attrs) do
        # Validate or modify lobby creation attributes
        {:ok, attrs}
      end

      @impl true
      def after_lobby_create(_lobby), do: :ok

      @impl true
      def before_lobby_join(user, lobby, opts) do
        # Check if user can join (e.g., level requirements)
        {:ok, {user, lobby, opts}}
      end

      @impl true
      def before_group_create(user, attrs) do
        # Check if user can create a group (e.g., enough coins in metadata)
        coins = get_in(user.metadata, ["coins"]) || 0
        if coins >= 50 do
          {:ok, attrs}
        else
          {:error, :not_enough_coins}
        end
      end

      @impl true
      def before_group_join(user, group, opts) do
        # Check if user can join group (e.g., level requirements based on metadata)
        {:ok, {user, group, opts}}
      end

      @impl true
      def after_lobby_join(_user, _lobby), do: :ok

      @impl true
      def before_lobby_leave(user, lobby) do
        {:ok, {user, lobby}}
      end

      @impl true
      def after_lobby_leave(_user, _lobby), do: :ok

      @impl true
      def before_lobby_update(_lobby, attrs) do
        {:ok, attrs}
      end

      @impl true
      def after_lobby_update(_lobby), do: :ok

      @impl true
      def before_lobby_delete(lobby) do
        {:ok, lobby}
      end

      @impl true
      def after_lobby_delete(_lobby), do: :ok

      @impl true
      def before_user_kicked(host, target, lobby) do
        {:ok, {host, target, lobby}}
      end

      @impl true
      def after_user_kicked(_host, _target, _lobby), do: :ok

      @impl true
      def after_lobby_host_change(_lobby, _new_host_id), do: :ok

      # Custom RPC handlers - define your own functions!
      # These are called from game clients via the RPC channel.
      #
      # def give_coins(amount, opts) do
      #   caller = Keyword.get(opts, :caller)
      #   # Update user's coins...
      #   {:ok, %{new_balance: 150}}
      # end
    end

## Hook Types

### User Lifecycle Hooks

- `after_user_register/1` - Called after a new user registers
- `after_user_login/1` - Called after a user logs in
- `after_user_updated/1` - Called after a user is updated (fire-and-forget)
- `after_user_online/1` - Called after a user comes online (fire-and-forget)
- `after_user_offline/1` - Called after a user goes offline (fire-and-forget)

### Lobby Lifecycle Hooks

Before hooks can block operations by returning `{:error, reason}`.
After hooks are fire-and-forget.

- `before_lobby_create/1` - Before lobby creation, receives attrs map
- `after_lobby_create/1` - After lobby is created
- `before_lobby_join/3` - Before user joins lobby
- `after_lobby_join/2` - After user joins lobby
- `before_group_create/2` - Before group creation, receives `(user, attrs)`. Return `{:ok, attrs}` to allow or `{:error, reason}` to block
- `after_group_create/1` - After group is created (fire-and-forget)
- `before_group_join/3` - Before user is accepted into a group (public join, invite accept, or request approval)
- `before_group_update/2` - Before group update, receives `(group, attrs)`. Return `{:ok, attrs}` to allow or `{:error, reason}` to block
- `after_group_update/1` - After group is updated (fire-and-forget)
- `after_group_join/2` - After a user joins a group (fire-and-forget), receives `(user_id, group)`
- `after_group_leave/2` - After a user leaves a group (fire-and-forget), receives `(user_id, group_id)`
- `after_group_delete/1` - After a group is deleted (fire-and-forget), receives `(group)`
- `after_group_kick/3` - After a member is kicked from a group (fire-and-forget), receives `(admin_id, target_id, group_id)`
- `before_party_create/2` - Before party creation, receives `(user, attrs)`. Return `{:ok, attrs}` to allow or `{:error, reason}` to block
- `after_party_create/1` - After party is created (fire-and-forget)
- `before_party_update/2` - Before party update, receives `(party, attrs)`. Return `{:ok, attrs}` to allow or `{:error, reason}` to block
- `after_party_update/1` - After party is updated (fire-and-forget)
- `after_party_join/2` - After a user joins a party via invite accept (fire-and-forget), receives `(user, party)`
- `after_party_leave/2` - After a user leaves a party (fire-and-forget), receives `(user, party_id)`
- `after_party_kick/3` - After a member is kicked from a party (fire-and-forget), receives `(target, leader, party)`
- `after_party_disband/1` - After a party is disbanded (fire-and-forget), receives `(party)`
- `after_achievement_unlocked/2` - After an achievement is unlocked (fire-and-forget), receives `(user_id, achievement)`
- `before_chat_message/2` - Before a chat message is sent, receives `(user, attrs)`. Return `{:ok, attrs}` to allow (and optionally modify), or `{:error, reason}` to block
- `after_chat_message/1` - After a chat message is persisted (fire-and-forget)
- `before_lobby_leave/2` - Before user leaves lobby
- `after_lobby_leave/2` - After user leaves lobby
- `before_lobby_update/2` - Before lobby is updated
- `after_lobby_update/1` - After lobby is updated
- `before_lobby_delete/1` - Before lobby is deleted
- `after_lobby_delete/1` - After lobby is deleted
- `before_user_kicked/3` - Before user is kicked from lobby
- `after_user_kicked/3` - After user is kicked from lobby
- `after_lobby_host_change/2` - After lobby host changes
- `before_kv_get/2` - Called before a client KV `get` to return a KV access decision such as `:public`, `:owner_only`, or `:server_only`

  ## Custom RPC Functions

  Game clients can call RPCs exposed by your hooks module in two ways:

  1. **Exported functions**: any exported function in your hooks module (other
    than the callbacks above) can be called from game clients.

  2. **Dynamic functions (custom hooks)**: your `after_startup/0` callback may
    return a list of dynamic exports describing additional callable function
    names that do **not** need to exist as exported Elixir functions.
    These dynamic calls are dispatched to `on_custom_hook/2`.

    # Client calls: rpc("give_coins", {amount: 50})
    def give_coins(amount, opts) do
      caller = Keyword.get(opts, :caller)
      # Your game logic here
      {:ok, %{success: true}}
    end

Return values:
- `{:ok, data}` - Success, data is sent back to client
- `{:error, reason}` - Error, reason is sent back to client
- `:ok` - Success with no data

# `group`

```elixir
@type group() :: GameServer.Groups.Group.t()
```

A group struct from GameServer.Groups.Group

# `hook_result`

```elixir
@type hook_result(t) :: {:ok, t} | {:error, term()}
```

Result type for before hooks

# `kv_access`

```elixir
@type kv_access() ::
  :public
  | :owner_only
  | :lobby_members_only
  | :owner_or_lobby_member
  | :admin_only
  | :server_only
```

Client KV API access decision returned by `before_kv_get/2`.

# `kv_access_result`

```elixir
@type kv_access_result() :: kv_access() | {:ok, kv_access()} | {:error, term()}
```

# `kv_opts`

```elixir
@type kv_opts() :: map() | keyword()
```

Options passed to hooks that accept an options map/keyword list.

Common keys include `:user_id`, `:lobby_id`, and other domain-specific options.
Hooks may accept either a map or keyword list for convenience.

# `lobby`

```elixir
@type lobby() :: GameServer.Lobbies.Lobby.t()
```

A lobby struct from GameServer.Lobbies.Lobby

# `message`

```elixir
@type message() :: GameServer.Chat.Message.t()
```

A chat message struct from GameServer.Chat.Message

# `party`

```elixir
@type party() :: GameServer.Parties.Party.t()
```

A party struct from GameServer.Parties.Party

# `rpc_export`

```elixir
@type rpc_export() :: %{:hook =&gt; String.t(), optional(:meta) =&gt; map()}
```

A dynamic RPC export returned by `after_startup/0`.

The minimal shape is:

    %{hook: "custom_hello"}

Optionally, provide `:meta` for tooling / UI hints:

    %{hook: "custom_hello", meta: %{description: "...", args: [...], example_args: [...]}}

Note: the runtime also accepts string keys (e.g. `%{"hook" => ...}`), but
this type focuses on the atom-keyed form.

# `user`

```elixir
@type user() :: GameServer.Accounts.User.t()
```

A user struct from GameServer.Accounts.User

# `after_achievement_unlocked`
*optional* 

```elixir
@callback after_achievement_unlocked(integer(), map()) :: any()
```

# `after_chat_message`
*optional* 

```elixir
@callback after_chat_message(message()) :: any()
```

# `after_group_create`
*optional* 

```elixir
@callback after_group_create(group()) :: any()
```

# `after_group_delete`
*optional* 

```elixir
@callback after_group_delete(group()) :: any()
```

# `after_group_join`
*optional* 

```elixir
@callback after_group_join(integer(), group()) :: any()
```

# `after_group_kick`
*optional* 

```elixir
@callback after_group_kick(integer(), integer(), integer()) :: any()
```

# `after_group_leave`
*optional* 

```elixir
@callback after_group_leave(integer(), integer()) :: any()
```

# `after_group_update`
*optional* 

```elixir
@callback after_group_update(group()) :: any()
```

# `after_lobby_create`

```elixir
@callback after_lobby_create(lobby()) :: any()
```

# `after_lobby_delete`

```elixir
@callback after_lobby_delete(lobby()) :: any()
```

# `after_lobby_host_change`

```elixir
@callback after_lobby_host_change(lobby(), new_host_id :: integer()) :: any()
```

# `after_lobby_join`

```elixir
@callback after_lobby_join(user(), lobby()) :: any()
```

# `after_lobby_leave`

```elixir
@callback after_lobby_leave(user(), lobby()) :: any()
```

# `after_lobby_update`

```elixir
@callback after_lobby_update(lobby()) :: any()
```

# `after_party_create`
*optional* 

```elixir
@callback after_party_create(party()) :: any()
```

# `after_party_disband`
*optional* 

```elixir
@callback after_party_disband(party()) :: any()
```

# `after_party_join`
*optional* 

```elixir
@callback after_party_join(user(), party()) :: any()
```

# `after_party_kick`
*optional* 

```elixir
@callback after_party_kick(user(), user(), party()) :: any()
```

# `after_party_leave`
*optional* 

```elixir
@callback after_party_leave(user(), integer()) :: any()
```

# `after_party_update`
*optional* 

```elixir
@callback after_party_update(party()) :: any()
```

# `after_startup`

```elixir
@callback after_startup() :: :ok | [rpc_export()]
```

# `after_user_kicked`

```elixir
@callback after_user_kicked(host :: user(), target :: user(), lobby()) :: any()
```

# `after_user_login`

```elixir
@callback after_user_login(user()) :: any()
```

# `after_user_offline`

```elixir
@callback after_user_offline(user()) :: any()
```

# `after_user_online`

```elixir
@callback after_user_online(user()) :: any()
```

# `after_user_register`

```elixir
@callback after_user_register(user()) :: any()
```

# `after_user_updated`

```elixir
@callback after_user_updated(user()) :: any()
```

# `before_chat_message`
*optional* 

```elixir
@callback before_chat_message(user(), attrs :: map()) :: hook_result(map())
```

# `before_group_create`
*optional* 

```elixir
@callback before_group_create(user(), map()) :: hook_result(map())
```

# `before_group_join`
*optional* 

```elixir
@callback before_group_join(user(), group(), opts :: map()) ::
  hook_result({user(), group(), map()})
```

# `before_group_update`
*optional* 

```elixir
@callback before_group_update(group(), map()) :: hook_result(map())
```

# `before_kv_get`

```elixir
@callback before_kv_get(String.t(), kv_opts()) :: kv_access_result()
```

Called before a KV `get/2` is performed. Implementations should return
one of these client KV API access decisions:

- `:public` — any authenticated client can read.
- `:owner_only` — only the caller matching the requested `user_id` can read.
- `:lobby_members_only` — only callers in the requested `lobby_id` can read.
- `:owner_or_lobby_member` — caller may match either requested `user_id` or `lobby_id`.
- `:admin_only` — only admins can read through the client KV API.
- `:server_only` — no client KV reads.

Server-side `GameServer.KV.get/2` calls are unaffected.

Receives the `key` and an `opts` map/keyword (see `t:kv_opts/0`). Return
either the bare atom (e.g. `:public`) or `{:ok, :public}`; return `{:error, reason}`
to block the read.

# `before_lobby_create`

```elixir
@callback before_lobby_create(attrs :: map()) :: hook_result(map())
```

# `before_lobby_delete`

```elixir
@callback before_lobby_delete(lobby()) :: hook_result(lobby())
```

# `before_lobby_join`

```elixir
@callback before_lobby_join(user(), lobby(), opts :: keyword()) ::
  hook_result({user(), lobby(), keyword()})
```

# `before_lobby_leave`

```elixir
@callback before_lobby_leave(user(), lobby()) :: hook_result({user(), lobby()})
```

# `before_lobby_update`

```elixir
@callback before_lobby_update(lobby(), attrs :: map()) :: hook_result(map())
```

# `before_party_create`
*optional* 

```elixir
@callback before_party_create(user(), map()) :: hook_result(map())
```

# `before_party_update`
*optional* 

```elixir
@callback before_party_update(party(), map()) :: hook_result(map())
```

# `before_stop`

```elixir
@callback before_stop() :: any()
```

# `before_user_kicked`

```elixir
@callback before_user_kicked(host :: user(), target :: user(), lobby()) ::
  hook_result({user(), user(), lobby()})
```

# `before_user_update`
*optional* 

```elixir
@callback before_user_update(user(), attrs :: map()) :: hook_result(map())
```

# `on_custom_hook`

```elixir
@callback on_custom_hook(String.t(), list()) :: any()
```

Handle a dynamically-exported RPC function.

This callback is invoked when a client calls a function name that was
registered at runtime from `after_startup/0`.

Receives the function name and the argument list.

# `__using__`
*macro* 

Use this macro to get default implementations for all callbacks.

This allows you to only implement the callbacks you need.

## Example

    defmodule MyGame.Hooks do
      use GameServer.Hooks

      @impl true
      def after_user_register(user) do
        # Only implement what you need
        :ok
      end
    end

# `caller`

```elixir
@spec caller() :: any() | nil
```

Returns the raw caller value for the current hook invocation.

When GameServer executes a hook function, it may inject a `:caller` into the
hook task's process dictionary. This helper fetches that raw value.

# `caller_id`

```elixir
@spec caller_id() :: integer() | nil
```

Returns the caller's numeric id when available.

If the caller is a `GameServer.Accounts.User` struct, returns its `id`.
If the caller is a map, returns `:id` or `"id"`.
If the caller is already an integer, returns it.
Otherwise returns `nil`.

# `caller_user`

```elixir
@spec caller_user() :: GameServer.Accounts.User.t() | nil
```

Returns the user struct for the current caller when available.

This is a convenience wrapper over `caller/0` that returns a user struct when
the caller is already a `%GameServer.Accounts.User{}`.

In the full GameServer application this may also resolve ids/maps via the DB.
In the SDK it only returns the struct when it is already present.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
