defmodule Odinsea.World.Family do @moduledoc """ Family management service. Ported from src/handling/world/family/MapleFamily.java Manages family trees with senior/junior relationships. Supports family blessings, reputation, and pedigree tracking. """ use GenServer require Logger alias Odinsea.Database.Repo import Ecto.Query # ============================================================================ # Data Structures # ============================================================================ defmodule FamilyCharacter do @moduledoc "Family member representation" defstruct [ :id, :name, :level, :job, :channel, :senior_id, :junior1_id, :junior2_id, :current_rep, :total_rep, :online, :pedigree, :descendants ] end # ============================================================================ # Client API # ============================================================================ def start_link(_) do GenServer.start_link(__MODULE__, [], name: __MODULE__) end @doc """ Creates a new family with the given leader. Returns {:ok, family_id} on success, {:error, reason} on failure. """ def create_family(leader_id) do GenServer.call(__MODULE__, {:create_family, leader_id}) end @doc """ Gets a family by ID. """ def get_family(family_id) do GenServer.call(__MODULE__, {:get_family, family_id}) end @doc """ Gets family by character ID. """ def get_family_by_character(character_id) do GenServer.call(__MODULE__, {:get_family_by_character, character_id}) end @doc """ Adds a family member (junior to a senior). """ def add_junior(family_id, senior_id, junior_character) do GenServer.call(__MODULE__, {:add_junior, family_id, senior_id, junior_character}) end @doc """ Removes a junior relationship. """ def remove_junior(family_id, senior_id, junior_id) do GenServer.call(__MODULE__, {:remove_junior, family_id, senior_id, junior_id}) end @doc """ Removes a senior relationship (character becomes leader of new family). """ def remove_senior(family_id, character_id) do GenServer.call(__MODULE__, {:remove_senior, family_id, character_id}) end @doc """ Leaves family completely. """ def leave_family(family_id, character_id) do GenServer.call(__MODULE__, {:leave_family, family_id, character_id}) end @doc """ Sets family notice. """ def set_notice(family_id, notice, leader_id) do GenServer.call(__MODULE__, {:set_notice, family_id, notice, leader_id}) end @doc """ Sets member online status. """ def set_online(family_id, character_id, online, channel) do GenServer.call(__MODULE__, {:set_online, family_id, character_id, online, channel}) end @doc """ Updates member info. """ def update_member(family_id, character) do GenServer.call(__MODULE__, {:update_member, family_id, character}) end @doc """ Gains reputation for a member. """ def gain_rep(family_id, character_id, amount, senior_old_level, senior_name) do GenServer.call(__MODULE__, {:gain_rep, family_id, character_id, amount, senior_old_level, senior_name}) end @doc """ Merges two families (old into new). Called when a character with juniors joins as a junior. """ def merge_families(new_family_id, old_family_id) do GenServer.call(__MODULE__, {:merge_families, new_family_id, old_family_id}) end @doc """ Disbands a family. """ def disband_family(family_id) do GenServer.call(__MODULE__, {:disband_family, family_id}) end @doc """ Broadcasts to family members. """ def broadcast(family_id, packet, recipient_ids \\ nil) do GenServer.cast(__MODULE__, {:broadcast, family_id, packet, recipient_ids}) end @doc """ Gets pedigree for a character (all related family members). """ def get_pedigree(family_id, character_id) do GenServer.call(__MODULE__, {:get_pedigree, family_id, character_id}) end @doc """ Gets all juniors recursively. """ def get_all_juniors(family_id, character_id) do GenServer.call(__MODULE__, {:get_all_juniors, family_id, character_id}) end @doc """ Gets online juniors (self + direct juniors + their juniors). """ def get_online_juniors(family_id, character_id) do GenServer.call(__MODULE__, {:get_online_juniors, family_id, character_id}) end # ============================================================================ # Server Callbacks # ============================================================================ @impl true def init(_) do # Load families from database families = load_families_from_db() Logger.info("Family service initialized with #{map_size(families)} families") {:ok, %{families: families}} end @impl true def handle_call({:create_family, leader_id}, _from, state) do case create_family_in_db(leader_id) do {:ok, family_id} -> family = %{ id: family_id, leader_id: leader_id, leader_name: nil, # Will be set when first member is added notice: "", members: %{} } new_state = %{state | families: Map.put(state.families, family_id, family)} Logger.info("Family #{family_id} created with leader #{leader_id}") {:reply, {:ok, family_id}, new_state} {:error, reason} -> {:reply, {:error, reason}, state} end end @impl true def handle_call({:get_family, family_id}, _from, state) do {:reply, Map.get(state.families, family_id), state} end @impl true def handle_call({:get_family_by_character, character_id}, _from, state) do family = state.families |> Map.values() |> Enum.find(fn f -> Map.has_key?(f.members, character_id) end) {:reply, family, state} end @impl true def handle_call({:add_junior, family_id, senior_id, junior}, _from, state) do case Map.get(state.families, family_id) do nil -> {:reply, {:error, :family_not_found}, state} family -> senior = Map.get(family.members, senior_id) cond do not senior -> {:reply, {:error, :senior_not_found}, state} senior.junior1_id != 0 && senior.junior2_id != 0 -> {:reply, {:error, :senior_has_max_juniors}, state} Map.has_key?(family.members, junior.id) -> {:reply, {:error, :already_in_family}, state} true -> # Create junior character junior_char = %FamilyCharacter{ id: junior.id, name: junior.name, level: junior.level, job: junior.job, channel: junior.channel_id || 1, senior_id: senior_id, junior1_id: 0, junior2_id: 0, current_rep: junior.current_rep || 0, total_rep: junior.total_rep || 0, online: true, pedigree: [], descendants: 0 } # Update senior's junior slot updated_senior = if senior.junior1_id == 0 do %{senior | junior1_id: junior.id} else %{senior | junior2_id: junior.id} end members = family.members |> Map.put(senior_id, updated_senior) |> Map.put(junior.id, junior_char) # Check if this is the first member (leader) leader_name = if map_size(family.members) == 0 do junior.name else family.leader_name end updated_family = %{family | members: members, leader_name: leader_name || family.leader_name } # Recalculate pedigree for affected members updated_family = recalculate_pedigrees(updated_family) # Save to database save_family_member_to_db(family_id, junior_char) update_member_in_db(senior_id, updated_senior) # Broadcast broadcast_family_joined(updated_family, junior) {:reply, {:ok, junior_char}, %{state | families: Map.put(state.families, family_id, updated_family)}} end end end @impl true def handle_call({:remove_junior, family_id, senior_id, junior_id}, _from, state) do case Map.get(state.families, family_id) do nil -> {:reply, {:error, :family_not_found}, state} family -> senior = Map.get(family.members, senior_id) junior = Map.get(family.members, junior_id) cond do not senior -> {:reply, {:error, :senior_not_found}, state} senior.junior1_id != junior_id && senior.junior2_id != junior_id -> {:reply, {:error, :not_junior}, state} true -> # Update senior's junior slot updated_senior = if senior.junior1_id == junior_id do %{senior | junior1_id: 0} else %{senior | junior2_id: 0} end # Junior becomes leader of new family # Get all juniors of the removed junior juniors_family = get_all_juniors_list(family, junior_id) # Create new family for the split {:ok, new_family_id} = create_family_in_db(junior_id) # Move juniors to new family {remaining_members, moved_members} = Enum.split_with(family.members, fn {id, _} -> id in juniors_family || id == junior_id end) # Update junior (now leader) updated_junior = %{junior | senior_id: 0} new_family_members = Map.new([{junior_id, updated_junior} | moved_members]) new_family = %{ id: new_family_id, leader_id: junior_id, leader_name: junior.name, notice: "", members: new_family_members } # Update old family old_family_members = Map.new([{senior_id, updated_senior} | remaining_members]) updated_family = %{family | members: old_family_members} updated_family = recalculate_pedigrees(updated_family) # Save to database update_member_in_db(senior_id, updated_senior) move_members_to_new_family(junior_id, new_family_id, moved_members) # Check if old family should disband (less than 2 members) final_state = if map_size(updated_family.members) < 2 do disband_family_in_db(family_id) broadcast_family_disband(updated_family) %{state | families: Map.delete(state.families, family_id)} else %{state | families: Map.put(state.families, family_id, updated_family)} end # Add new family to state final_state = %{final_state | families: Map.put(final_state.families, new_family_id, new_family)} {:reply, {:ok, new_family_id}, final_state} end end end @impl true def handle_call({:remove_senior, family_id, character_id}, _from, state) do case Map.get(state.families, family_id) do nil -> {:reply, {:error, :family_not_found}, state} family -> character = Map.get(family.members, character_id) if not character || character.senior_id == 0 do {:reply, {:error, :no_senior}, state} else senior = Map.get(family.members, character.senior_id) # Update senior's junior slot updated_senior = if senior.junior1_id == character_id do %{senior | junior1_id: 0} else %{senior | junior2_id: 0} end # Character becomes leader of new family {:ok, new_family_id} = create_family_in_db(character_id) # Get character's juniors juniors_family = get_all_juniors_list(family, character_id) # Move character and juniors to new family {remaining_members, moved_members} = Enum.split_with(family.members, fn {id, _} -> not (id in juniors_family || id == character_id) end) # Update character (now leader) updated_character = %{character | senior_id: 0} new_family_members = Map.new([{character_id, updated_character} | moved_members]) new_family = %{ id: new_family_id, leader_id: character_id, leader_name: character.name, notice: "", members: new_family_members } # Update old family old_family_members = Map.new([{senior.id, updated_senior} | remaining_members]) updated_family = %{family | members: old_family_members} updated_family = recalculate_pedigrees(updated_family) # Save to database update_member_in_db(senior.id, updated_senior) move_members_to_new_family(character_id, new_family_id, moved_members) # Check if old family should disband final_state = if map_size(updated_family.members) < 2 do disband_family_in_db(family_id) broadcast_family_disband(updated_family) %{state | families: Map.delete(state.families, family_id)} else %{state | families: Map.put(state.families, family_id, updated_family)} end # Add new family to state final_state = %{final_state | families: Map.put(final_state.families, new_family_id, new_family)} {:reply, {:ok, new_family_id}, final_state} end end end @impl true def handle_call({:leave_family, family_id, character_id}, _from, state) do case Map.get(state.families, family_id) do nil -> {:reply, {:error, :family_not_found}, state} family -> character = Map.get(family.members, character_id) if not character do {:reply, {:error, :not_in_family}, state} else # If leader leaves, disband the family if character_id == family.leader_id do # Disband everyone disband_family_in_db(family_id) broadcast_family_disband(family) {:reply, :ok, %{state | families: Map.delete(state.families, family_id)}} else # Handle juniors members = family.members # Juniors become their own family leaders members = if character.junior1_id != 0 do junior1 = Map.get(members, character.junior1_id) if junior1 do updated_junior1 = %{junior1 | senior_id: 0} # Split off junior's branch {:ok, new_family_id} = create_family_in_db(character.junior1_id) juniors_family = get_all_juniors_list(family, character.junior1_id) move_members_to_new_family(character.junior1_id, new_family_id, Enum.map(juniors_family, fn id -> {id, Map.get(members, id)} end)) # Remove from current family Map.delete(members, character.junior1_id) |> Map.put(character.junior1_id, updated_junior1) else members end else members end members = if character.junior2_id != 0 do junior2 = Map.get(members, character.junior2_id) if junior2 do updated_junior2 = %{junior2 | senior_id: 0} {:ok, new_family_id} = create_family_in_db(character.junior2_id) juniors_family = get_all_juniors_list(family, character.junior2_id) move_members_to_new_family(character.junior2_id, new_family_id, Enum.map(juniors_family, fn id -> {id, Map.get(members, id)} end)) Map.delete(members, character.junior2_id) |> Map.put(character.junior2_id, updated_junior2) else members end else members end # Update senior members = if character.senior_id != 0 do senior = Map.get(members, character.senior_id) if senior do updated_senior = if senior.junior1_id == character_id do %{senior | junior1_id: 0} else %{senior | junior2_id: 0} end Map.put(members, character.senior_id, updated_senior) else members end else members end # Remove character members = Map.delete(members, character_id) # Check if family should disband if map_size(members) < 2 do disband_family_in_db(family_id) broadcast_family_disband(%{family | members: members}) {:reply, :ok, %{state | families: Map.delete(state.families, family_id)}} else updated_family = %{family | members: members} updated_family = recalculate_pedigrees(updated_family) remove_family_member_from_db(family_id, character_id) {:reply, :ok, %{state | families: Map.put(state.families, family_id, updated_family)}} end end end end end @impl true def handle_call({:set_notice, family_id, notice, leader_id}, _from, state) do case Map.get(state.families, family_id) do nil -> {:reply, {:error, :family_not_found}, state} %{leader_id: actual_leader} when actual_leader != leader_id -> {:reply, {:error, :not_leader}, state} family -> updated_family = %{family | notice: String.slice(notice, 0, 255)} update_family_notice_in_db(family_id, updated_family.notice) {:reply, :ok, %{state | families: Map.put(state.families, family_id, updated_family)}} end end @impl true def handle_call({:set_online, family_id, character_id, online, channel}, _from, state) do case Map.get(state.families, family_id) do nil -> {:reply, {:error, :family_not_found}, state} family -> case Map.get(family.members, character_id) do nil -> {:reply, {:error, :not_in_family}, state} character -> updated_character = %{character | online: online, channel: if(online, do: channel, else: -1) } members = Map.put(family.members, character_id, updated_character) updated_family = %{family | members: members} # Broadcast login/logout broadcast_member_login(updated_family, character, online) {:reply, :ok, %{state | families: Map.put(state.families, family_id, updated_family)}} end end end @impl true def handle_call({:update_member, family_id, character}, _from, state) do case Map.get(state.families, family_id) do nil -> {:reply, {:error, :family_not_found}, state} family -> case Map.get(family.members, character.id) do nil -> {:reply, {:error, :not_in_family}, state} existing -> updated_character = %{existing | level: character.level || existing.level, job: character.job || existing.job, channel: character.channel_id || existing.channel } members = Map.put(family.members, character.id, updated_character) updated_family = %{family | members: members} # Broadcast level/job change if existing.level != updated_character.level do broadcast_member_levelup(updated_family, updated_character) end if existing.job != updated_character.job do broadcast_member_jobchange(updated_family, updated_character) end {:reply, :ok, %{state | families: Map.put(state.families, family_id, updated_family)}} end end end @impl true def handle_call({:gain_rep, family_id, character_id, amount, senior_old_level, senior_name}, _from, state) do case Map.get(state.families, family_id) do nil -> {:reply, {:error, :family_not_found}, state} family -> case Map.get(family.members, character_id) do nil -> {:reply, {:error, :not_in_family}, state} character -> # Reduce rep if senior is higher level adjusted_amount = if senior_old_level > character.level do div(amount, 2) else amount end updated_character = %{character | current_rep: character.current_rep + adjusted_amount, total_rep: character.total_rep + adjusted_amount } members = Map.put(family.members, character_id, updated_character) updated_family = %{family | members: members} # Broadcast rep change broadcast_rep_change(updated_family, adjusted_amount, senior_name, character_id) {:reply, {:ok, character.senior_id}, %{state | families: Map.put(state.families, family_id, updated_family)}} end end end @impl true def handle_call({:merge_families, new_family_id, old_family_id}, _from, state) do new_family = Map.get(state.families, new_family_id) old_family = Map.get(state.families, old_family_id) if not new_family or not old_family do {:reply, {:error, :family_not_found}, state} else # Merge old family's members into new family merged_members = Map.merge(new_family.members, old_family.members) # Update family IDs for all old members merged_members = Enum.map(merged_members, fn {id, member} -> if Map.has_key?(old_family.members, id) do {id, %{member | family_id: new_family_id}} else {id, member} end end) |> Map.new() updated_family = %{new_family | members: merged_members} updated_family = recalculate_pedigrees(updated_family) # Move members in database merge_families_in_db(new_family_id, old_family_id, Map.keys(old_family.members)) # Disband old family disband_family_in_db(old_family_id) new_state = state |> put_in([:families, new_family_id], updated_family) |> Map.update!(:families, fn families -> Map.delete(families, old_family_id) end) Logger.info("Family #{old_family_id} merged into #{new_family_id}") {:reply, :ok, new_state} end end @impl true def handle_call({:disband_family, family_id}, _from, state) do case Map.get(state.families, family_id) do nil -> {:reply, {:error, :family_not_found}, state} family -> # Notify all members broadcast_family_disband(family) # Clear family from database disband_family_in_db(family_id) Logger.info("Family #{family_id} disbanded") {:reply, :ok, %{state | families: Map.delete(state.families, family_id)}} end end @impl true def handle_call({:get_pedigree, family_id, character_id}, _from, state) do case Map.get(state.families, family_id) do nil -> {:reply, [], state} family -> pedigree = calculate_pedigree(family, character_id) {:reply, pedigree, state} end end @impl true def handle_call({:get_all_juniors, family_id, character_id}, _from, state) do case Map.get(state.families, family_id) do nil -> {:reply, [], state} family -> juniors = get_all_juniors_list(family, character_id) {:reply, juniors, state} end end @impl true def handle_call({:get_online_juniors, family_id, character_id}, _from, state) do case Map.get(state.families, family_id) do nil -> {:reply, [], state} family -> character = Map.get(family.members, character_id) if not character do {:reply, [], state} else online = [character_id] # Direct juniors online = if character.junior1_id != 0 do junior1 = Map.get(family.members, character.junior1_id) if junior1 && junior1.online do [character.junior1_id | online] else online end else online end online = if character.junior2_id != 0 do junior2 = Map.get(family.members, character.junior2_id) if junior2 && junior2.online do [character.junior2_id | online] else online end else online end # Juniors' juniors online = if character.junior1_id != 0 do junior1 = Map.get(family.members, character.junior1_id) if junior1 do junior1_juniors = [junior1.junior1_id, junior1.junior2_id] |> Enum.filter(&(&1 != 0)) |> Enum.filter(fn id -> m = Map.get(family.members, id) m && m.online end) junior1_juniors ++ online else online end else online end online = if character.junior2_id != 0 do junior2 = Map.get(family.members, character.junior2_id) if junior2 do junior2_juniors = [junior2.junior1_id, junior2.junior2_id] |> Enum.filter(&(&1 != 0)) |> Enum.filter(fn id -> m = Map.get(family.members, id) m && m.online end) junior2_juniors ++ online else online end else online end {:reply, online, state} end end end @impl true def handle_cast({:broadcast, family_id, packet, recipient_ids}, state) do case Map.get(state.families, family_id) do nil -> :ok family -> recipients = if recipient_ids do Enum.filter(family.members, fn {id, m} -> id in recipient_ids && m.online end) else Enum.filter(family.members, fn {_, m} -> m.online end) end Enum.each(recipients, fn {id, _} -> case Registry.lookup(Odinsea.CharacterRegistry, id) do [{pid, _}] -> send(pid, {:send_packet, packet}) [] -> :ok end end) end {:noreply, state} end # ============================================================================ # Helper Functions # ============================================================================ defp get_all_juniors_list(family, character_id) do character = Map.get(family.members, character_id) if not character do [] else juniors = [character_id] juniors = if character.junior1_id != 0 do juniors ++ get_all_juniors_list(family, character.junior1_id) else juniors end juniors = if character.junior2_id != 0 do juniors ++ get_all_juniors_list(family, character.junior2_id) else juniors end juniors end end defp calculate_pedigree(family, character_id) do character = Map.get(family.members, character_id) if not character do [] else pedigree = [character_id] # Add senior and senior's relatives pedigree = if character.senior_id != 0 do senior = Map.get(family.members, character.senior_id) if senior do pedigree = [character.senior_id | pedigree] # Senior's senior pedigree = if senior.senior_id != 0 do [senior.senior_id | pedigree] else pedigree end # Senior's other junior other_junior = if senior.junior1_id == character_id do senior.junior2_id else senior.junior1_id end if other_junior != 0 do [other_junior | pedigree] else pedigree end else pedigree end else pedigree end # Add juniors and their juniors pedigree = if character.junior1_id != 0 do junior1 = Map.get(family.members, character.junior1_id) if junior1 do pedigree = pedigree ++ [character.junior1_id] if junior1.junior1_id != 0 do pedigree ++ [junior1.junior1_id] else pedigree end |> then(fn p -> if junior1.junior2_id != 0 do p ++ [junior1.junior2_id] else p end end) else pedigree end else pedigree end pedigree = if character.junior2_id != 0 do junior2 = Map.get(family.members, character.junior2_id) if junior2 do pedigree = pedigree ++ [character.junior2_id] if junior2.junior1_id != 0 do pedigree ++ [junior2.junior1_id] else pedigree end |> then(fn p -> if junior2.junior2_id != 0 do p ++ [junior2.junior2_id] else p end end) else pedigree end else pedigree end pedigree end end defp recalculate_pedigrees(family) do members = Enum.map(family.members, fn {id, member} -> pedigree = calculate_pedigree(family, id) descendants = count_descendants(family, id) {id, %{member | pedigree: pedigree, descendants: descendants}} end) |> Map.new() %{family | members: members} end defp count_descendants(family, character_id) do character = Map.get(family.members, character_id) if not character do 0 else count = 0 count = if character.junior1_id != 0 do count + 1 + count_descendants(family, character.junior1_id) else count end count = if character.junior2_id != 0 do count + 1 + count_descendants(family, character.junior2_id) else count end count end end # ============================================================================ # Database Functions (Stub implementations) # ============================================================================ defp load_families_from_db do # TODO: Implement actual database loading %{} end defp create_family_in_db(_leader_id) do # TODO: Implement database insert {:ok, System.unique_integer([:positive])} end defp save_family_member_to_db(_family_id, _member) do # TODO: Implement :ok end defp update_member_in_db(_character_id, _member) do # TODO: Implement :ok end defp move_members_to_new_family(_new_leader_id, _new_family_id, _members) do # TODO: Implement :ok end defp merge_families_in_db(_new_family_id, _old_family_id, _member_ids) do # TODO: Implement :ok end defp remove_family_member_from_db(_family_id, _character_id) do # TODO: Implement :ok end defp update_family_notice_in_db(_family_id, _notice) do # TODO: Implement :ok end defp disband_family_in_db(_family_id) do # TODO: Implement :ok end # ============================================================================ # Broadcast Functions # ============================================================================ defp broadcast_family_joined(family, junior) do Logger.debug("Broadcast family join for #{junior.name} to family #{family.id}") end defp broadcast_family_disband(family) do Logger.debug("Broadcast family disband for family #{family.id}") end defp broadcast_member_login(family, character, online) do Logger.debug("Broadcast family member #{character.name} login=#{online} to family #{family.id}") end defp broadcast_member_levelup(family, character) do Logger.debug("Broadcast family member #{character.name} levelup to family #{family.id}") end defp broadcast_member_jobchange(family, character) do Logger.debug("Broadcast family member #{character.name} job change to family #{family.id}") end defp broadcast_rep_change(family, amount, name, character_id) do Logger.debug("Broadcast family rep change #{amount} for #{name} in family #{family.id}") end end