%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 场景进程池管理
%%% @end
%%% -------------------------------------------------------
-module(scene_mgr).
-behaviour(gen_server).
%% API
-export([
        start_link/0                    % 启动管理器
        ,start_scene/3                  % 开启新场景进程
        ,scene_terminate/3              % 场景进程中止时回调通知
        ,check_lock_enter/6             % 检测可否进入场景，锁定进入名额
        ,release_lock_enter/4           % 清理场景进入等待锁
        ,fetch_scene/2                  % 从场景进程池管理器中分配一个工作进程
        ,fetch_scene/4                  % 从场景进程池管理器中分配一个工作进程
        ,get_scene_pool_ids/1           % 获取指定场景id相应的活动进程pool_id列表
        ,get_scenes_pool_ids/1          % 获取指定场景id列表相应的活动进程pool_id列表
        ,get_zone_scene_pool_ids/2      % 获取指定分区场景id相应的活动进程pool_id列表
        ,manual_divide_pool/2           % 手动分配分线
        ,scene_dungeon_sub_line/1       % 判断是否自动分线型的特殊副本场景并返回分线阈值
        ,get_min_unlist_id/2            % 找出从指定值开始递增，列表的首个中断点数值
        ,apply_cast/2                   % 异步去相应分线处理
    ]).
-export([
        is_auto_sub_scene/2             % 是否自动分线场景
        ,is_force_sub_scene/2           % 是否强制自动分线场景
        ,get_pool_scope/1               % 自动分线的线路编号范围
        ,get_pool_zone/1                % 根据分线计算当前分区
        ,calc_pool_base/3               % 分区分线初始编号计算
    ]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
%% only for debug / gm
-export([
        get_scene_count/0               % 获得场景总数量
        ,gm_start_scene/2
    ]).
%% only for test
-export([
        find_apt_room/3
        ,gm_fetch_scene/4               % 模拟测试并发请求分配场景
    ]).

-include("scene.hrl").
-include("common.hrl").
-include("clusters.hrl").
-include("predefine.hrl").

%% 场景数据
-record(state, {
        scenes = #{}                    % #{SceneId => #scene_info{}}
        ,timer                          % 场景池清理定时器
    }).

%% 场景进程池管理器中各场景进程相关信息
-record(scene_info, {
        type = 0                        % 场景类型
        ,cls_type = 0                   % 是否跨服
        ,pool = #{}                     % #{PoolId => ScenePid}
    }).

%% 保存FETCH_TIME与CLEAN_TIME的原因相似，都是为了避免并发情况下，
%% 分线短时间内刚好处于清理与分配的情况下，导致的玩家旧的（进程字典）尚未清理掉场景进程Pid记录，
%% 而引起的进入场景时，无法加载怪物问题
%% 问题情形举例：
%% 情形1：
%%      玩家（或副本等）请求分配场景，场景分配了空闲分线X
%%      场景管理器检查需要清理的场景，空闲分线X符合场景人数更新的清理条件，执行清理
%%      玩家向旧的Pid请求进入场景（曾经进过该分线，有场景Pid记录）
%%      分线清理消息抵达该服该玩家进程
%% 情形2：
%%      场景管理器检查需要清理的场景，空闲分线X符合场景人数更新的清理条件，执行清理
%%      玩家（或副本等）请求分配场景，场景分配时，又计算到了刚清理的分线X同样的PoolId值
%%      玩家向旧的Pid请求进入场景（曾经进过该分线，有场景Pid记录）
%%      分线清理消息抵达该服该玩家进程
%% 由于清理的过程是个异步过程，执行时间无法精确确定，
%% 如果不对清理、分配时间做控制，以上两种情形都是有可能的发生的，
%% 所以为了更好避免场景问题，增加两个时间的记录，辅助场景的清理和分配管理

%% 保存锁定进入名额用的进程字典KEY（以保证尽可能精准地限制场景进入人数、自动分线人数）
-define(WAIT_LIST_KEY(SceneId, PoolId), {wait_list, SceneId, PoolId}).
%% 保存场景最近分配出去的时间（避免刚分配出去就回收的临界情况，导致见不到怪物问题）
-define(FETCH_TIME(SceneId, PoolId), {fetch_time, SceneId, PoolId}).
%% 保存场景最近清理的PoolId及时间信息（避免刚回收，立即重新分配到该PoolId，理论上可能存在的问题）
-define(CLEAN_TIME(SceneId), {clean_time, SceneId}).

%% 时间常量定义
-define(CLEAN_TIME, 10 * 120).          % 场景池清理检测间隔(s)
-define(CLEAN_AFTER_FETCH, 10).         % 在最后分配场景分线之后多长时间允许清理
-define(FETCH_AFTER_CLEAN, 10).         % 在新分配时，刚清理掉的PoolId不允许直接使用的时间

%% WAIT_LIST清理时间，用于异步加载场景过程中限制进入场景的人数
%% 此值需要评估客户端加载场景资源的时间消耗，通常给予1~2秒宽限
%% 也不宜太大，因为会影响scene_mgr的效率
%% 目前版本客户端评估较慢的正常机器，大约6~8秒才能加载完进入场景，所以限制9秒
-define(WAIT_CLEAN, 9).

%% 定义常规自动分线型，分区之间PoolId间隔
%% （即单分区理论分线数上限，设得比较大是为了实际上不可能造成重叠，代码中无需检查）
%% 非常规自动分线，由相应的功能管理分线编号
-define(ZONE_POOL_COUNT, 1000).

%%%===================================================================
%%% API
%%%===================================================================

start_scene(Id, PoolId, SceneType) ->
    gen_server:call(?MODULE, {'start_scene', Id, PoolId, SceneType}).

%% 场景子进程非关服退出时，需要清理，由场景子进程terminate时调用
scene_terminate(Id, PoolId, ScenePid) ->
    gen_server:cast(?MODULE, {'scene_terminate', Id, PoolId, ScenePid}).

%% 检测可否进入场景，根据场景所在类型，决定是否锁定进入名额
%% 实际上只是短时间记录有等待列表，超时或进入则释放名额，
%% 且通过ETS查询，高并发时并不是完全准确的人数
%% 注意：check_lock_enter接口在游戏服节点专用，跨服中心不能直接调用
%% LockResult : true 表示锁定成功，可以切换；false表示失败，不能切换
-spec check_lock_enter(RoleId, SceneId, PoolId, CopyId, SceneType, SceneClsType) -> LockResult when
    RoleId :: integer(), SceneId :: non_neg_integer(), PoolId :: non_neg_integer(), CopyId :: any(),
    SceneType :: integer(), SceneClsType :: integer(), LockResult :: boolean().
check_lock_enter(RoleId, SceneId, PoolId, CopyId, SceneType, SceneClsType)->
    case is_auto_sub_scene(SceneId, SceneType) of
        true  ->
            case SceneClsType of
                ?SCENE_CLS_TYPE_GAME ->
                    gen_server:call(?MODULE, {'check_lock_enter', RoleId, SceneId, PoolId, CopyId});
                ?SCENE_CLS_TYPE_CENTER ->
                    clusters_node:apply_call(
                        gen_server, call, [?MODULE, {'check_lock_enter', RoleId, SceneId, PoolId, CopyId}]
                    )
            end;
        false ->
            false
    end.

%% 由场景进程在玩家进入时调用以释放锁定
release_lock_enter(RoleId, SceneId, PoolId, SceneType) ->
    case is_auto_sub_scene(SceneId, SceneType) of
        true  -> gen_server:cast(?MODULE, {'release_lock_enter', RoleId, SceneId, PoolId});
        false -> ignore
    end.

%%
apply_cast(SceneId,MFA) ->
    gen_server:cast(?MODULE, {'apply_cast', SceneId, MFA}).


%% 从场景进程池管理器中获取场景工作进程信息
%% 注意：通常本接口游戏服专用，跨服中心慎用
%%      需要RoleId | RoleIdList是因为需要检查是否已经在排队列表，这样计算人数才相对正确
-spec fetch_scene(SceneId, RoleIdOrList) -> {PoolId, CopyId} | error when
    SceneId :: non_neg_integer(),
    RoleIdOrList :: [RoleId] | RoleId, RoleId :: integer(),
    PoolId  :: integer(),
    CopyId  :: integer().
fetch_scene(SceneId, RoleIdOrList) ->
    case data_scene:get(SceneId) of
        #ets_scene{type = SceneType, cls_type = ClsType} ->
            fetch_scene(SceneId, SceneType, ClsType, RoleIdOrList);
        _ ->
            error
    end.

fetch_scene(SceneId, _SceneType, SceneClsType, RoleIdList = [_|_]) ->
    IsCenterNode  = clusters_api:is_center(),
    NewRoleIdList = case length(RoleIdList) > 10 of
        true  -> lists:sublist(RoleIdList, 10);
        false -> RoleIdList
    end,
    case IsCenterNode of
        false ->
            PoolBase = calc_pool_base(IsCenterNode, SceneId, RoleIdList),
            case is_integer(PoolBase) of
                true  ->
                    case SceneClsType of
                        ?SCENE_CLS_TYPE_GAME ->
                            gen_server:call(?MODULE, {'fetch_scene', SceneId, NewRoleIdList, PoolBase}, 3000);
                        ?SCENE_CLS_TYPE_CENTER ->
                            clusters_node:apply_call(
                                gen_server, call, [?MODULE, {'fetch_scene', SceneId, NewRoleIdList, PoolBase}, 3000]
                            );
                        _ -> error
                    end;
                false ->
                    %% 对于未完成跨服聊天分区的服，不允许进入跨服主城，转向请求本服主城
                    %% （请求重定向由lib_scene -> auto_select_scene接口处理）
                    error
            end;
        true  ->
            case SceneClsType of
                ?SCENE_CLS_TYPE_GAME ->
                    %% 在跨服中心节点请求进入游戏服节点场景
                    %% 不应该出现这种情况的调用，应为开发者使用方式不正确
                    error;
                ?SCENE_CLS_TYPE_CENTER ->
                    PoolBase = calc_pool_base(IsCenterNode, SceneId, RoleIdList),
                    case is_integer(PoolBase) of
                        true  -> gen_server:call(?MODULE, {'fetch_scene', SceneId, NewRoleIdList, PoolBase}, 3000);
                        false ->
                            %% 在跨服中心节点请求进入跨服中心场景，但是尚无对应的场景分线
                            %% 不应如此调用，用法错误，应修正为在玩家个人所在服请求进入本服主城
                            error
                    end;
                _ ->
                    error
            end
    end;
fetch_scene(SceneId, SceneType, SceneClsType, RoleId) ->
    fetch_scene(SceneId, SceneType, SceneClsType, [RoleId]).

%% 获取指定场景id相应的活动进程pool_id列表
get_scene_pool_ids(SceneId) ->
    gen_server:call(?MODULE, {'get_scene_pool_ids', SceneId}).

%% 获取指定场景id列表相应的活动进程pool_id列表
%% return: #{scene => pool_list}
get_scenes_pool_ids(SceneList) ->
    gen_server:call(?MODULE, {'get_scenes_pool_ids', SceneList}).

%% 获取指定分区场景id相应的活动进程pool_id列表
get_zone_scene_pool_ids(ZoneId, SceneId) ->
    gen_server:call(?MODULE, {'get_zone_scene_pool_ids', ZoneId, SceneId}).

start_link() ->
    gen_server:start_link({local,?MODULE}, ?MODULE, [], []).

%% 手动分配分线
manual_divide_pool(SceneId, RoleList) ->
    case scene_dungeon_sub_line(SceneId) of
        false -> 0;
        _Other ->
            case fetch_scene(SceneId, RoleList) of
                {PoolId, _} -> PoolId;
                _ -> 0
            end
    end.

%% 判断是否自动分线型的特殊副本场景并返回分线阈值
%% 这类特殊场景是根据策划的场景使用情况，结合线上游戏服负载数据调整的人数限制
%% 由于没有合适的规律可供判断，干脆就不加额外的配置，直接在代码里控制了
-spec scene_dungeon_sub_line(SceneId :: non_neg_integer()) -> RoleCountLimit :: integer() | false.
scene_dungeon_sub_line(SceneId) ->
    %% SceneList = [{SceneId, MaxRolePerLine}],
    %% proplists:get_value(SceneId, SceneList, false).
    %% 数量多时，map比list查询优秀很多
    SceneMap = #{
        2001  => 20,            % 个人BOSS
        2002  => 20,            % 个人BOSS
        2003  => 20,            % 个人BOSS
        2101  => 10,            % 转职
        2201  => 10,            % 座驾副本
        2202  => 10,            % 宝具副本
        2203  => 10,            % 伙伴副本
        2204  => 10,            % 神翼副本
        2205  => 10,            % 武器副本
        2206  => 10,            % 磁炮副本
        2207  => 10,            % 幻甲副本
        2208  => 10,            % 星翼副本
        2211  => 10,            % 幻光副本
        5001  => 10,            % 经验副本场景
        7001  => 20,            % 爬塔副本场景
        2500  =>  5,            % 宠物副本场景
        2300  =>  5,            % 宝宝副本
        8600  =>  5,            % 星辰副本
        3100  =>  3,            % 3人守塔场景
        8000  =>  6,            % 讨伐副本场景1
        8001  =>  6,            % 讨伐副本场景2
        8002  =>  6,            % 讨伐副本场景3
        8003  =>  6,            % 讨伐副本场景4
        8004  =>  6,            % 讨伐副本场景5
        8005  =>  6,            % 讨伐副本场景6
        8006  =>  6,            % 讨伐副本场景7
        8007  =>  6,            % 讨伐副本场景8
        8008  =>  6,            % 讨伐副本场景9
        8009  =>  6,            % 讨伐副本场景10
        8010  =>  6,            % 讨伐副本场景11
        8011  =>  6,            % 讨伐副本场景12
        8012  =>  6,            % 讨伐副本场景13
        8013  =>  6,            % 讨伐副本场景14
        8014  =>  6,            % 讨伐副本场景15
        8015  =>  6,            % 讨伐副本场景16
        8016  =>  6,            % 讨伐副本场景17
        8017  =>  6,            % 讨伐副本场景18
        8018  =>  6,            % 讨伐副本场景19
        8019  =>  6,            % 讨伐副本场景20
        8020  =>  6,            % 讨伐副本场景21
        8021  =>  6,            % 讨伐副本场景22
        8022  =>  6,            % 讨伐副本场景23
        8023  =>  6,            % 讨伐副本场景24
        8024  =>  6,            % 讨伐副本场景25
        8025  =>  6,            % 讨伐副本场景26
        8026  =>  6,            % 讨伐副本场景27
        8027  =>  6,            % 讨伐副本场景28
        9001  => 10,            % 沙盘副本
        9002  => 10,            % 沙盘副本
        9003  => 10,            % 沙盘副本
        9004  => 10,            % 沙盘副本
        10011 => 20,            % 大世界镜像场景
        10021 => 20,
        10031 => 20,
        10041 => 20,
        10051 => 20,
        10061 => 20,
        10071 => 20,
        10081 => 20,
        10091 => 20,
        10101 => 20,
        10111 => 20,
        10121 => 20,
        10131 => 20,
        10141 => 20,
        10151 => 20,
        10161 => 20,
        10171 => 20,
        20010 => 50           % 跳一跳活动场景
    },
    maps:get(SceneId, SceneMap, false).

%% 获取场景个数（调试专用，不要在可能频繁调用的场合使用）
get_scene_count() ->
    gen_server:call(?MODULE, 'get_scene_count').

%% 检查是否自动分线场景
is_auto_sub_scene(SceneId, SceneType) ->
    is_force_sub_scene(SceneId, SceneType) orelse
    scene_dungeon_sub_line(SceneId) =/= false.

%% 检查是否强制自动分线场景
is_force_sub_scene(SceneId, SceneType) ->
    lists:member(SceneType, ?SCENE_TYPE_NEED_SUB_LINE) orelse
    lib_scene:is_center_city(SceneId).

%% scene_mgr自动管理分线的场景分线编号范围
-spec get_pool_scope(SceneId) -> {PoolMin, PoolMax} | false when
    SceneId :: non_neg_integer(),
    PoolMin :: non_neg_integer(), PoolMax :: non_neg_integer().
get_pool_scope(SceneId) ->
    %% {0, ?ZONE_POOL_COUNT - 1}.
    case lib_scene:is_clusters_scene(SceneId) of
        true  ->
            ZoneId = czone_api:env_get_zone_id(),
            case is_integer(ZoneId) of
                true  ->
                    PoolBase = zone_to_pool_base(ZoneId),
                    {PoolBase, PoolBase + ?ZONE_POOL_COUNT - 1};
                false ->
                    false
            end;
        false ->
            {0, ?ZONE_POOL_COUNT - 1}
    end.

%% GM专用，强制开启线路（不要调用）
gm_start_scene(SceneId, PoolId) when is_integer(PoolId) andalso PoolId > 0 ->
    PoolIds = get_scene_pool_ids(SceneId),
    case lists:member(PoolId, PoolIds) of
        true  -> ignore;
        false ->
            case data_scene:get(SceneId) of
                #ets_scene{type = SceneType} ->
                    start_scene(SceneId, PoolId, SceneType);
                _ ->
                    ignore
            end
    end;
gm_start_scene(SceneId, _) ->
    PoolIds = get_scene_pool_ids(SceneId),
    PoolId  = get_min_unlist_id(PoolIds),
    case data_scene:get(SceneId) of
        #ets_scene{type = SceneType} ->
            start_scene(SceneId, PoolId, SceneType);
        _ ->
            ignore
    end.

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

init([]) ->
    process_flag(trap_exit, true),
    process_flag(priority, high),
    %% 场景进程人数记录，由各场景进程维护数据，供其他进程需要的时候查询使用
    ets:new(?SCENE_ROLE_COUNT,  [{keypos, #scene_role_count.key}, named_table, public, set, {write_concurrency, true}]),
    ClsType = config:get_cls_type(),
    proc_lib:spawn_link(fun() -> open_all_scene(ClsType) end),
    Timer = erlang:send_after(?CLEAN_TIME * 1000, self(), 'check_clean'),
    {ok, #state{scenes = #{}, timer = Timer}}.

handle_call(Req, From, Status) ->
    case catch do_handle_call(Req, From, Status) of
        {'EXIT', _ErrorReason} ->
            ?ERR("handle_call error, req:~w, From:~w, reason:~w", [Req, From, _ErrorReason]),
            {reply, error, Status};
        Ret ->
            Ret
    end.

handle_cast(Msg, Status) ->
    case catch do_handle_cast(Msg, Status) of
        {'EXIT', _ErrorReason} ->
            ?ERR("handle_cast error, msg:~w, reason:~w", [Msg, _ErrorReason]),
            {noreply, Status};
        Ret ->
            Ret
    end.

handle_info(Info, Status) ->
    case catch do_handle_info(Info, Status) of
        {'EXIT', _ErrorReason} ->
            ?ERR("handle_info error, info:~w, reason:~w", [Info, _ErrorReason]),
            {noreply, Status};
        Ret ->
            Ret
    end.

terminate(_Reason, _) ->
    ok.

code_change(_OldVsn, Status, _Extra)->
    {ok, Status}.


%%%===================================================================
%%% handle_call/handle_cast/handle_info Internal functions
%%%===================================================================

%% 获取场景个数
do_handle_call('get_scene_count', _From, Status = #state{scenes = ScenesMaps}) ->
    SceneNum = maps:fold(
        fun(_SceneId, #scene_info{pool = ScenePoolMaps}, Num) ->
                maps:size(ScenePoolMaps) + Num
        end, 0, ScenesMaps),
    {reply, SceneNum, Status};

%% 获取一个新的场景进程worker
do_handle_call({'fetch_scene', SceneId, RoleIdList, PoolBase}, _From, Status = #state{scenes = ScenesMaps}) ->
    {Reply, NewStatus} =
    try
        NowTime = utime:unixtime(),
        case select_scene_room(RoleIdList, SceneId, ScenesMaps, PoolBase, NowTime) of
            {false, SceneType, ClsType, IsAutoSubScene} ->
                %% 需要创建新的场景PoolWorker（ScenesMaps中可能缺失场景信息，也可能存在信息但线路人数已满）
                {PoolId, ScenePoolMaps} =
                case maps:find(SceneId, ScenesMaps) of
                    {ok, #scene_info{pool = ScenePoolMapsX}} ->
                        ScenePoolIds = maps:keys(ScenePoolMapsX),
                        %% 排除掉最近很短时间刚刚清理掉的场景PoolId，进行分配，避免临界点问题
                        CleanPoolsFewTime = get_clean_pools_few_time(SceneId, NowTime),
                        {get_min_unlist_id(CleanPoolsFewTime ++ ScenePoolIds, PoolBase), ScenePoolMapsX};
                    error -> {PoolBase, #{}}
                end,
                case mod_scene_agent:start_scene(SceneId, PoolId) of
                    Pid when is_pid(Pid) ->
                        NewScenePoolMaps= ScenePoolMaps#{PoolId => Pid},
                        SceneInfo       = #scene_info{type = SceneType, cls_type = ClsType, pool = NewScenePoolMaps},
                        NewScenesMaps   = ScenesMaps#{SceneId => SceneInfo},
                        check_join_save_wait_list(IsAutoSubScene, RoleIdList, NowTime, SceneId, PoolId),
                        put_fetch_time(SceneId, PoolId, NowTime),
                        {{PoolId, 0}, Status#state{scenes = NewScenesMaps}};
                    _ ->
                        {error, Status}
                end;
            {PoolId, CopyId, IsAutoSubScene} ->
                check_join_save_wait_list(IsAutoSubScene, RoleIdList, NowTime, SceneId, PoolId),
                put_fetch_time(SceneId, PoolId, NowTime),
                {{PoolId, CopyId}, Status};
            error ->    %% 错误的场景配置
                {error, Status}
        end
    catch
        ?EXCEPTION(_Class, _Reason, _Stack) ->
            ?ERR("fetch_scene error : {~p,~p}~n  ~p", [_Class, _Reason, ?GET_STACK(_Stack)]),
            {error, Status}
    end,
    {reply, Reply, NewStatus};

%% 由管理进程开启新场景
do_handle_call({'start_scene', Id, PoolId, _SceneType}, _From, #state{scenes = ScenesMaps} = Status) ->
    {Reply, NewStatus} =
    try
        {ScenePoolMaps, ScenePid} =
        case maps:find(Id, ScenesMaps) of
            {ok, #scene_info{pool = ScenePoolMapsX}} ->
                case maps:find(PoolId, ScenePoolMapsX) of
                    {ok, Pid} -> {ScenePoolMapsX, Pid};
                    error     -> {ScenePoolMapsX, false}
                end;
            error -> {#{}, false}
        end,
        case is_pid(ScenePid) andalso misc:is_process_alive(ScenePid) of
            true  ->
                {ScenePid, Status};
            false ->
                case data_scene:get(Id) of
                    #ets_scene{type = SceneType, cls_type = ClsType} ->
                        case mod_scene_agent:start_scene(Id, PoolId) of
                            NewScenePid when is_pid(NewScenePid) ->
                                NewScenePoolMaps= ScenePoolMaps#{PoolId => NewScenePid},
                                SceneInfo       = #scene_info{type = SceneType, cls_type = ClsType, pool = NewScenePoolMaps},
                                NewScenesMaps   = ScenesMaps#{Id => SceneInfo},
                                {NewScenePid, Status#state{scenes = NewScenesMaps}};
                            _Other ->
                                ?ERR("scene id:~w, pool id:~w, start result:~p~n", [Id, PoolId, _Other]),
                                {undefined, Status}
                        end;
                    _ ->
                        ?ERR("scene id:~w, pool id:~w, could not get config~n", [Id, PoolId]),
                        {undefined, Status}
                end
        end
    catch
        ?EXCEPTION(_Class, _Reason, _Stack) ->
            ?ERR("scene id:~w, pool id:~w, start result:{~p,~p}~n", [Id, PoolId, _Class, _Reason]),
            {undefined, Status}
    end,
    {reply, Reply, NewStatus};

%% 检测可否进入场景，锁定进入名额
do_handle_call({'check_lock_enter', RoleId, SceneId, PoolId, CopyId}, _From, Status)->
    SceneKey = {SceneId, PoolId},
    NowTime  = utime:unixtime(),
    Res =
    case ets:lookup(?SCENE_ROLE_COUNT, SceneKey) of
        [#scene_role_count{num = RoleCount, num_maps = NumMaps}] ->
            SceneMaxRole = lib_scene:get_scene_max_role(SceneId),
            RoomMaxRole  = lib_scene:get_room_max_role(SceneId),
            {_IsClearUpdate, WaitList} = get_scene_wait_list(SceneId, PoolId, NowTime),
            WaitCount = length(WaitList),
            CopyRoleCount = maps:get(CopyId, NumMaps, 0),
            if
                RoleCount + WaitCount >= SceneMaxRole ->
                    false;
                CopyRoleCount + WaitCount >= RoomMaxRole ->
                    %% 这里其实要判断特定场景房间号等待人数，
                    %% 这里直接粗略以整个场景的WaitCount来判断省事，而且只会加强限制，并无不良影响
                    false;
                true ->
                    NewWaitList = join_scene_wait_list(RoleId, NowTime, WaitList),
                    put_wait_list(SceneId, PoolId, NewWaitList),
                    true
            end;
        _ ->    %% 指定的场景线路可能未开启，不给进入
            false
    end,
    {reply, Res, Status};

do_handle_call({'get_scene_pool_ids', SceneId}, _From, Status = #state{scenes = ScenesMaps}) ->
    PoolIds =
    case maps:find(SceneId, ScenesMaps) of
        {ok, #scene_info{pool = ScenePoolMaps}} ->
            maps:keys(ScenePoolMaps);
        _ ->
            []
    end,
    {reply, PoolIds, Status};

do_handle_call({'get_scenes_pool_ids', SceneList}, _From, Status = #state{scenes = ScenesMaps}) ->
    Fun = fun(SceneId, AccMap) ->
        PoolIds =
        case maps:find(SceneId, ScenesMaps) of
            {ok, #scene_info{pool = ScenePoolMaps}} ->
                maps:keys(ScenePoolMaps);
            _ ->
                []
        end,
        maps:put(SceneId, PoolIds, AccMap)
    end,
    ScenePoolsMap = lists:foldl(Fun, #{}, SceneList),
    {reply, ScenePoolsMap, Status};

do_handle_call({'get_zone_scene_pool_ids', ZoneId, SceneId}, _From, Status = #state{scenes = ScenesMaps}) ->
    PoolIds =
        case maps:find(SceneId, ScenesMaps) of
            {ok, #scene_info{pool = ScenePoolMaps}} ->
                [ PoolId || PoolId <- maps:keys(ScenePoolMaps), get_pool_zone(PoolId) =:= ZoneId];
            _ ->
                []
        end,
    {reply, PoolIds, Status};

do_handle_call(_R , _From, Status) ->
    {reply, ok, Status}.

%% 释放场景等待名额锁
do_handle_cast({'release_lock_enter', RoleId, SceneId, PoolId}, Status) ->
    NowTime  = utime:unixtime(),
    {IsClearUpdate, WaitList} = get_scene_wait_list(SceneId, PoolId, NowTime),
    case IsClearUpdate orelse WaitList =/= [] of
        true  ->
            NewWaitList = lists:keydelete(RoleId, 1, WaitList),
            put_wait_list(SceneId, PoolId, NewWaitList);
        false ->
            ignore
    end,
    {noreply, Status};

%%分线场景通知分线处理
do_handle_cast({'apply_cast', SceneId, [Mod,Fun,Args]},Status = #state{scenes = ScenesMaps})->
    PoolIds =
    case maps:find(SceneId, ScenesMaps) of
        {ok, #scene_info{pool = ScenePoolMaps}} ->
            maps:keys(ScenePoolMaps);
        _ ->
            []
    end,
    [apply(Mod,Fun,[Pool|Args])||Pool<-PoolIds],
    {noreply,Status};

do_handle_cast({'scene_terminate', Id, PoolId, ScenePid}, Status = #state{scenes = ScenesMaps}) ->
    erase_wait_list(Id, PoolId),
    erase_fetch_time(Id, PoolId),
    NewScenesMaps =
    case maps:find(Id, ScenesMaps) of
        {ok, SceneInfo = #scene_info{pool = ScenePoolMaps}} ->
            case maps:find(PoolId, ScenePoolMaps) of
                {ok, ScenePid} ->   % 场景信息符合，移除
                    NewScenePoolMaps = maps:remove(PoolId, ScenePoolMaps),
                    case maps:size(NewScenePoolMaps) =:= 0 of
                        true  ->
                            maps:remove(Id, ScenesMaps);
                        false ->
                            NewSceneInfo = SceneInfo#scene_info{pool = NewScenePoolMaps},
                            ScenesMaps#{Id => NewSceneInfo}
                    end;
                {ok, _Pid} ->   % 场景信息不符合，忽略
                    ScenesMaps;
                error ->
                    ScenesMaps
            end;
        error ->
            ScenesMaps
    end,
    {noreply, Status#state{scenes = NewScenesMaps}};

do_handle_cast(_R , Status) ->
    {noreply, Status}.

%% 检查清理多余的场景进程、清理场景过期数据和内存检测等
do_handle_info('check_clean', #state{scenes = ScenesMaps, timer = Timer} = Status) ->
    util:cancel_timer(Timer),
    NewTimer        = erlang:send_after(?CLEAN_TIME * 1000, self(), 'check_clean'),
    NowTime         = utime:unixtime(),
    ClearScenesKeys = check_clean(maps:to_list(ScenesMaps), NowTime, []),
    NewScenesMaps   = clean_scenes(ScenesMaps, ClearScenesKeys, NowTime),
    %% 检测清理过期信息和回收资源
    monitor_scene_list(NewScenesMaps),
    {noreply, Status#state{scenes = NewScenesMaps, timer = NewTimer}};

do_handle_info(_Reason, Status) ->
    {noreply, Status}.

%%%===================================================================
%%% Internal functions
%%%===================================================================

%% 开启所有场景
open_all_scene(ClsType) ->
    L = data_scene:get_id_list(), %% id头位已经区分场景类型，1为普通地图，优先加载
    F = fun(Id) ->
            case data_scene:get(Id) of
                #ets_scene{cls_type = ClsType} ->
                    %% 场景的节点类型符合当前节点类型
                    open_scene(Id);
                #ets_scene{npc = Npc} when ClsType =:= 0 ->
                    %% 节点类型不符，并且是游戏服节点碰到跨服场景，
                    %% 要加载这类场景的NPC数据，便于直接游戏服取数据
                    mod_scene_agent:load_npc(Npc, Id);
                _ ->
                    ignore
            end,
            timer:sleep(20)
    end,
    [ F(Id) || Id <- L ].

open_scene(Id) ->
    % mod_scene_mark:start_link(Id),
    mod_scene_agent:get_scene_pid(Id).
    % mod_scene_mark:load_mask(Id).

%% 选择场景房间 ->  error | {false, SceneType, ClsType, IsAutoSubScene} | {{PoolId, CopyId, IsAutoSubScene}
select_scene_room(RoleIdList, SceneId, ScenesMaps, PoolBase, NowTime) ->
    case maps:find(SceneId, ScenesMaps) of
        {ok, #scene_info{type = SceneType, cls_type = ClsType, pool = ScenePoolMaps}} ->
            %% 除了自动分线类场景，还有半自动分线的特殊副本场景（例如经验副本、巫女副本）
            %% 特殊场景在副本进程调用接口申请时自动分线（但是角色进入时不自动分线，按副本通知的分线信息进入）
            IsAutoSubScene = is_auto_sub_scene(SceneId, SceneType),
            case IsAutoSubScene of
                true  ->
                    PoolIds = maps:keys(ScenePoolMaps),
                    SceneRoleCounts =
                    lists:foldl(fun(PoolId, Acc) when PoolId >= PoolBase andalso PoolId < PoolBase + ?ZONE_POOL_COUNT ->
                                case ets:lookup(?SCENE_ROLE_COUNT, {SceneId, PoolId}) of
                                    [SceneRoleCount = #scene_role_count{num = RealCount}] ->
                                        %% 在判断是否需要开新线路时，场景等待进入人数也计入场景人数，但是不能计算自己
                                        {_, WaitList} = get_scene_wait_list(SceneId, PoolId, NowTime),
                                        WaitCount = count_wait_without_us(WaitList, RoleIdList, 0),
                                        [SceneRoleCount#scene_role_count{num = RealCount + WaitCount} | Acc];
                                    _ -> Acc
                                end;
                            (_, Acc) -> Acc
                        end, [], PoolIds),
                    case find_apt_room(SceneId, SceneType, SceneRoleCounts) of
                        false ->
                            {false, SceneType, ClsType, IsAutoSubScene};
                        {PoolId, CopyId} ->
                            {PoolId, CopyId, IsAutoSubScene}
                    end;
                false ->    %% 暂时此类场景不作分线处理
                    {0, 0, IsAutoSubScene}
            end;
        _ ->
            case data_scene:get(SceneId) of
                #ets_scene{type = SceneType, cls_type = ClsType} ->
                    IsAutoSubScene = is_auto_sub_scene(SceneId, SceneType),
                    {false, SceneType, ClsType, IsAutoSubScene};
                _ ->
                    %% SceneId配置缺失
                    error
            end
    end.

%% 查找合适的场景进程和房间
find_apt_room(_SceneId, _SceneType, []) ->
    false;
find_apt_room(SceneId, SceneType, SceneRoleCounts) ->
    SceneDunSubLineLimit = scene_dungeon_sub_line(SceneId),
    SceneMaxRole =
    case SceneDunSubLineLimit of
        false  -> lib_scene:get_scene_max_role(SceneId);
        _Other -> SceneDunSubLineLimit
    end,
    RoomMaxRole = lib_scene:get_room_max_role(SceneId, SceneMaxRole),
    MaxRoomSize =   %% 强制分线场景只允许一个房间，其他的才允许开多个房间
    case is_force_sub_scene(SceneId, SceneType) of
        true  -> 1;
        false -> max(util:ceil(SceneMaxRole / RoomMaxRole), 1)
    end,
    %% 由于房间数存在特殊设定，需要纠正场景人数上限，用于准确确定筛选线路时的负载阈值
    RealSceneMaxRole= min(SceneMaxRole, RoomMaxRole * MaxRoomSize),
    ThresholdCount  = umath:floor(RealSceneMaxRole * 0.85),
    SortSceneList   = sort_scene(SceneRoleCounts, ThresholdCount, RealSceneMaxRole),
    check_apt_scene_room(SortSceneList, RoomMaxRole, MaxRoomSize).

check_apt_scene_room([SceneRoleCount | Others], RoomMaxRole, MaxRoomSize) ->
    #scene_role_count{key = {_, PoolId}, num_maps = NumMaps} =  SceneRoleCount,
    {CopyId, CopyRoleNum, CopyIds} = find_least_role_scene_room(NumMaps),
    RoomSize = length(CopyIds),
    Ret =
    if
        CopyRoleNum >= RoomMaxRole andalso RoomSize >= MaxRoomSize ->
            %% 没有合适的场景房间，并且已经达到最大场景房间数
            false;
        CopyRoleNum >= RoomMaxRole ->
            %% 场景中没有合适的房间，新建一个房间
            %% 从CopyIds中找到可用的最小CopyId
            MinUnlistCopyId = get_min_unlist_id(CopyIds),
            {PoolId, MinUnlistCopyId};
        MaxRoomSize =:= 1 ->
            %% 正常应该按选取到合适的场景房间即可，不需要这个判断，但由于未知的原因，
            %% 运营服上出现主城和野外超出数量的房间（房间未真实启动怪物加载），
            %% 则可能选取到人数少的异常房间，导致加载怪物异常，所以增加修正处理
            {PoolId, 0};
        true ->
            %% 选取到其他合适的场景房间
            {PoolId, CopyId}
    end,
    case Ret of
        false ->
            check_apt_scene_room(Others, RoomMaxRole, MaxRoomSize);
        _ ->
            Ret
    end;
check_apt_scene_room([], _RoomMaxRole, _MaxRoomSize) ->
    %% 当前没有合适的场景，外部应启动新进程
    false.

%% 按特定的优先规则排出场景次序，方便调用者再从这个有序列表中查找可进入的场景
%% 规则：
%% 1. 设定一个阈值，优先选择未达到阈值的场景（如果有）
%% 2. 未达到阈值的场景中，优先线路编号小的场景
%% 3. 如果所有线路都达到阈值以上，则选择人数较少的场景
%% 4. 如果全部都满员，则新开一个场景
sort_scene(AllSceneRoleCounts, ThresholdCount, MaxRoleCount) ->
    %% 筛选未满人数的场景
    SceneRoleCounts = [
        SceneRoleCount
        || SceneRoleCount = #scene_role_count{num = RoleCount} <- AllSceneRoleCounts,
        RoleCount < MaxRoleCount
    ],
    %% 以指定阈值规则对场景进行排序
    SortFun = fun(A, B) -> sort_scene_threshold(A, B, ThresholdCount) end,
    lists:sort(SortFun, SceneRoleCounts).

%% 以指定阈值、在线数及线路编号决定场景的次序
sort_scene_threshold(
    #scene_role_count{key = {_, PoolIdA}, num = CountA},
    #scene_role_count{key = {_, PoolIdB}, num = CountB},
    ThresholdCount) ->
    if
        %% 均未达到阈值时，以线路编号决定顺序，分线编号小的优先
        CountA < ThresholdCount andalso CountB < ThresholdCount ->
            PoolIdA =< PoolIdB;
        %% 否则（一方或者两方都超阈值），负载低的场景优先
        CountA < CountB -> true;
        CountA > CountB -> false;
        true -> PoolIdA =< PoolIdB
    end.

%% 查找最少在线的场景房间
find_least_role_scene_room(NumMaps) ->
    maps:fold(fun(CopyId, Num, Acc) ->
                case Acc of
                    {0, 0, []} ->
                        {CopyId, Num, [CopyId]};
                    {MinNumCopyId, MinNum, CopyIds} ->
                        case Num < MinNum of
                            true  ->
                                {CopyId, Num, [CopyId | CopyIds]};
                            false ->
                                {MinNumCopyId, MinNum, [CopyId | CopyIds]}
                        end
                end
        end, {0, 0, []}, NumMaps).

get_min_unlist_id(Ids) ->
    get_min_unlist_id(Ids, 0).

%% 找出从指定值开始递增，列表的首个中断点数值
%% 例如 [0,1,2,5,10]，指定从0开始，则第一个中断点数值是3
get_min_unlist_id(Ids, Min) ->
    SortedIds = lists:sort(Ids),
    sub_get_min_unlist_id(SortedIds, Min).

sub_get_min_unlist_id([Id | Others], MinUnlistId) when Id < MinUnlistId ->
    sub_get_min_unlist_id(Others, MinUnlistId);
sub_get_min_unlist_id([Id | Others], MinUnlistId) when Id =:= MinUnlistId ->
    sub_get_min_unlist_id(Others, MinUnlistId + 1);
sub_get_min_unlist_id(_, MinUnlistId) ->
    MinUnlistId.

%% ---------------------------------------------
%% 场景进入提前锁定名额
%% ---------------------------------------------

%% 获取对应场景的等待列表（前端出错不发load_scene时，就靠超时来清理数据了）
%% 这里按时间清理队列可能引起超出场景人数限制（前端或网络异常异常，load_scene太晚，后面又进入场景）
%% 但是不清理的话，如果前端有问题也会引起场景人数太少，综合情况，暂时设置为几秒内，见?WAIT_CLEAN
%% -> {IsClearUpdate, NewWaitList}
get_scene_wait_list(SceneId, PoolId, NowTime) ->
    WaitList = get_wait_list(SceneId, PoolId),
    lists:foldl(
        fun (WaitRole = {_Id, Time}, {IsUpdate, NewList}) when NowTime - Time < ?WAIT_CLEAN ->
                {IsUpdate, [WaitRole | NewList]};
            (_, {_, AccList}) ->
                {true, AccList}
        end, {false, []}, WaitList).

%% 更新保存wait_list，如果为空会清除数据
put_wait_list(SceneId, PoolId, WaitList = [_|_]) ->
    put(?WAIT_LIST_KEY(SceneId, PoolId), WaitList);
put_wait_list(SceneId, PoolId, []) ->
    erase(?WAIT_LIST_KEY(SceneId, PoolId)).

get_wait_list(SceneId, PoolId) ->
    case get(?WAIT_LIST_KEY(SceneId, PoolId)) of
        [_|_] = WaitList ->
            WaitList;
        _ ->
            []
    end.

join_scene_wait_list(RoleIdList, NowTime, SceneId, PoolId) ->
    {_, WaitList} = get_scene_wait_list(SceneId, PoolId, NowTime),
    join_scene_wait_list(RoleIdList, NowTime, WaitList).

%% 添加到等待列表
join_scene_wait_list([RoleId | RoleIdList], NowTime, OldSceneWaitList) ->
    WaitList = [{RoleId, NowTime} | lists:keydelete(RoleId, 1, OldSceneWaitList)],
    join_scene_wait_list(RoleIdList, NowTime, WaitList);
join_scene_wait_list([], _NowTime, WaitList) ->
    WaitList;
join_scene_wait_list(RoleId, NowTime, OldSceneWaitList) ->
    [{RoleId, NowTime} | lists:keydelete(RoleId, 1, OldSceneWaitList)].

check_join_save_wait_list(true, RoleIdList, NowTime, SceneId, PoolId) ->
    % ?INFO("Need part scene, RoleIdList: ~w, scene:~w", [RoleIdList, {SceneId, PoolId}]),
    NewWaitList = join_scene_wait_list(RoleIdList, NowTime, SceneId, PoolId),
    put_wait_list(SceneId, PoolId, NewWaitList);
check_join_save_wait_list(_, _RoleIdList, _NowTime, _SceneId, _PoolId) ->
    ignore.

erase_wait_list(SceneId, PoolId) ->
    erase(?WAIT_LIST_KEY(SceneId, PoolId)).

count_wait_without_us([{WaitRoleId, _Time} | WaitList], RoleIds, Count) ->
    NewCount =
    case lists:member(WaitRoleId, RoleIds) of
        true  -> Count;
        false -> Count + 1
    end,
    count_wait_without_us(WaitList, RoleIds, NewCount);
count_wait_without_us([_ | WaitList], RoleIds, Count) ->
    count_wait_without_us(WaitList, RoleIds, Count);
count_wait_without_us([], _RoleIds, Count) ->
    Count.

%% ---------------------------------------------
%% 场景进程清理检测
%% 清理最近一段时间内人数少于X的场景
%% ---------------------------------------------

put_fetch_time(SceneId, PoolId, NowTime) ->
    Key = ?FETCH_TIME(SceneId, PoolId),
    put(Key, NowTime).

get_fetch_time(SceneId, PoolId) ->
    Key = ?FETCH_TIME(SceneId, PoolId),
    case get(Key) of
        undefined -> 0;
        FetchTime -> FetchTime
    end.

erase_fetch_time(SceneId, PoolId) ->
    Key = ?FETCH_TIME(SceneId, PoolId),
    erase(Key).

get_clean_pools_few_time(SceneId, NowTime) ->
    CleanTimeMap = get_clean_time(SceneId),
    {CleanPoolsFewTime, NewCleanTimeMap} =
    maps:fold(fun(PoolId, CleanTime, Acc) ->
                case NowTime - CleanTime =< ?FETCH_AFTER_CLEAN of
                    true  ->
                        {AccPools, AccMap} = Acc,
                        {[PoolId | AccPools], AccMap#{PoolId => CleanTime}};
                    false ->
                        Acc
                end
        end,
        {[], #{}}, CleanTimeMap),
    put_clean_time(SceneId, NewCleanTimeMap),
    CleanPoolsFewTime.

add_clean_time(SceneId, PoolId, NowTime) ->
    CleanTimeMap = get_clean_time(SceneId),
    NewCleanTimeMap = CleanTimeMap#{PoolId => NowTime},
    put_clean_time(SceneId, NewCleanTimeMap).

get_clean_time(SceneId) ->
    case get(?CLEAN_TIME(SceneId)) of
        undefined -> #{};
        CleanTimeMap -> CleanTimeMap
    end.

put_clean_time(SceneId, CleanTimeMap) ->
    put(?CLEAN_TIME(SceneId), CleanTimeMap).

%% 检测场景是否符合清理条件，收集符合条件的场景信息
check_clean([{SceneId, #scene_info{type = SceneType, pool = ScenePoolMaps}} | Scenes ], NowTime, CleanSceneKeys) ->
    NewCleanSceneKeys =
    case is_auto_sub_scene(SceneId, SceneType) of
        true  ->
            ScenePoolIds = maps:keys(ScenePoolMaps),
            lists:foldl(fun(PoolId, TmpCleanAcc) ->
                        case check_clean_child(SceneId, PoolId, NowTime) of
                            {true, SceneKey} ->
                                [SceneKey | TmpCleanAcc];
                            false ->
                                TmpCleanAcc
                        end
                end, CleanSceneKeys, ScenePoolIds);
        false ->
            CleanSceneKeys
    end,
    check_clean(Scenes, NowTime, NewCleanSceneKeys);
check_clean([_ | Scenes], NowTime, CleanSceneKeys) ->
    check_clean(Scenes, NowTime, CleanSceneKeys);
check_clean([], _NowTime, CleanSceneKeys) ->
    CleanSceneKeys.

%% 检测场景是否清理
check_clean_child(SceneId, PoolId, NowTime) when PoolId =/= 0 ->
    SceneKey = {SceneId, PoolId},
    case ets:lookup(?SCENE_ROLE_COUNT, SceneKey) of
        [#scene_role_count{num = 0, change_time = ChangeTime}] when (NowTime - ChangeTime) >= (?CLEAN_TIME div 2) ->
            %% 该场景当前没有人，并且已经过了达到了可以清理的时间条件
            FetchTime = get_fetch_time(SceneId, PoolId),
            case NowTime - FetchTime >= ?CLEAN_AFTER_FETCH of
                true  -> {true, SceneKey};
                false -> false
            end;
        _ ->
            false
    end;
check_clean_child(_SceneId, _PoolId, _NowTime) ->
    false.

clean_scenes(ScenesMaps, [{SceneId, PoolId} | ClearScenesKeys], NowTime) ->
    NewScenesMaps =
    case maps:find(SceneId, ScenesMaps) of
        {ok, SceneInfo = #scene_info{pool = ScenePoolMaps}} ->
            mod_scene_agent:clear_scene(SceneId, PoolId),
            mod_scene_agent:close_scene(SceneId, PoolId),
            add_clean_time(SceneId, PoolId, NowTime),
            NewScenePoolMaps = maps:remove(PoolId, ScenePoolMaps),
            case maps:size(NewScenePoolMaps) =:= 0 of
                true  ->
                    maps:remove(SceneId, ScenesMaps);
                false ->
                    NewSceneInfo = SceneInfo#scene_info{pool = NewScenePoolMaps},
                    ScenesMaps#{SceneId => NewSceneInfo}
            end;
        _ ->
            ScenesMaps
    end,
    clean_scenes(NewScenesMaps, ClearScenesKeys, NowTime);
clean_scenes(ScenesMaps, [], _NowTime) ->
    ScenesMaps.

%% ---------------------------------------------
%% 清理回收场景中的资源
%% ---------------------------------------------
monitor_scene_list(SceneMaps) ->
    SceneList       = maps:to_list(SceneMaps),
    ClusterType     = clusters_api:get_cluster_type(),
    NeedClearDirty  = do_count(clear_dirty, 2),
    NeedGC          = do_count(gc, 3),
    CheckList = [{clear_dirty, NeedClearDirty}, {gc, NeedGC}],
    monitor_scene_list(ClusterType, CheckList, SceneList),
    ok.

monitor_scene_list(ClusterType, CheckList, [{SceneId, #scene_info{pool = ScenePoolMaps}} | SceneList]) ->
    NeedCheck =
    case data_scene:get(SceneId) of
        Scene when is_record(Scene, ets_scene) ->
            case ClusterType of
                ?CLUSTER_NODE   -> not lib_scene:is_clusters_scene(SceneId);
                _               -> true
            end;
        _ -> false
    end,
    case NeedCheck of
        true  ->
            ScenePoolList = maps:to_list(ScenePoolMaps),
            monitor_check(CheckList, SceneId, ScenePoolList);
        false -> ignore
    end,
    monitor_scene_list(ClusterType, CheckList, SceneList);
monitor_scene_list(_, _, _) ->
    ok.

%% CheckInterval可以为一定范围的随机值
do_count(Key, CheckInterval) ->
    {LastDo, LastCount} =
    case get(Key) of
        {_, _} = Val -> Val;
        _ -> {0, 0}
    end,
    NewCount = LastCount + 1,
    NeedCheck = NewCount - LastDo >= CheckInterval,
    NewVal =
    if
        NeedCheck andalso LastCount >= 1000000 ->
            %% 重置计数
            {1, 1};
        LastCount >= 1000000 ->
            %% 重置计数
            {0, NewCount - LastDo};
        NeedCheck ->
            {NewCount, NewCount};
        true ->
            {LastDo, NewCount}
    end,
    put(Key, NewVal),
    NeedCheck.

%% 清除场景残余用户数据
monitor_check([{clear_dirty, true} | CheckList], SceneId, ScenePoolList) ->
    clear_dirty(SceneId, ScenePoolList),
    monitor_check(CheckList, SceneId, ScenePoolList);
%% 执行场景及相关怪物GC
monitor_check([{gc, true} | CheckList], SceneId, ScenePoolList) ->
    scene_gc(SceneId, ScenePoolList, 0),
    monitor_check(CheckList, SceneId, ScenePoolList);
monitor_check([_ | CheckList], SceneId, ScenePoolList) ->
    monitor_check(CheckList, SceneId, ScenePoolList);
monitor_check(_, _, _) ->
    ok.

clear_dirty(SceneId, [{_PoolId, ScenePid} | Others]) ->
    case is_pid(ScenePid) andalso is_process_alive(ScenePid) of
        true  -> ScenePid ! clear_dirty;
        false -> ignore
    end,
    clear_dirty(SceneId, Others);
clear_dirty(_SceneId, []) ->
    ok.

%% 以Delay错开场景的gc时间，避免全部场景及相应怪物短时间内同时gc造成资源集中调度
scene_gc(SceneId, [{_PoolId, ScenePid} | Others], Delay) ->
    NewDelay =
    case is_pid(ScenePid) andalso is_process_alive(ScenePid) of
        true  ->
            case Delay > 0 of
                true  -> erlang:start_timer(Delay, ScenePid, gc);
                false -> ScenePid ! {timeout, undefined, gc}
            end,
            Delay + 3000;
        false ->
            Delay
    end,
    scene_gc(SceneId, Others, NewDelay);
scene_gc(_SceneId, [], _) ->
    ok.

%% ---------------------------------------------
%% 分区分线初始编号计算
%% ---------------------------------------------

%% 游戏服节点调用计算
%% calc_pool_base(IsCenterNode, SceneId, RoleIdList) -> PoolBase | false.
calc_pool_base(false, SceneId, _RoleIdList) ->
    case lib_scene:is_center_city(SceneId)
        orelse convoy_api:is_center_convoy(SceneId)
        orelse madness_land_api:is_madness_land_center(SceneId)
        orelse lib_void_fam:is_in_kf_void_fam(SceneId)
    of
        true ->
            ZoneId = czone_api:env_get_zone_id(),
            case is_integer(ZoneId) of
                true  -> zone_to_pool_base(ZoneId);
                false -> false
            end;
        false ->
            0
    end;
%% 跨服中心节点调用计算
calc_pool_base(true, SceneId, [_RoleId | _]) ->
    case lib_scene:is_center_city(SceneId)
        orelse convoy_api:is_center_convoy(SceneId)
        orelse lib_void_fam:is_in_kf_void_fam(SceneId) of
        true  ->    %% 跨服主城
            ZoneId = czone_api:role_zone(_RoleId),
            case is_integer(ZoneId) of
                true  -> zone_to_pool_base(ZoneId);
                false -> false
            end;
        false ->
            0
    end;
calc_pool_base(_IsCenterNode, _SceneId, _) ->
    false.

-compile({inline, [
            zone_to_pool_base/1,
            get_pool_zone/1
        ]
    }).

%% 通过ZoneId，计算PoolBase（即PoolId区间下限）
%% 旧式分区使用的计算公式：
%%     PoolBase = max((ZoneId - 1) * ?ZONE_POOL_COUNT, 0),
%% 旧式分区ZoneId从1开始，为了利用到0线才使用ZoneId - 1，
%% 新式分区无法利用到0线，直接ZoneId作为基数计算分线编号在人类视角来看更直观
zone_to_pool_base(ZoneId) ->
    max(ZoneId * ?ZONE_POOL_COUNT, 0).

%% 通过PoolId计算所在ZoneId
get_pool_zone(PoolId) ->
    PoolId div ?ZONE_POOL_COUNT.

%% ---------------------------------------------
%% 测试自动分线创建
%% ---------------------------------------------

%% 模拟测试并发请求分配场景
-spec gm_fetch_scene(SceneId, Min, Len, SleepMS) -> FetchRetAccList when
    SceneId :: non_neg_integer(), Min :: integer(), Len :: integer(), SleepMS :: integer(),
    FetchRetAccList :: [FetchRet],
    FetchRet :: {SceneId, PoolId} | error | {'EXIT', Reason},
    PoolId :: non_neg_integer(), Reason :: any().
%% e.g. gm_fetch_scene(SceneId, 1, 100, 1).
gm_fetch_scene(SceneId, Min, Len, SleepMS) ->
    Max = Min + Len,
    G = fun(FetchRetAcc, Remain, Fun) when Remain > 0 ->
            NewFetchRetAcc =
            receive
                {ret, Ret} ->
                    [Ret | FetchRetAcc]
            end,
            Fun(NewFetchRetAcc, Remain - 1, Fun);
        (FetchRetAcc, _, _) ->
            FetchRetAcc
    end,
    RoleIdList = lists:seq(Min, Max),
    Pid = self(),
    #ets_scene{type = SceneType, cls_type = ClsType} = data_scene:get(SceneId),
    [
        spawn(fun() ->
                    Ret = scene_mgr:fetch_scene(SceneId, SceneType, ClsType, RoleId),
                    timer:sleep(SleepMS),
                    Pid ! {ret, Ret}
            end) || RoleId <- RoleIdList
    ],
    G([], Len, G).

