%%%-----------------------------------
%%% module  : lib_scene
%%% @author  : xyao
%%% email   : jiexiaowen@gmail.com
%%% Created : 2010.05.08
%%% description: 场景信息
%%%-----------------------------------
-module(lib_scene).
-export([
        user_move/10,
        change_scene/5,
        change_scene/6,
        change_scene/7,
        change_scene/8,
        change_scene/9,
        change_player_scene/5,
        change_player_scene/7,
        change_player_scene/8,
        change_scene_queue/7,
        change_player_scene_queue/7,
        player_change_default_scene/2,
        player_change_relogin_scene/2,
        change_default_scene/2,
        change_relogin_scene/2,
        notify_fin_change_scene/1,
        change_speed/8,
        leave_scene/1,
        leave_scene/3,
        % enter_scene/1,
        % send_scene_info_to_uid/6,
        % revive_to_scene/2,
        get_data/1,
        change_dynamic_eff/4,
        is_safe/4,
        is_safe_scene/1,
        is_outside_scene/1,
        is_clusters_scene/1,
        check_enter/4,
        check_enter/6,
        check_requirement/2,
        check_enter_lv/2,
        is_enough_lv_enter/2,
        check_need_ai_scene/1,
        trigger_mon_ai/5,
        check_need_patrol_scene/1,
        is_dungeon_scene/1,
        is_boss_scene/1,
        is_transferable/1,                      % 是否可以传送
        get_scene_name/1,
        get_born_xy/1,
        repair_xy/4,
        logout_repair_xy/1,
        repair_to_default/1,
        is_broadcast_scene/1,
        is_force_broadcast_scene/1,
        change_scene_handler/4,
        get_create_role_position/0,
        get_main_city_x_y/0,
        is_in_main_city_scene/1,
        is_in_normal_and_outside/1,
        is_can_join_common_act/4,
        %% change_population/2,
        return_scene_user_tps/1,
        clear_scene_room/3,
        clear_scene/2,
        broadcast_player_attr/2,                % 广播主角属性变化
        broadcast_player_attr/7,

        change_act_label/3,
        change_scene_act_label/3,
        broadcast_player_act_label/2,
        broadcast_player_act_label/7,

        broadcast_player_figure/1,
        broadcast_player_scene_info/2,

        is_guild_scene/1,
        is_in_normal_and_outside_and_guild/1,
        is_near/6,                              % 是否在附近
        get_scene_type/1,                       % 获取场景类型
        setup_attr_tired/5,                     % 根据玩家id同步疲劳值
        setup_attr_tired/3                      % 根据玩家ps同步疲劳值
        ,kick_players_out_scene/3               % 将所有玩家踢出场景，回到上一个场景
        ,kick_players_out_scene/4
        ,is_broadcast_enter/1                   % 是否需要全场景广播进入的场景
        ,get_role_enter_scene_setting/2         % 进新场景修正pk状态
    ]).
-export([
        get_room_list/2
        ,get_scene_max_role/1
        ,get_room_max_role/1
        ,get_room_max_role/2
        ,get_role_count/1                       % 获取某场景id下所有场景进程内角色数量
        ,get_role_count/2                       % 获取某场景进程下角色数量
        ,get_pool_role_count/1                  % 获取某场景全部线路角色数量信息
        ,get_pool_role_count/2                  % 获取某场景全部线路角色数量信息
        ,is_center_city/1
        ,center_city_map/1
        ,check_can_active_switch/1
        ,node_disconnect_kickout/1              % 节点断开与跨服的连接，踢出跨服场景的玩家
    ]).
-include("server.hrl").
-include("scene.hrl").
-include("attr.hrl").
-include("battle.hrl").
-include("buff.hrl").
-include("dungeon.hrl").
-include("predefine.hrl").
-include("figure.hrl").
-include("team.hrl").
-include("language.hrl").
-include("common.hrl").
-include("def_fun.hrl").
-include("errcode.hrl").
-include("skill.hrl").
-include("def_event.hrl").
-include("guild.hrl").

%% ----------------------------------------------------------------
%% @doc 玩家移动
%% TargetX, TargetY
%%      当前客户端移动到的新像素坐标
%% OX, OY   玩家原坐标像素（服务端之前记录的数据）
%% Param1, Param2
%%      旧项目原来的情况：
%%        应是客户端下一轨迹点坐标
%%        这个服务端暂时未限制，理论上可能存在外挂以此欺骗其他玩家，
%%        以达成战斗时，A可攻击B，B够不着A的状况。
%%        服务端如果要做下一轨迹点距离限制的话，前端应保证这个轨迹点计算
%%      目前情况由客户端自定义
%% MotionType
%%      移动类型，走路、跳跃、...
%% Param3, Param4
%%      旧项目应为飞行起始点
%%      目前情况由客户端自定义
%% User #ets_scene_user{}
%% ----------------------------------------------------------------
user_move(TargetX, TargetY, OX, OY, Param1, Param2, MotionType, Param3, Param4, User) ->
    #ets_scene_user{
        id = Id,
        scene = SceneId,
        copy_id = CopyId
    } = User,
    {ok, BinData} = pt_120:write(12001, [TargetX, TargetY, Id, MotionType, Param1, Param2, Param3, Param4]),
    case lib_scene:is_broadcast_scene(SceneId) of
        true ->
            lib_scene_agent:send_to_local_scene(User#ets_scene_user.copy_id, BinData);
        false ->
            %% 移除
            {ok, BinData1} = pt_120:write(12004, Id),
            %% 有玩家进入
            {ok, BinData2} = pt_120:write(12003, User),
            %% 以目标区域人数来判断是否广播
            AreaRoleCount = lib_scene_agent:get_area_role_count(lib_scene_calc:get_xy(TargetX, TargetY), CopyId),
            MoveBroadCast = lib_scene_agent:check_move_broadcast(AreaRoleCount, Id),
            lib_scene_calc:move_broadcast(MoveBroadCast, SceneId, CopyId, TargetX, TargetY, OX, OY, BinData, BinData1, BinData2, [User#ets_scene_user.node, User#ets_scene_user.sid])
    end.


%% 游戏内更改玩家场景信息
%% @param IdOrPid        玩家Id或Pid
%% @param SceneId        目标场景ID
%% @param ScenePoolId    场景进程id 默认为0
%% @param CopyId         房间号ID 为0时，普通房间，非0值切换房间。值相同，在同一个房间。
%% @param X              目标场景出生点X
%% @param Y              目标场景出生点Y
%% @param KeyValueList   切换场景同步执行mod_sever_cast:set_data_sub/1 更新 player_status{} 的值
change_player_scene(IdOrPid, SceneId, ScenePoolId, CopyId, KeyValueList) ->
    [X, Y] = get_born_xy(SceneId),
    change_player_scene(IdOrPid, SceneId, ScenePoolId, CopyId, X, Y, KeyValueList).

change_player_scene(IdOrPid, SceneId, ScenePoolId, CopyId, X, Y, KeyValueList) ->
    Pid =
    case is_pid(IdOrPid) of
        true  -> IdOrPid;
        false -> misc:get_player_process(IdOrPid)
    end,
    case is_pid(Pid) of
        true  -> gen_server:cast(Pid, {'change_scene', SceneId, ScenePoolId, CopyId, X, Y, KeyValueList});
        false -> ignore
    end.

change_player_scene(IdOrPid, SceneId, ScenePoolId, CopyId, X, Y, AutoSelect, KeyValueList) ->
    Pid =
    case is_pid(IdOrPid) of
        true  -> IdOrPid;
        false -> misc:get_player_process(IdOrPid)
    end,
    case is_pid(Pid) of
        true  -> gen_server:cast(Pid, {'change_scene', SceneId, ScenePoolId, CopyId, X, Y, AutoSelect, KeyValueList});
        false -> ignore
    end.

%% 切换场景（应由角色进程调用）
%% Player       玩家当前状态
%% TSceneId     目标场景ID
%% TPoolId      目标场景进程id 默认为0
%% TCopyId      房间号ID
%% TargetX      目标坐标X
%% TargetY      目标坐标Y
%% AutoSelect   是否需要自动选择线路   true | false
%% TransferType 传送类型
%% KeyValueList 切换场景同步执行mod_sever_cast:set_data_sub/1 更新 player_status{} 的值
%%@return 新玩家状态
change_scene(Player, TSceneId, TPoolId, TCopyId, KeyValueList) ->
    [X, Y] = get_born_xy(TSceneId),
    change_scene(Player, TSceneId, TPoolId, TCopyId, X, Y, KeyValueList).
change_scene(Player, TSceneId, TPoolId, TCopyId, AutoSelect, KeyValueList) ->
    [X, Y] = get_born_xy(TSceneId),
    TransferType = ?TRANS_TYPE_SERVER,
    change_scene(Player, TSceneId, TPoolId, TCopyId, X, Y, AutoSelect, TransferType, KeyValueList).
change_scene(Player, TSceneId, TPoolId, TCopyId, TargetX, TargetY, KeyValueList) ->
    TransferType = ?TRANS_TYPE_SERVER,
    AutoSelect = true,
    change_scene(Player, TSceneId, TPoolId, TCopyId, TargetX, TargetY, AutoSelect, TransferType, KeyValueList).

change_scene(Player, TSceneId, TPoolId, TCopyId, TargetX, TargetY, AutoSelect, KeyValueList) ->
    TransferType = ?TRANS_TYPE_SERVER,
    change_scene(Player, TSceneId, TPoolId, TCopyId, TargetX, TargetY, AutoSelect, TransferType, KeyValueList).

change_scene(Player, TSceneId, TPoolId, TCopyId, TargetX, TargetY, AutoSelect, TransferType, KeyValueList)->
    #player_status{id=Id, figure = #figure{guild_id = GuildId, lv = Lv},
        scene = NowSceneId, scene_pool_id=NowScenePoolId,
        copy_id = NowCopyId, x = NowX, y = NowY, online = OnlineFlag,
        old_scene_info=OldSceneInfo, battle_attr=BA, team=Team
    } = Player,

    %% 检查跨服主城条件及场景id自动切换
    NewTSceneId = change_fix_city_scene(Player, TSceneId),

    TargetSceneInfo = {NewTSceneId, TargetX, TargetY, TPoolId, TCopyId},
    NowSceneInfo = {NowSceneId, NowScenePoolId, NowCopyId, NowX, NowY},

    {SceneId, ScenePoolId, CopyId, X, Y, NewOldSceneInfo} =
    change_scene_pool(OnlineFlag, Lv, Id, GuildId, AutoSelect, OldSceneInfo, TargetSceneInfo, NowSceneInfo),
    %% 工会驻地矫正分线id
    {FixPoolId, FixCopyId} = lib_guild_api:fix_guild_pool(SceneId, ScenePoolId, CopyId, GuildId),
    %% 离开旧场景
    leave_scene(Player),
    case BA#battle_attr.hp =:= 0 of
        true  ->
            %% 告诉客户端复活成功
            lib_revive:send_revive(Player, 0, SceneId, X, Y, "", BA#battle_attr.hp_lim, Player#player_status.gold, Player#player_status.bgold, 0);
        false ->
            ok
    end,
    %% Ps赋值:换场景满血
    TmpPlayer1 = Player#player_status{
        scene = SceneId, scene_pool_id = FixPoolId,
        copy_id = FixCopyId, x = X, y = Y, change_scene_sign=0,
        leave_scene_sign = 0, old_scene_info = NewOldSceneInfo,
        battle_attr = BA#battle_attr{hp = BA#battle_attr.hp_lim}
    },
    %% 回调一些模块做数据处理，返回新的玩家数据
    TmpPlayer2 = change_scene_callback(TmpPlayer1, NowSceneInfo),
    %% 计算进入场景所需要状态设置
    RoleEnterSetting = get_role_enter_scene_setting(SceneId, BA),
    %% 切场景处理（清除buff，重新计算属性，Kv赋值等）

    %% 给6秒无敌时间
    %Now = utime:unixtime(),
    %ProtectArg = [{pk_protect_time, Now + 7}],
    %% 切场景默认重置幽灵状态
    KeyValueList1 = [{ghost, 0}, {killed_pos, []}|lists:keydelete(ghost, 1, KeyValueList)],
    SyncSceneStatus = change_scene_handler(TmpPlayer2, SceneId, NowSceneId, RoleEnterSetting ++ KeyValueList1),
    #player_status{dungeon = #status_dungeon{dun_id = DunId}, x = FinalX, y = FinalY} = SyncSceneStatus,
    %% 通知客户端切换场景
    CallbackVal = 0,        %% 前端发送过来，（一般情况下）服务端再原样转发回去的回调标记
    {ok, Bin} = pt_120:write(12005, [SceneId, FinalX, FinalY, ?SUCCESS, DunId, CallbackVal, TransferType]),
    lib_server_send:send_to_sid(Player#player_status.sid, Bin),
    %% 队员位置修改通知
    lib_team_api:where_i_am(Team#status_team.team_id, Id, SceneId, FixPoolId, FixCopyId, X, Y),
    %% 跟随发送坐标到组队进程
    lib_team_api:change_location(SyncSceneStatus),
    %% 切场景判断队伍匹配状态
    % lib_dun_multi:change_scene_check(SyncSceneStatus),
    % ?INFO_TTY("change_scene Id:~p SceneId:~p,CopyId:~p txy:~w,~w xy:~w,~w=>~w,~w ~n",
    %     [Id, SceneId, CopyId, TargetX, TargetY, NowX, NowY, FinalX, FinalY]
    % ),
    SyncSceneStatus#player_status{fin_change_scene = 0}.

%% 切换场景时回调需要的模块做相应的数据处理
%% （通常用于防止意外切换方式引发一些数据问题）
%%
%% 这里实际包含了正常切换、非法的意外切换漏洞、进入离线挂机时的切换等情况
%% 回调接口参数固定：
%% Player           已经设置目标场景之后的数据 #player_status{}
%% OldSceneInfo     当前所处的场景信息 {OldSceneId, OldScenePoolId, OldCopyId, OldX, OldY}
%% OldSceneData     当前旧场景配置数据 #ets_scene{}
%% NewSceneData     目标场景的配置数据 #ets_scene{}
change_scene_callback(Player = #player_status{scene = NewSceneId}, OldSceneInfo) ->
    {OldSceneId, _OldScenePoolId, _OldCopyId, _OldX, _OldY} = OldSceneInfo,
    OldSceneData = data_scene:get(OldSceneId),
    NewSceneData = data_scene:get(NewSceneId),
    NewSceneData =:= [] andalso
        ?DEBUG("NewSceneId：~p, OldSceneInfo：~p", [NewSceneId, OldSceneInfo]),
    %% Callback: {Mod, Func},
    %%      Mod:Func(Player, OldSceneInfo, OldSceneData, NewSceneData) ->
    %%          ok | NewPlayer | {ok, NewPlayer}.
    Callbacks = [
        {lib_guild_support_mod, change_scene_callback}
        ,{quickbar_api, change_scene_callback}
        ,{local_boss_api, change_scene_callback}
        ,{guild_war_api, change_scene_callback}
        ,{lib_guild_tribe_mod, change_scene_callback}
        ,{madness_land_api, change_scene_callback}
        ,{offline_hosting_api, change_scene_callback}
        ,{convoy_api, change_scene_callback}
        ,{lib_team_api, change_scene_callback}
        ,{lib_guild_collect, change_scene_callback}
        ,{exp_sea_logic, change_scene_callback}
    ],
    %% 注意，回调模块中，Player的场景信息已经是目标场景
    lists:foldl(fun({Mod, Func}, AccPlayer) ->
                case Mod:Func(AccPlayer, OldSceneInfo, OldSceneData, NewSceneData) of
                    NewPlayer = #player_status{}        -> NewPlayer;
                    {ok, NewPlayer = #player_status{}}  -> NewPlayer;
                    _ -> AccPlayer
                end
        end, Player, Callbacks).

%% 托管玩家结束活动后可能有两种行为
%% 1是直接在活动场景下线/2是由活动管理踢出场景后然后下线
%% 跨服团战-活动场景直接结束托管
%% 跨服国战-活动结束先通知托管玩家下线同时也踢玩家出场，但是出场和下线是有延时的，所以下线和离场有先有后
%% 温泉-结束托管/踢人离场/关闭场景同时进行
%% 社团活动-直接场景直接结束托管
%% 百团大战-活动场景结束托管/15秒后踢人并关闭场景
%% 狂战领域-活动结束踢出场景，延迟5秒关闭场景/结束托管同时进行
%% 无尽领域-踢人出场/结束托管同时进行

%% 为了避免托管玩家踢出活动场景回到大世界场景后开辟无畏的场景分线
%% 托管玩家目标场景id为0的时候，不进行自动分线选择，进入默认场景。另外下线时有需求，托管玩家不记录离线时的场景id。所以这个默认场景id只是一个临时数据。
%% 为了减少计算，直接写死一个大世界作为一个临时场景id
%% 托管玩家离开活动场景的时候，如果没有显示的调用load_scene，默认是不会load_scene的，所以玩家数据不会进入场景进程
%% 而且一般情况下，回到大世界的托管玩家也会在较短的时间内结束托管下线，不会产生很大影响，而对于在托管下线前真实上线的玩家
%% 来说，前端会请求12002来加载场景，也不会有影响。
change_scene_pool(?ONLINE_OFF_HOSTING, Lv, Id, GuildId, _AutoSelect, OldSceneInfo, {0, _TargetX, _TargetY, _TargetScenePoolId, _TargetCopyId}, NowSceneInfo) ->
    change_scene_pool(?ONLINE_ON, Lv, Id, GuildId, false, OldSceneInfo, {?MAIN_CITY_SCENE, 0, 0, 0, 0}, NowSceneInfo);
%% 切换场景分线判断
change_scene_pool(_OnlineFlag, Lv, RoleId, GuildId, _IsAutoSelect = true, OldSceneInfo,
    {TargetSceneId, TargetX, TargetY, TargetScenePoolId, TargetCopyId},
    NowSceneInfo = {_NowSceneId, _NowScenePoolId, _NowCopyId, _NowX, _NowY}) ->
    %% TargetScenePoolId和TargetCopyId暂时未使用
    {SceneId, PoolId, CopyId, NewX, NewY, NewOldSceneInfo} =
    auto_select_scene({TargetSceneId, TargetX, TargetY, TargetScenePoolId, TargetCopyId}, OldSceneInfo, GuildId, Lv, NowSceneInfo),
    case data_scene:get(SceneId) of
        #ets_scene{type = SceneType, cls_type = SceneClsType} = SceneData ->
            {PoolIdRet, CopyIdRet} =
            case scene_mgr:is_force_sub_scene(SceneId, SceneType) of
                true  ->
                    %% 无视指定的场景poolId，尝试自动分线
                    case scene_mgr:fetch_scene(SceneId, SceneType, SceneClsType, RoleId) of
                        {NewPoolId, NewCopyId} ->
                            {NewPoolId, NewCopyId};
                        _Error ->
                            %% 考虑选择直接让玩家断线是否更安全合理
                            {0, 0}
                    end;
                false when TargetSceneId =:= SceneId orelse TargetSceneId =:= 0 ->
                    %% 按指定参数设置相应的TargetCopyId
                    {PoolId, CopyId};
                false ->
                    {0, 0}
            end,
            %% 这里检测是否可行走区域是为了避免进入到非可行走区
            %% （进入非可行走区域时，前端会自己往其他点走，会导致无法清晰理解玩家的移动行为）
            case scene_landform:is_block(SceneId, NewX, NewY) of
                true  ->
                    [BornX, BornY] = get_born_xy(SceneData),
                    {SceneId, PoolIdRet, CopyIdRet, BornX, BornY, NewOldSceneInfo};
                false ->
                    {SceneId, PoolIdRet, CopyIdRet, NewX, NewY, NewOldSceneInfo}
            end;
        _ ->
            {SceneId, 0, 0, NewX, NewY, NewOldSceneInfo}
    end;
change_scene_pool(_OnlineFlag, Lv, _RoleId, GuildId, _IsAutoSelect = false, OldSceneInfo,
    {TargetSceneId, TargetX, TargetY, TargetScenePoolId, TargetCopyId},
    NowSceneInfo = {_NowSceneId, _NowScenePoolId, _NowCopyId, _NowX, _NowY}) ->
    {SceneId, ScenePoolId, CopyId, NewX, NewY, NewOldSceneInfo} =
        auto_select_scene({TargetSceneId, TargetX, TargetY, TargetScenePoolId, TargetCopyId}, OldSceneInfo, GuildId, Lv, NowSceneInfo),
    {SceneId, ScenePoolId, CopyId, NewX, NewY, NewOldSceneInfo}.

%% 根据输入自动选择目标场景，记录旧场景信息
%% auto_select_scene(TargetSceneInfo, OldSceneInfo, GuildId, NowSceneInfo) -> NewSceneInfo.
%% TargetSceneInfo  : {TargetSceneId, TargetX, TargetY, TargetScenePoolId, TargetCopyId}
%% OldSceneInfo     : {OldSceneId, OldScenePoolId, OldCopyId, OldSceneX, OldSceneY} | undefined
%% NowSceneInfo     : {NowSceneId, NowScenePoolId, NowCopyId, NowSceneX, NowSceneY}
%% NewSceneInfo     : {NewSceneId, NewSceneX, NewSceneY, NewOldSceneInfo}
%% NewOldSceneInfo  : {NewOldSceneId, NewOldScenePoolId, NewOldCopyId, NewOldSceneX, NewOldSceneY} | undefined
auto_select_scene({0, _, _, _, _}, {OldSceneId, OldScenePoolId, OldCopyId, OldSceneX, OldSceneY},
    _GuildId, _Lv, _NowSceneInfo) ->
    {OldSceneId, OldScenePoolId, OldCopyId, OldSceneX, OldSceneY, undefined};
auto_select_scene({0, _, _, _, _}, _OldSceneInfo, _GuildId, Lv, _NowSceneInfo) ->
    [SceneId, X, Y] = lib_scene:repair_to_default(Lv),
    {SceneId, 0, 0, X, Y, undefined};
auto_select_scene({TargetSceneId, TargetX, TargetY, TargetScenePoolId, TargetCopyId}, OldSceneInfo, _GuildId, _Lv, NowSceneInfo) ->
    NewOldSceneInfo = select_scene_log_old(OldSceneInfo, NowSceneInfo),
    [X, Y] =
    case [TargetX, TargetY] of
        [0, 0] -> get_born_xy(TargetSceneId);
        _      -> [TargetX, TargetY]
    end,
    {TargetSceneId, TargetScenePoolId, TargetCopyId, X, Y, NewOldSceneInfo}.

%% 选择场景时，判断应当记录的旧场景信息
select_scene_log_old(OldSceneInfo, NowSceneInfo) ->
    {NowSceneId, _NowScenePoolId, _NowCopyId, _NowX, _NowY} = NowSceneInfo,
    %% 旧场景不能保存非主城、野外、公会驻地类的场景，否则有可能在特殊情况下引起死循环
    %% 如果有特殊需要允许保存的，再单独在这里做处理
    case data_scene:get(NowSceneId) of
        #ets_scene{type = NowSceneType} ->
            %% 判断是否需要记录当前场景信息作为下一次使用的旧场景信息
            IsGenericScene = lists:member(NowSceneType,
                [?SCENE_TYPE_NORMAL, ?SCENE_TYPE_OUTSIDE, ?SCENE_TYPE_GUILD, ?SCENE_TYPE_DESERTED_BOSS]
            ),
            IsDayNight = lib_guild_api:check_day_night(NowSceneInfo,OldSceneInfo),
            if
                IsGenericScene -> NowSceneInfo;
                IsDayNight -> OldSceneInfo; %%驻地白夜更替 不换
                true -> OldSceneInfo
            end;
        _ ->
            OldSceneInfo
    end.

%% 获取角色进入场景设置属性
get_role_enter_scene_setting(SceneId, #battle_attr{pk = #pk{pk_status = PkStatus}}) ->
    case data_scene:get(SceneId) of
        #ets_scene{requirement = Requirement} ->
            case lists:keyfind(pkstate, 1, Requirement) of
                {_, PkType} when PkType =/= PkStatus ->
                    [{pk, {PkType, true}}];
                _ ->
                    []
            end;
        _ ->
            []
    end.

%% 通知完成了场景切换
notify_fin_change_scene(Status) ->
    #player_status{fin_change_scene_ref = Ref, change_scene_num = ChangeSceneNum} = Status,
    util:cancel_timer(Ref),
    {ok, EvtStatus} = if
        ChangeSceneNum == 1 -> lib_player_event:dispatch(Status, ?EVENT_CLIENT_LOGIN_SUCCESS);
        true -> {ok, Status}
    end,
    {ok, EvtStatus2} = lib_player_event:dispatch(EvtStatus, ?EVENT_FIN_CHANGE_SCENE),
    ActionLock = case EvtStatus2 of
        #player_status{action_lock = Lock} when is_integer(Lock) ->
            Lock;
        _ ->
            0
    end,
    {ok, Bin} = pt_130:write(13009, [ActionLock]),
    lib_server_send:send_to_sid(Status#player_status.sid, Bin),
    {ok, EvtStatus2#player_status{fin_change_scene_ref = undefined}}.

%% 游戏内更改玩家场景信息(公共线调用)
%%@param Id                      玩家ID
%%@param SceneId                 目标场景ID
%%@param ScenePoolId             目标场景进程id
%%@param CopyId                  房间号ID 为0时，普通房间，非0值切换房间。值相同，在同一个房间。
%%@param X                       目标场景出生点X
%%@param Y                       目标场景出生点Y
%%@param KeyValueList =  lists() 用于在换场景后的数据处理,换线后会执行mod_server_cast:set_data_sub(Value, Status)
%%                      | 0      不做处理
change_player_scene_queue(Id, SceneId, ScenePoolId, CopyId, X, Y, KeyValueList) ->
    case misc:get_player_process(Id) of
        Pid when is_pid(Pid) ->
            catch gen_server:cast(Pid, {'change_scene_sign', [SceneId, ScenePoolId, CopyId, X, Y, KeyValueList]});
        _ ->
            void
    end.

%%排队-切换场景方法
%%@param PlayerStatus   玩家当前状态
%%@param SceneId        目标场景ID
%%@param CopyId         房间号ID
%%@param ScenePoolId    目标场景进程id
%%@param X              目标坐标X
%%@param Y              目标坐标Y
%%@param KeyValueList   自定义结构,用于在换场景后的数据处理
%%@return
change_scene_queue(PlayerStatus, SceneId, ScenePoolId, CopyId, X, Y, KeyValueList) ->
    catch gen_server:cast(PlayerStatus#player_status.pid, {'change_scene_sign', [SceneId, ScenePoolId, CopyId, X, Y, KeyValueList]}).

%%更改玩家到默认场景
player_change_default_scene(Id, KeyValueList) ->
    case misc:get_player_process(Id) of
        Pid when is_pid(Pid) ->
            gen_server:cast(Pid, {'change_default_scene', KeyValueList});
        _ ->
            void
    end.

change_default_scene(Status, KeyValueList) ->
    [SceneId, TargetX, TargetY] = repair_to_default(Status#player_status.figure#figure.lv),
    TargetScenePoolId = 0,
    TargetCopyId = 0,
    KeyValueList2= [{group, 0}, {ghost, 0}, {pk_protect_time, 0}] ++ KeyValueList,
    change_scene(Status, SceneId, TargetScenePoolId, TargetCopyId, TargetX, TargetY, KeyValueList2).

%%更改玩家到relogin场景
player_change_relogin_scene(Id, KeyValueList) ->
    case misc:get_player_process(Id) of
        Pid when is_pid(Pid) ->
            gen_server:cast(Pid, {'change_relogin_scene', KeyValueList});
        _ ->
            void
    end.

change_relogin_scene(Status, KeyValueList) ->
    #player_status{
        scene = SceneId,
        scene_pool_id = TargetScenePoolId,
        copy_id = TargetCopyId,
        x = TargetX,
        y = TargetY
    } = Status,
    change_scene(Status, SceneId, TargetScenePoolId, TargetCopyId, TargetX, TargetY, KeyValueList).

%%改变速度
change_speed(Id, Scene, ScenePoolId, CopyId, X, Y, Speed, Sign)->
    {ok, BinData} = pt_120:write(12082, [Sign, Id, Speed]),
    lib_server_send:send_to_area_scene(Scene, ScenePoolId, CopyId, X, Y, BinData).


%%离开当前场景
%% Sid:场景id
%% player_status:记录
leave_scene(Status) ->
    mod_scene_agent:leave(Status).

%% @doc 处理离开场景操作:根据Type:做离开场景的处理(不在玩家进程时调用)
%% -spec leave_scene(Pid|Id, Type, Args) -> ok when
%%    Id          :: integer(),                %% 玩家id
%%    Pid         :: pid(),                    %% 玩家进程pid
%%    Type        :: all,                      %% all
%%    Args        :: list().                   %% 列表
%% @end
%% -----------------------------------------------------------
leave_scene(Pid, Type, Args) when is_pid(Pid)->
    gen_server:cast(Pid, {'leave_scene', Type, Args});
leave_scene(Id, Type, Args) when is_integer(Id)->
    case misc:get_player_process(Id) of
        Pid when is_pid(Pid) ->
            gen_server:cast(Pid, {'leave_scene', Type, Args});
        _ ->
            ok
    end;
leave_scene(_Id, _Type, _Args)->
    ok.

% %% 进入当前场景
% %% player_status记录
% enter_scene(Status, EnterType) ->
%     mod_scene_agent:join(Status, EnterType).

% %% 给用户发送场景信息 - 12002内容
% send_scene_info_to_uid(Key, Sid, ScenePoolId, CopyId, X, Y) ->
%     mod_scene_agent:send_scene_info_to_uid(Key, Sid, ScenePoolId, CopyId, X, Y).

% %% 复活进入场景
% %% status复活前的状态，status1复活后的状态
% revive_to_scene(Status1, Status2) ->
%     %%告诉原来场景玩家你已经离开
%     leave_scene(Status1),
%     %%告诉复活点的玩家你进入场景进
%     enter_scene(Status2),
%     send_scene_info_to_uid(Status2#player_status.id, Status2#player_status.scene, Status2#player_status.scene_pool_id,
%         Status2#player_status.copy_id, Status2#player_status.x, Status2#player_status.y).

%% 获取场景信息，唯一id，区分是不是副本
get_data(Id) -> %data_scene:get(Id).
    case ets:lookup(?ETS_SCENE, Id) of
        []  -> data_scene:get(Id);
        [S] -> S
    end.

%% @doc 进入场景条件检查并且根据副本类型做相关处理
%% DunId      : 副本id
%% DstSceneId : 目标场景id
%% TransType  : 传送类型
%% @end
check_enter(Status, _DunId, DstSceneId, TransType) ->
    check_enter(Status, _DunId, DstSceneId, TransType, 0, 0).

check_enter(Status, _DunId, DstSceneId, TransType, DstX, DstY) ->
    #player_status{battle_attr = #battle_attr{ghost = Ghost}} = Status,
    {IsTransable, ErrorCode1} = is_transferable(Status),
    %% 特殊处理：跳一跳匹配可以正常切换场景
    IsJumpMatching = custom_act_jump_act:is_in_jump_match(Status),
    ConvoyCheck = convoy_api:convoy_check(Status, DstSceneId),
    case IsTransable orelse IsJumpMatching orelse ConvoyCheck of
        true  ->
            #player_status{scene = PlayerSceneId} = Status,
            PlayerScene = case data_scene:get(PlayerSceneId) of
                [] -> #ets_scene{};
                SceneData -> SceneData
            end,
            DstScene = data_scene:get(DstSceneId),
            IsForbid = is_forbidden_change(DstScene),
            if
                not is_record(DstScene, ets_scene) ->
                    %% 配置数据非法
                    {false, ?ERRCODE(err120_cannot_transfer_scene_not_exist)};
                IsForbid ->
                    %% 目标场景是禁止传送抵达的场景
                    {false, ?ERRCODE(err120_cannot_transfer_scene)};
                Ghost == 1 ->
                    %% 幽灵状态禁止传送
                    {false, ?ERRCODE(err120_ghost_state_limit)};
                true ->
                    case sub_check_enter(Status, PlayerScene, DstScene, TransType, DstX, DstY) of
                        {false, ErrorCode} ->
                            {false, ErrorCode};
                        {true, DstXNew, DstYNew} ->
                            {true, DstXNew, DstYNew}
                    end
            end;
        _False ->
            %% 当前所在场景是禁止传送的场景
            {false, ErrorCode1}
    end.

%% 进入场景条件检查(处理场景的基本检查)
sub_check_enter(Status, PlayerScene, DstScene, TransType, DstX, DstY) ->
    #player_status{scene = PlayerSceneId, x = X, y = Y} = Status,
    #ets_scene{
        id = DstSceneId, type = DstSceneType, cls_type = DstClsType,
        elem = DstSceneElem,
        requirement = Requirement
    } = DstScene,
    [DstBornX, DstBornY] = get_born_xy(DstScene),
    case check_requirement(Status, Requirement) of
        true ->
            IsSubLineType = lists:member(DstSceneType, ?SCENE_TYPE_NEED_SUB_LINE),
            IsConn = clusters_api:is_connected_center(),
            #ets_scene{elem = PlayerSceneElem} = PlayerScene,
            if
                IsSubLineType andalso
                DstClsType =:= ?SCENE_CLS_TYPE_CENTER andalso IsConn == false ->
                    {false, ?ERRCODE(not_connect_center)};
                DstSceneId =:= PlayerSceneId ->
                    {true, X, Y};
                TransType =:= ?TRANS_TYPE_WORLD ->
                    %% 大世界传送，抵达场景出生点
                    {true, DstBornX, DstBornY};
                TransType =:= ?TRANS_TYPE_SERVER orelse TransType =:= ?TRANS_TYPE_FLY ->
                    %% 服务端传送 及 使用飞鞋，一般使用指定的目标
                    case {DstX, DstY} =:= {0, 0} of
                        true  -> {true, DstBornX, DstBornY};
                        false -> {true, DstX, DstY}
                    end;
                TransType =:= ?TRANS_TYPE_GATE_NEAR ->
                    %% 传统传送门，相邻城市传送，出口为对应的传送点
                    case check_scene_gate_near(DstSceneId, PlayerSceneElem, X, Y) of
                        {true, _, _} = Result ->
                            Result;
                        false ->    % 不在传送门附近，修正为传送到目标场景出生点
                            {true, DstBornX, DstBornY}
                    end;
                TransType =:= ?TRANS_TYPE_GATE_ANY ->
                    %% 任意城市传送门，出口为目标城市距离目的地最近的传送门
                    case check_scene_gate_any(PlayerSceneElem, DstSceneElem, X, Y, DstX, DstY) of
                        {true, _, _} = Result ->
                            Result;
                        false ->
                            %% 不在传送门附近，或目标城市无传送门，修正为目标场景出生点
                            {true, DstBornX, DstBornY}
                    end;
                true ->
                    %% 非法传送类型，前面已排除不可传送情况，此处修正为目标场景出生点
                    {true, DstBornX, DstBornY}
            end;
        {false, Reason} ->
            {false, Reason}
    end.

%% 检查是不是禁止玩家主动切换的场景（12005的协议）
%% 只有大世界可以自动切过去，其他场景必须走玩法调用
is_forbidden_change(#ets_scene{type = Type})->
    not (Type =:= ?SCENE_TYPE_NORMAL orelse Type =:= ?SCENE_TYPE_OUTSIDE);
is_forbidden_change(_) ->
    true.

%% 逐个检查进入需求
check_requirement(_, []) ->
    true;
check_requirement(Status, [{K, V} | T]) ->
    case K of
        lv -> %% 等级需求
            %% 注: 给机器人放行逻辑
            IsRobot = lib_robot:is_robot(Status),
            case check_enter_lv(Status#player_status.figure#figure.lv, V)  of
                true ->
                    check_requirement(Status, T);
                false when IsRobot ->
                    check_requirement(Status, T);
                false ->
                    {false, ?ERRCODE(err120_cannot_transfer_lv_not_reach)}
            end;
        _ ->
            check_requirement(Status, T)
    end.

check_enter_lv(Lv, LvList) when is_list(LvList) ->
    WorldLv = util:get_world_lv(),
    EnterLv = util:term_area_value(LvList, 1, 2, WorldLv),
    Lv>=EnterLv;
check_enter_lv(Lv, NeedLv) ->
    Lv>=NeedLv.

%% 玩家是否达到进入场景等级条件
is_enough_lv_enter(Lv, SceneId) ->
    case data_scene:get(SceneId) of
        [] ->
            false;
        #ets_scene{} = Scene ->
            LvRequirement = lists:keyfind(lv, 1, Scene#ets_scene.requirement),
            case LvRequirement of
                {lv, LvList} when is_list(LvList) -> check_enter_lv(Lv, LvList);
                {lv, NeedLv} when is_integer(NeedLv) -> Lv>=NeedLv;
                _ -> true
            end
    end.

%% 检查是不是需要触发ai的场景
check_need_ai_scene(Type) ->
    if
        Type == ?SCENE_TYPE_DUNGEON orelse      %% 副本场景
        Type == ?SCENE_TYPE_3v3 orelse          %% 3v3场景
        Type == ?SCENE_TYPE_DESERTED_BOSS ->
            true;
        true ->
            false
    end.

%% check_need_trigger_mon_ai
check_need_trigger_mon_ai(Scene, User) ->
    #ets_scene_user{battle_attr=#battle_attr{hide = Hide, ghost = Ghost} } = User,
    #ets_scene{type = Type} = Scene,
    NeedAiScene = check_need_ai_scene(Type),
    if
        Hide == 1 orelse Ghost == 1 -> false;
        NeedAiScene ->
            true;
        true ->
            false
    end.

%% 触发怪物AI
trigger_mon_ai(Scene, User, TX, TY, MonMgrPid) ->
    #ets_scene_user{id = RoleId, copy_id=CopyId,  pid = RolePid,
        battle_attr=#battle_attr{group = Group},
        deserted_boss_tired = DesertedBossTired } = User,
    NoticeMon = check_need_trigger_mon_ai(Scene, User),
    case NoticeMon of
        true  ->
            case local_boss_api:is_deserted_boss_scene(Scene) of
                false ->
                    {Xlg, Ylg} = lib_scene_calc:pixel_to_logic_coordinate(TX, TY),
                    Msg = {'ripple', RoleId, RolePid, ?BATTLE_SIGN_PLAYER, TX, TY, Group},
                    [
                        % old: Aid ! {'ripple', RoleId, RolePid, ?BATTLE_SIGN_PLAYER, TX, TY, Group}
                        mon_mgr:send(MonMgrPid, MonId, Msg)
                        || MonId <- lib_scene_object_agent:get_trace(CopyId, Xlg, Ylg)
                    ];
                _ ->
                    {Xlg, Ylg} = lib_scene_calc:pixel_to_logic_coordinate(TX, TY),
                    AiMap = #{
                        id => RoleId, pid => RolePid, sign => ?BATTLE_SIGN_PLAYER,
                        pos => {TX, TY}, group => Group, deserted_boss_tired => DesertedBossTired
                    },
                    % old: [ Aid ! {'deserted_ripple', AiMap} || Aid <- lib_scene_object_agent:get_trace(CopyId, Xlg, Ylg)]
                    [
                        mon_mgr:send(MonMgrPid, MonId, {'deserted_ripple', AiMap}) ||
                        MonId <- lib_scene_object_agent:get_trace(CopyId, Xlg, Ylg)
                    ]
            end;
        false ->
            skip
    end.

%% 检查是否需要触发巡逻的场景
check_need_patrol_scene(Type) ->
    if
        Type == ?SCENE_TYPE_NORMAL orelse       %% 主城（普通场景）
        Type == ?SCENE_TYPE_OUTSIDE orelse      %% 野外场景
            true;
        true ->
            false
    end.

% %% 更改区域属性
% %% ClientType:0可行走 1障碍区, 5安全区
% %% AreaMarkL: [{AreaId, ClientMark},...]
% change_area_mark(SceneId, ScenePoolId, CopyId, AreaMarkL) ->
%     SerAreaMarkL = lib_scene_mark:mark_c2s(AreaMarkL),
%     mod_scene_agent:apply_cast(SceneId, ScenePoolId, lib_scene_agent, change_area_mark, [CopyId, SerAreaMarkL]),
%     mod_scene_mark:change_area_mark(SceneId, CopyId, SerAreaMarkL),
%     {ok, BinData} = pt_120:write(12030, AreaMarkL),
%     lib_server_send:send_to_scene(SceneId, ScenePoolId, CopyId, BinData),
%     ok.

%% 更改场景特效状态
%% EffChangeValues = [{EffId, DelOrAdd},...]
%% -- EffId:特效id
%% -- DelOrAdd (0隐藏|1增加)
change_dynamic_eff(SceneId, ScenePoolId, CopyId, EffChangeValues) ->
    mod_scene_agent:apply_cast(SceneId, ScenePoolId, lib_scene_agent, change_dynamic_eff, [CopyId, EffChangeValues]),
    {ok, BinData} = pt_120:write(12032, [SceneId, EffChangeValues]),
    lib_server_send:send_to_scene(SceneId, ScenePoolId, CopyId, BinData),
    ok.

%% 是否为副本场景，唯一id，会检查是否存在这个场景
is_dungeon_scene(Id) ->
    case data_scene:get(Id) of
        [] -> false;
        S -> S#ets_scene.type =:= ?SCENE_TYPE_DUNGEON
    end.

%% 是否boss场景
is_boss_scene(Id) when is_integer(Id) ->
    case data_scene:get(Id) of
        [] -> false;
        S -> is_boss_scene(S)
    end;
is_boss_scene(S) when is_record(S, ets_scene)->
    S#ets_scene.type =:= ?SCENE_TYPE_WORLD_BOSS orelse  S#ets_scene.type == ?SCENE_TYPE_REWARD_BOSS;
is_boss_scene(S) ->
    ?ERR("param erroe:~p~n", [S]),
    false.

%% 是否可以传送
is_transferable(#player_status{
        scene = SceneId, copy_id = CopyId, x = Xpx, y = Ypx,
        change_scene_sign = IsChangeSceneSign, battle_attr = BattleAttr,
        last_att_time = _LastAttTime, last_be_att_time = _LastBeAttTime, action_lock = ActionLock
    } = _Status)->
    if
        %% 排队换线中
        IsChangeSceneSign == 1 ->
            {false, ?ERRCODE(err120_cannot_transfer_change_scene_sign)};
        ActionLock =/= free ->
            {false, ActionLock};
        is_record(BattleAttr, battle_attr) andalso BattleAttr#battle_attr.ghost =/= 0 -> %% 幽灵状态限制切场景
            {false, ?ERRCODE(err120_ghost_state_limit)};
        true ->
            is_transferable(SceneId, CopyId, Xpx, Ypx)
    end.

is_transferable(SceneId, _CopyId, _Xpx, _Ypx) ->
    case data_scene:get(SceneId) of
        [] ->
            {false, ?ERRCODE(err120_cannot_transfer_scene)};
        %% 主城场景
        #ets_scene{type = ?SCENE_TYPE_NORMAL} -> {true, 1};
        %% 公会场景可以去其他地方
        #ets_scene{type = ?SCENE_TYPE_GUILD} -> {true, 1};
        %% 幻魔星域场景可以去其他地方
        #ets_scene{type = ?SCENE_TYPE_DESERTED_BOSS} -> {true, 1};
        %% 野外场景的安全区
        #ets_scene{type = ?SCENE_TYPE_OUTSIDE} -> {true, 1};
        %% 其他场景都不可以传送.
        _ ->
            {false, ?ERRCODE(err120_cannot_transfer_scene)}
    end.

%% 判断在场景Sid 的像素坐标是否在安全区
is_safe(Sid, _CopyId, Xpx, Ypx) ->
    case data_scene:get(Sid) of
        [] ->       % 非安全区
            false;
        #ets_scene{subtype = ?SCENE_SUBTYPE_SAFE} ->
            true;
        _ ->
            scene_landform:is_safe_pos(Sid, Xpx, Ypx)
    end.

%% 判断是否安全场景
is_safe_scene(Sid) ->
    case data_scene:get(Sid) of
        #ets_scene{subtype = ?SCENE_SUBTYPE_SAFE} -> true;
        _ -> false
    end.

%% 判断是否野外场景
is_outside_scene(Sid) ->
    case data_scene:get(Sid) of
        #ets_scene{type=?SCENE_TYPE_OUTSIDE} -> true;
        _ -> false
    end.

%% 是否跨服场景
is_clusters_scene(SceneId) ->
    case data_scene:get(SceneId) of
        [] -> false;% 非跨服场景
        Scene -> Scene#ets_scene.cls_type == ?SCENE_CLS_TYPE_CENTER
    end.

%% 获取场景名称
get_scene_name(SceneId) ->
    case data_scene:get(SceneId) of
        []  -> <<>>;
        S -> util:make_sure_binary(S#ets_scene.name)
    end.

%% 获取当前场景出生点
%% （安全区复活及进入场景）
get_born_xy(#ets_scene{type = ?SCENE_TYPE_MADNESS, mon= Mons} = SceneData) ->
    %% 以BOSS刷新点 {3792,3787} 为圆心, 2000像素为内圈半径，2400像素为外圈半径。他们所形成的圆环即为复活区域，在复活区域随机
    case Mons of
        [[_, X, Y, _, _]|_] ->
            Dis   = urand:rand(2000, 2400),
            Angle = urand:rand(0, 360) * math:pi() / 180,
            [
                erlang:round(Dis * math:cos(Angle) + X),
                erlang:round(Dis * math:sin(Angle) + Y)
            ];
        _ ->
            get_born_xy(SceneData#ets_scene{type = 0})
    end;
get_born_xy(#ets_scene{x = X, y = Y, reborn_xys = RebornXYs}) ->
    case RebornXYs of
        [{BornX, BornY}] ->
            %% 只有一个点
            [BornX, BornY];
        [{_, _} | _] ->
            %% 多个点内随机
            case urand:list_rand(RebornXYs) of
                {BornX, BornY} -> [BornX, BornY];
                _ -> [X, Y]
            end;
        [{CenterX, CenterY, Radius}] ->
            %% 以某个点为圆心的圆内随机一点
            {BornX, BornY} = umath:rand_point_in_circle(CenterX, CenterY, Radius, 1),
            [BornX, BornY];
        _ ->
            [X, Y]
    end;
get_born_xy(SceneId) ->
    case data_scene:get(SceneId) of
        #ets_scene{} = SceneData ->
            get_born_xy(SceneData);
        _ ->
            [0, 0]
    end.

%% 其他场景
%% Scene:副本id
repair_xy(Lv, Scene, X, Y) ->
    IsEnoughLv = is_enough_lv_enter(Lv, Scene),
    InitSceneData = data_scene:get(Scene),
    NewSceneData  = ?IF(is_record(InitSceneData, ets_scene), InitSceneData, #ets_scene{}),
    IsSubLineType = lists:member(NewSceneData#ets_scene.type, ?SCENE_TYPE_NEED_SUB_LINE),
    IsConn = clusters_api:is_connected_center(),

    case InitSceneData of
        [] -> repair_to_default(Lv);
        %% 连接不上跨服中心时，将跨服野外主城场景repair
        #ets_scene{cls_type = ?SCENE_CLS_TYPE_CENTER}
            when IsSubLineType == true andalso IsConn == false ->
            repair_to_default(Lv);
        %% 进入等级不足
        #ets_scene{} when IsEnoughLv == false->
            repair_to_default(Lv);
        %% 普通场景不需要矫正
        #ets_scene{type = SceneType}
            when SceneType =:= ?SCENE_TYPE_NORMAL
            orelse SceneType =:= ?SCENE_TYPE_OUTSIDE ->
            case scene_landform:is_block(Scene, X, Y) of
                true ->
                    [ReX, ReY] = get_born_xy(Scene),
                    [Scene, ReX, ReY];
                false ->
                    case X =< 0 orelse InitSceneData#ets_scene.width =< X orelse Y =< 0 orelse InitSceneData#ets_scene.height =< Y of
                        true  -> [Scene, InitSceneData#ets_scene.x, InitSceneData#ets_scene.y];
                        false -> [Scene, X, Y]
                    end
            end;
        _ ->
            %% 其他矫正
            repair_to_default(Lv)
    end.

%% 离线时矫正坐标 SceneType == ?SCENE_TYPE_GUILD
logout_repair_xy(Player) ->
    Repair2Old =
    case data_scene:get(Player#player_status.scene) of
        #ets_scene{type = ?SCENE_TYPE_NORMAL} ->
            %% 普通场景不需要矫正坐标
            false;
        #ets_scene{type = ?SCENE_TYPE_OUTSIDE} ->
            %% 野外场景不需要矫正坐标
            false;
        #ets_scene{type = ?SCENE_TYPE_DUNGEON} ->
            %% 副本场景不需要矫正坐标
            false;
        _ ->
            %% 其他场景都默认矫正回之前的场景
            true
    end,
    case Repair2Old of
        true  ->
            case Player#player_status.old_scene_info of
                {OldScene, _OldScenePooldId, _OldCopyId, OldX, OldY} ->
                    Player#player_status{scene = OldScene, x = OldX, y = OldY};
                _ ->
                    [Scene, X, Y] = repair_to_default(Player#player_status.figure#figure.lv),
                    Player#player_status{scene = Scene, x = X, y = Y}
            end;
        false ->
            Player
    end.

%% 根据等级修复到相应等级的挂机场景
repair_to_default(Lv) ->
    %% 从高级到低级
    L = [1005, 1004, 1002, 1001],
    case repair_to_lv_outsize_scene(L, Lv) of
        false ->
            [ReX, ReY] = get_born_xy(?MAIN_CITY_SCENE),
            [?MAIN_CITY_SCENE, ReX, ReY];
        SceneId ->
            [ReX, ReY] = get_born_xy(SceneId),
            [SceneId, ReX, ReY]
    end.

repair_to_lv_outsize_scene([], _Lv) -> false;
repair_to_lv_outsize_scene([SceneId|T], Lv) ->
    case is_enough_lv_enter(Lv, SceneId) of
        true -> SceneId;
        _ -> repair_to_lv_outsize_scene(T, Lv)
    end.

%% 是否需要场景广播
is_broadcast_scene(#ets_scene{broadcast= ?BROADCAST_ALL}) -> true;
is_broadcast_scene(#ets_scene{}) -> false;
is_broadcast_scene(SceneId) when is_integer(SceneId) ->
    is_broadcast_scene(data_scene:get(SceneId)).

%% 是否强制全场景广播
%% 某些活动场景是九宫格广播，但是怪物变化需要全场景广播
is_force_broadcast_scene(#ets_scene{type = ?SCENE_TYPE_GWAR}) -> true;
is_force_broadcast_scene(#ets_scene{}) -> false;
is_force_broadcast_scene(SceneId) when is_integer(SceneId)  ->
    is_force_broadcast_scene(data_scene:get(SceneId)).

%% 切换场景需要处理的操作
change_scene_handler(Status, _EnterSceneId, _LeaveSceneId, KeyValueList) ->
    %% 重新计算物品buff属性
    %% 如果buff属性没有变化不会重复计算战力
    {ok, NewStatus} = lib_goods_buff:check_player_buff(Status),
    %% Kv赋值
    LastPs = mod_server_cast:set_data_sub(KeyValueList, NewStatus),
    LastPs.

%% 创角场景及坐标 -> {SceneId, X, Y}
get_create_role_position() ->
    SceneId = ?BORN_SCENE,
    case data_scene:get(SceneId) of
        #ets_scene{x = X, y = Y} ->
            %% 创角场景，在初次进入场景时不使用reborn_xys，固定进入点
            %% 后面进入时遵循一般的进入逻辑，优先reborn_xys配置
            {SceneId, X, Y};
        _ ->
            {SceneId, 1760, 1754}
    end.

%% 获取主城随机出生地 -> {X, Y}
get_main_city_x_y() ->
    [X, Y] = get_born_xy(?MAIN_CITY_SCENE),
    {X, Y}.

%% 是否在主城
is_in_main_city_scene(SceneId) when is_integer(SceneId) ->
    lists:member(SceneId, ?MAIN_CITY_SCENE_LIST);
is_in_main_city_scene(Status) when is_record(Status, player_status) ->
    lists:member(Status#player_status.scene, ?MAIN_CITY_SCENE_LIST).

%% 是否在主城和野外场景
is_in_normal_and_outside(SceneId) ->
    case data_scene:get(SceneId) of
        #ets_scene{type = Type} when Type == ?SCENE_TYPE_NORMAL orelse Type == ?SCENE_TYPE_OUTSIDE -> true;
        _ -> false
    end.

%% 是否公会场景
is_guild_scene(SceneId) ->
    case data_scene:get(SceneId) of
        #ets_scene{type = Type} when Type == ?SCENE_TYPE_GUILD -> true;
        _ -> false
    end.

%% 是否在主城和野外场景和公会场景
is_in_normal_and_outside_and_guild(PS) ->
    #player_status{scene = SceneId, change_scene_sign = IsChangeSceneSign} = PS,
    if
        %% 排队换线中
        IsChangeSceneSign==1 ->
            {false, ?ERRCODE(err120_cannot_transfer_change_scene_sign)};
        true ->
            case data_scene:get(SceneId) of
                #ets_scene{type = Type} when Type == ?SCENE_TYPE_NORMAL orelse Type == ?SCENE_TYPE_OUTSIDE orelse Type == ?SCENE_TYPE_GUILD ->
                    true;
                _ -> {false, ?ERRCODE(err120_cannot_transfer_not_in_safe)}
            end
    end.

%% 是否能参与普通活动[主城场景、野外场景的安全区、公会场景.]
is_can_join_common_act(SceneId, CopyId, X, Y) ->
    case data_scene:get(SceneId) of
        #ets_scene{type = Type} when
        Type == ?SCENE_TYPE_NORMAL orelse Type == ?SCENE_TYPE_OUTSIDE orelse Type == ?SCENE_TYPE_GUILD ->
            is_safe(SceneId, CopyId, X, Y);
        _ -> false
    end.

%% 返回场景玩家数据给玩家进程
return_scene_user_tps(SceneUser) ->
    lib_player:update_player_info(SceneUser#ets_scene_user.id,
        [{hp, SceneUser#ets_scene_user.battle_attr#battle_attr.hp}]),
    ok.

%% 清理场景副本
clear_scene_room(SceneId, ScenePoolId, CopyId) ->
    case data_scene:get(SceneId) of
        #ets_scene{type = ?SCENE_TYPE_OUTSIDE} ->
            %% 记录下清除野外场景怪物信息，便于查找野怪消失原因
            % tool:back_trace_to_file(),
            mod_scene_agent:apply_cast(SceneId, ScenePoolId, lib_scene_agent, clear_scene_room, [CopyId]);
        #ets_scene{} ->
            mod_scene_agent:apply_cast(SceneId, ScenePoolId, lib_scene_agent, clear_scene_room, [CopyId]);
        _ ->
            ?ERR("clear_scene_room scene_id error:~p~n", [{SceneId, ScenePoolId, CopyId}])
    end.

%% 清理场景(仅保留进程和障碍区和安全区数据)
clear_scene(SceneId, ScenePoolId) ->
    mod_scene_agent:apply_cast(SceneId, ScenePoolId, lib_scene_agent, clear_scene, []).

%% 广播场景主角属性更新
%% AttrKeyValues :: [{attrkey,value}|_]
broadcast_player_attr(#player_status{id=PlayerId, scene=Scene, scene_pool_id=ScenePoolId, copy_id=CopyId, x=Xpx, y=Ypx}, AttrKeyValues) ->
    broadcast_player_attr(PlayerId, Scene, ScenePoolId, CopyId, Xpx, Ypx, AttrKeyValues).

broadcast_player_attr(PlayerId, Scene, ScenePoolId, CopyId, Xpx, Ypx, AttrKeyValues) ->
    {ok, BinData} = pt_120:write(12010, [PlayerId, AttrKeyValues]),
    lib_server_send:send_to_area_scene(Scene, ScenePoolId, CopyId, Xpx, Ypx, BinData).

%% 更改玩家活动形象
change_act_label(RoleId, Key, Value) when is_integer(RoleId) ->
    lib_player:apply_cast(RoleId, ?APPLY_CAST_STATUS, ?MODULE, change_act_label, [Key, Value]);
change_act_label(Player, Key, Value) ->
    #player_status{act_label = ActLabel} = Player,
    NewActLabel = case Value == 0 of
                      true ->
                          lists:keydelete(Key, 1, ActLabel);
                      false ->
                          lists:keystore(Key, 1, ActLabel, {Key, Value})
                  end,
    NewPlayer = Player#player_status{act_label = NewActLabel},
    change_scene_act_label(Player, Key, Value),
    NewPlayer.
change_scene_act_label(Pid, Key, Value) when is_pid(Pid) ->
    lib_player:apply_cast(Pid, ?APPLY_CAST_STATUS, ?MODULE, change_scene_act_label, [Key, Value]);
change_scene_act_label(RoleId, Key, Value) when is_integer(RoleId) ->
    lib_player:apply_cast(RoleId, ?APPLY_CAST_STATUS, ?MODULE, change_scene_act_label, [Key, Value]);
change_scene_act_label(Player, Key, Value) ->
    #player_status{
        id = RoleId, scene = SceneId, scene_pool_id = PoolId, copy_id = CopyId,
        x = X, y = Y
    } = Player,
    mod_scene_agent:update(SceneId, PoolId, RoleId, [{act_label, [{Key, Value}]}]),
    broadcast_player_act_label(RoleId, SceneId, PoolId, CopyId, X, Y, [{Key, Value}]),
    Player.
%% 广播场景主角活动标识
%% AttrKeyValues :: [{attrkey,value}|_]
broadcast_player_act_label(#player_status{id=PlayerId, scene=Scene, scene_pool_id=ScenePoolId, copy_id=CopyId, x=Xpx, y=Ypx}, AttrKeyValues) ->
    broadcast_player_act_label(PlayerId, Scene, ScenePoolId, CopyId, Xpx, Ypx, AttrKeyValues).

broadcast_player_act_label(PlayerId, Scene, ScenePoolId, CopyId, Xpx, Ypx, AttrKeyValues) ->
    {ok, BinData} = pt_120:write(12089, [PlayerId, AttrKeyValues]),
    lib_server_send:send_to_area_scene(Scene, ScenePoolId, CopyId, Xpx, Ypx, BinData).

broadcast_player_figure(#player_status{id=PlayerId, scene=Scene, scene_pool_id=ScenePoolId, copy_id=CopyId, x=Xpx, y=Ypx, figure = Figure}) ->
    {ok, BinData} = pt_120:write(12078, [PlayerId, Figure]),
    lib_server_send:send_to_area_scene(Scene, ScenePoolId, CopyId, Xpx, Ypx, BinData).

broadcast_player_scene_info(Player, Arg) ->
    #player_status{id=PlayerId, scene=Scene, scene_pool_id=ScenePoolId, copy_id=CopyId, x=Xpx, y=Ypx} = Player,
    {ok, BinData} = pt_120:write(12010, [PlayerId, Arg]),
    lib_server_send:send_to_area_scene(Scene, ScenePoolId, CopyId, Xpx, Ypx, BinData),
    ok.
%% 获取场景最大人数
get_scene_max_role(_SceneId) ->
    300.

%% 获取场景房间最大人数
%% 注意：通常只用于分线场景
get_room_max_role(SceneId) ->
    get_room_max_role(SceneId, get_scene_max_role(SceneId)).

%% 场景房间人数上限
get_room_max_role(SceneId, SceneMaxRole) ->
    case data_scene_other:get(SceneId) of
        #ets_scene_other{room_max_people = RoomMaxRole} ->
            max(min(RoomMaxRole, SceneMaxRole), ?MIN_SCENE_MAX_ROLE);
        _ -> 50
    end.

%% 是否在附近
is_near(SceneId, MX, MY, TSceneId, TX, TY)->
    case SceneId =/= TSceneId of
        true  -> false;
        false -> fuzzy_nearby(MX, MY, TX, TY, 100)
    end.

%% 获取场景类型
%% [type, subtype, cls_type]
get_scene_type(Sid) ->
    case data_scene:get(Sid) of
        #ets_scene{type = Type, subtype = Subtype, cls_type = ClsType} ->
            [Type, Subtype, ClsType];
        _ -> [0, 0, 0]
    end.

%% 同步疲劳值
%% RoleId  :玩家id
%% Type    :类型 后续添加新玩法,各自公式处理（predefine.hrl定义）
%% Val     :疲劳值
%% SceneId :场景id
%% PoolId  :场景进程id,默认为0
setup_attr_tired(RoleId, _Type, SceneId, PoolId, Val)  ->
    KeyValueList = [{att_tired, Val}],
    mod_scene_agent:update(SceneId, PoolId, RoleId, KeyValueList).

%% 同步疲劳值
%% Player  :玩家ps
%% Type    :类型 后续添加新玩法,各自公式处理（predefine.hrl定义）
%% Val     :疲劳值
setup_attr_tired(Player, _Type, Val)  ->
    KeyValueList = [{att_tired, Val}],
    mod_scene_agent:update(Player, KeyValueList).


%% 获取野外场景开启房间号列表
get_room_list(SceneId, ScenePoolId) ->
    case ets:lookup(?SCENE_ROLE_COUNT, {SceneId, ScenePoolId}) of
        [#scene_role_count{num_maps = NumMaps}] -> maps:keys(NumMaps);
        _ -> []
    end.

%% 将所有玩家踢出场景，回到上一个场景
kick_players_out_scene(SceneID, PoolId, KeyValueList) ->
    kick_players_out_scene(SceneID, PoolId, all, KeyValueList).

kick_players_out_scene(SceneID, PoolId, CopyID, KeyValueList) ->
    mod_scene_agent:apply_cast(SceneID, PoolId, lib_scene_agent, kick_out_players, [CopyID, KeyValueList]).


%% 判断是否需要全场景广播角色进入的场景类型
%% （部分玩法需要）
is_broadcast_enter(SceneType) ->
    lists:member(SceneType, [
            ?SCENE_TYPE_HOT_SPRING
            ,?SCENE_TYPE_GWAR
            ,?SCENE_TYPE_WASTELAND
            ,?SCENE_TYPE_CAMP_WAR
        ]).

%% -------------------------------------------------------------
%% 获取场景角色数量相关信息
%% （跨服场景需要在跨服中心节点调用）
%% -------------------------------------------------------------

%% 获取某场景id下所有场景进程内角色数量
get_role_count(SceneId) ->
    PoolIds = scene_mgr:get_scene_pool_ids(SceneId),
    lists:sum([get_role_count(SceneId, PoolId) || PoolId <- PoolIds]).

%% 获取某场景进程下角色数量
get_role_count(SceneId, PoolId) ->
    case ets:lookup(?SCENE_ROLE_COUNT, {SceneId, PoolId}) of
        [#scene_role_count{num = RoleCount}] ->
            RoleCount;
        _ ->
            0
    end.

%% 获取某场景全部线路角色数量信息
get_pool_role_count(SceneId) ->
    PoolIds = scene_mgr:get_scene_pool_ids(SceneId),
    [
        {PoolId, get_role_count(SceneId, PoolId)} ||
        PoolId <- PoolIds
    ].

get_pool_role_count(SceneId, {PoolMin, PoolMax}) ->
    PoolIds = scene_mgr:get_scene_pool_ids(SceneId),
    [
        {PoolId, get_role_count(SceneId, PoolId)} ||
        PoolId <- PoolIds,
        PoolId >= PoolMin andalso PoolId =< PoolMax
    ].

%% -------------------------------------------------------------
%% 跨服功能相关逻辑接口
%% -------------------------------------------------------------

%% 是否跨服主城
is_center_city(SceneId) ->
    SceneId =:= 1016.

%% 游戏服跨服主城及护送场景id映射
%% 返回结果可能为原始SceneId 或 对应的CenterSceneId、LocalSceneId
%% -> NewSceneId
%%    NewSceneId =:= SceneId : 非跨服主城关联场景 且 非跨服护送关联场景
%%    NewSceneId =/= SceneId : 跨服主城与跨服护送对应SceneId的映射场景
center_city_map(SceneId) ->
    SceneMapList = [{1003, 1016}, {1006, 1017}],
    find_city_scene(SceneId, SceneMapList).

find_city_scene(SceneId, [{LocalSceneId, CenterSceneId} | Others]) ->
    if
        LocalSceneId =:= SceneId ->
            CenterSceneId;
        CenterSceneId =:= SceneId ->
            LocalSceneId;
        true ->
            find_city_scene(SceneId, Others)
    end;
find_city_scene(SceneId, []) ->
    SceneId.

%% 切换场景时，按跨服主城/护送场景的规则，修正进入的场景
change_fix_city_scene(Player, SceneId) ->
    case data_scene:get(SceneId) of
        #ets_scene{cls_type = ClsType} = SceneData ->
            case ClsType =:= ?SCENE_CLS_TYPE_GAME of
                true  ->
                    %% 游戏服节点的场景，尝试转换为相应跨服场景
                    case center_city_map(SceneId) of
                        SceneId -> SceneId;
                        CenterSceneId ->
                            CenterSceneData = data_scene:get(CenterSceneId),
                            case check_fix_city_scene_2(Player, CenterSceneData) of
                                true  -> CenterSceneId;
                                false -> SceneId
                            end
                    end;
                false ->
                    %% 跨服中心节点的场景
                    case center_city_map(SceneId) of
                        SceneId -> SceneId;
                        LocalSceneId ->
                            case check_fix_city_scene_2(Player, SceneData) of
                                true  -> SceneId;
                                false -> LocalSceneId
                            end
                    end
            end;
        _ ->
            SceneId
    end.

%% 检查跨服主城/护送场景是否满足基本的进入条件
%% 内部接口，不了解的不要调用，
%% 要求传入参数，SceneData为跨服主城或跨服护送场景的配置数据，不能是其他场景
%% check_fix_city_scene_2(Player, SceneData) -> true | false.
check_fix_city_scene_2(Player, #ets_scene{id = SceneId, requirement = Requirement}) ->
    case check_requirement(Player, Requirement) of
        true ->
            #player_status{server_id = ServerId} = Player,
            case is_center_city(SceneId) of
                true  ->    % 跨服主城
                    czone_api:zone_enter_check_city(ServerId);
                false ->    % 非跨服主城（即跨服护送场景）
                    case czone_api:zone_enter_base_check(ServerId) of
                        {true, _} -> true;
                        false     -> false
                    end
            end;
        _Err ->
            false
    end;
check_fix_city_scene_2(_Player, _) ->
    false.

%% 检查是否可主动切换线路的场景
check_can_active_switch(SceneType) ->
    lists:member(SceneType, ?SCENE_TYPE_CAN_ACTIVE_SWITCH).

%% 节点断开与跨服中心的连接，需要将玩家踢出场景
node_disconnect_kickout(Player = #player_status{
        id = RoleId, figure = #figure{name = Name},
        scene = SceneId, scene_pool_id = OldScenePoolId,
        action_lock = ActionLock
    }) ->
    case data_scene:get(SceneId) of
        #ets_scene{cls_type = ?SCENE_CLS_TYPE_CENTER, name = SceneName} ->
            ?ERR("center node down, kickout role[~w,~ts], old scene:~w,~w,~ts break Lock:~w",
                [RoleId, Name, SceneId, OldScenePoolId, SceneName, ActionLock]
            ),
            change_scene(Player#player_status{action_lock = free}, 0, 0, 0, 0, 0, []);
        _ ->
            Player
    end.

%% -------------------------------------------------------------
%% 传送门相关检测处理
%% -------------------------------------------------------------

%% 前端移动一格可能会多移动100像素
%% 传送门附近的距离标准大约要350像素才比较充分

%% 传送门距离检测
check_scene_gate_near(DstSceneId, PlayerSceneElem, PlayerX, PlayerY) ->
    case lists:keyfind(DstSceneId, 1, PlayerSceneElem) of
        {_, GateSrcX, GateSrcY, GateDstX, GateDstY} ->
            case fuzzy_nearby(PlayerX, PlayerY, GateSrcX, GateSrcY, 350) of
                true  ->
                    {true, GateDstX, GateDstY};
                false ->
                    false
            end;
        _ ->
            false
    end.

check_scene_gate_any(PlayerSceneElem, DstSceneElem, PlayerX, PlayerY, DstX, DstY) ->
    IsAnyGateNear = lists:any(fun({_DstSceneId, GateSrcX, GateSrcY, _GateDstX, _GateDstY}) ->
                fuzzy_nearby(PlayerX, PlayerY, GateSrcX, GateSrcY, 350);
            (_) ->
                false
        end, PlayerSceneElem),
    case IsAnyGateNear of
        true  ->
            DstGatePosList = [{GateSrcX, GateSrcY} || {_, GateSrcX, GateSrcY, _, _} <- DstSceneElem],
            case DstGatePosList of
                [_ | _] ->
                    {NearestPosX, NearestPosY} = find_nearest_pos(DstX, DstY, DstGatePosList),
                    {true, NearestPosX, NearestPosY};
                _ ->    % 目的地无传送门
                    false
            end;
        false ->    % 不在当前场景传送门附近
            false
    end.

%% 从坐标列表中，查找距离目标点最近的坐标点
find_nearest_pos(DstX, DstY, [HeadPos = {X, Y} | PosList]) ->
    DistancePower = umath:distance_power(X, Y, DstX, DstY),
    find_nearest_pos(DstX, DstY, PosList, HeadPos, DistancePower);
find_nearest_pos(_DstX, _DstY, _) ->
    false.

find_nearest_pos(DstX, DstY, [Pos = {X, Y} | Others], NearestPos, NearestPower) ->
    DistancePower = umath:distance_power(X, Y, DstX, DstY),
    case DistancePower < NearestPower of
        true  ->
            find_nearest_pos(DstX, DstY, Others, Pos, DistancePower);
        false ->
            find_nearest_pos(DstX, DstY, Others, NearestPos, NearestPower)
    end;
find_nearest_pos(_DstX, _DstY, _PosList, NearestPos, _NearestPower) ->
    NearestPos.

%% 模糊检测是否附近（非真实距离）
fuzzy_nearby(X, Y, X2, Y2, NearbyDiff) ->
    abs(X - X2) =< NearbyDiff andalso
    abs(Y - Y2) =< NearbyDiff.

