ServerPython/CoreServerGroup/GameServer/Script/Player/PlayerFB.py
@@ -30,59 +30,79 @@
import PlayerTeam
import GameWorld
import ChConfig
import IPY_PlayerDefine
import CrossBattlefield
import CrossRealmPlayer
import CrossChampionship
import DataRecordPack
import CrossRealmMsg
import ShareDefine
import CrossBoss
import time
import random
DynamicShuntType_No = 0 # 不分流
DynamicShuntType_Fill = 1    # 填满式分流,按存在的线路人数多的优先填充,都满后开启新线路
DynamicShuntType_Equally = 2 # 均摊式分流,按存在的线路人数少的优先填充,都满后开启新线路
#---------------------------------------------------------------------
## 跨服地图动态分配的功能线路,如果有人数上限的,则同分区同地图玩法的可能同时存在多个相同功能线路的数据
##  {dataMapID:{(zoneID, funcLineID):[CrossFuncLineInfo, CrossFuncLineInfo, ...], ...}, ...}
class CrossFuncLineInfo():
    
    def __init__(self):
        self.mapID = 0
        self.realMapID = 0
        self.copyMapID = 0
        self.newFuncLineNum = 0
        self.funcLineDataCache = None # 功能线路自定义缓存数据
        return
    
    def OnCopyMapClose(self, funcLineDataCache):
        self.mapID = 0
    def OnCopyMapClose(self):
        self.realMapID = 0
        self.copyMapID = 0
        self.funcLineDataCache = funcLineDataCache
        return
## 跨服地图动态分配的虚拟线路信息
## 跨服地图动态分配的虚拟线路信息 {(mapID, copyMapID):CrossCopyMapInfo, ...}
class CrossCopyMapInfo():
    
    def __init__(self, zoneID, funcLineID):
        self.zoneID = zoneID
        self.funcLineID = funcLineID
        self.openState = 0
        self.newFuncLineNum = 0
        self.realMapID = 0
        self.copyMapID = 0
        self.openState = IPY_PlayerDefine.fbosClosed
        self.fbPlayerDict = {} # 副本中的玩家信息 {playerID:serverGroupID, ...}
        self.waitPlayerDict = {} # 等待进入的玩家信息 {playerID:[serverGroupID, tick], ...}
        self.offlinePlayerDict = {} # 掉线的玩家信息,非主动退出的 {playerID:serverGroupID, ...}
        self.enterPlayerIDList = [] # 有进入过此分线的玩家ID列表,不会清除 [playerID, ...]
        return
    
    def OnRequestEnterCrossCopyMap(self, playerID, serverGroupID, tick, copyMapPlayerMax):
        # 已经在请求队列里,可进入
        if playerID in self.waitPlayerDict or not copyMapPlayerMax:
            self.waitPlayerDict[playerID] = [serverGroupID, tick]
            return True
    def GetCopyMapPlayerCount(self, includeOffline, tick):
        ## 获取该分线玩家数
        # @param includeOffline: 是否包含离线玩家
        
        # 移除请求进入超时的玩家
        for waitPlayerID, playerInfo in self.waitPlayerDict.items():
            serverGroupID, requestTick = playerInfo
            _, requestTick = playerInfo
            if tick - requestTick > 60000: # 请求进入时间保留1分钟
                self.waitPlayerDict.pop(waitPlayerID)
                
        # 判断是否超过人数上限
        fbPlayerCount, waitPlayerCount = len(self.fbPlayerDict), len(self.waitPlayerDict)
        if fbPlayerCount + waitPlayerCount >= copyMapPlayerMax:
            return False
        totalPlayerCount = fbPlayerCount + waitPlayerCount
        if includeOffline:
            totalPlayerCount += len(self.offlinePlayerDict)
        return totalPlayerCount
    def IsMustCopyMapPlayer(self, playerID):
        ## 是否必定在此分线的玩家, 在请求队列里 或 曾经进入到该分线的,都强制认为属于该分线的玩家
        return playerID in self.waitPlayerDict or playerID in self.enterPlayerIDList
        
        self.waitPlayerDict[playerID] = [serverGroupID, tick]
        return True
    def OnRequestEnterCrossCopyMap(self, playerID, tick, copyMapPlayerMax, includeOffline):
        if not copyMapPlayerMax or self.IsMustCopyMapPlayer(playerID):
            return True
        return self.GetCopyMapPlayerCount(includeOffline, tick) < copyMapPlayerMax
    
#---------------------------------------------------------------------
def GetFBLineIpyData(mapID, lineID, isDefaultLine=True):
@@ -125,158 +145,401 @@
def ClientServerMsg_EnterFB(serverGroupID, msgData, tick):
    ## 收到子服请求进入动态分配的跨服副本
    playerID = msgData["PlayerID"]
    dataMapID = msgData["DataMapID"]
    mapID = msgData["MapID"]
    funcLineID = msgData["FuncLineID"]
    playerLV = msgData["LV"]
    
    zoneIpyData = CrossRealmPlayer.GetCrossZoneIpyDataByServerGroupID(dataMapID, serverGroupID)
    if mapID == ChConfig.Def_FBMapID_CrossChampionship:
        CrossChampionship.OnRequestChampionshipVSRoom(playerID, serverGroupID)
        return
    zoneIpyData = CrossRealmPlayer.GetCrossZoneIpyDataByServerGroupID(mapID, serverGroupID)
    if not zoneIpyData:
        return
    zoneID = zoneIpyData.GetZoneID()
    
    copyMapPlayerMax = 0 # 0为不限制人数,默认不限制
    if dataMapID == ChConfig.Def_FBMapID_CrossDemonKing:
    openHour, openMinute = None, None
    dynamicShuntType = DynamicShuntType_Fill
    includeOffline = False
    tagCopyMapObj = None
    # 基础验证是否可进入等 及 数据准备
    if mapID == ChConfig.Def_FBMapID_CrossDemonKing:
        bossID = msgData["BossID"]
        if not CrossBoss.GetCrossBossIsAliveOrCanReborn(zoneID, bossID):
            GameWorld.DebugLog("当前跨服妖王死亡状态,不可进入! serverGroupID=%s,funcLineID=%s,zoneID=%s,bossID=%s" % (serverGroupID, funcLineID, zoneID, bossID))
            GameWorld.ErrLog("当前跨服妖王死亡状态,不可进入! funcLineID=%s,zoneID=%s,bossID=%s" % (funcLineID, zoneID, bossID), playerID)
            return
        
    elif dataMapID in [ChConfig.Def_FBMapID_CrossGrasslandLing, ChConfig.Def_FBMapID_CrossGrasslandXian]:
        copyMapPlayerMax = 10
    elif mapID in [ChConfig.Def_FBMapID_CrossGrasslandLing, ChConfig.Def_FBMapID_CrossGrasslandXian]:
        pass
    elif mapID == ChConfig.Def_FBMapID_CrossBattlefield:
        openTimeInfo = CrossBattlefield.GetCrossBattlefieldOpenTime(serverGroupID, zoneID, playerID)
        if not openTimeInfo:
            #GameWorld.ErrLog("非活动时间或未开启! funcLineID=%s,zoneID=%s" % (funcLineID, zoneID), playerID)
            return
        dynamicShuntType = DynamicShuntType_Equally
        isCallBattle, openHour, openMinute = openTimeInfo
        if isCallBattle:
            # 召集场次默认 funcLineID 为0,不分等级,不分流
            funcLineID = 0
            dynamicShuntType = DynamicShuntType_No
            includeOffline = True
    else:
        return
    
    mapCopyLineInfo = __GetCrossDynamicLineInfo(playerID, serverGroupID, dataMapID, funcLineID, zoneID, copyMapPlayerMax, tick)
    if not mapCopyLineInfo:
    dynamicLineMaxPlayerCountDict = IpyGameDataPY.GetFuncEvalCfg("CrossDynamicLineMap", 2)
    copyMapPlayerMin, copyMapPlayerMax = dynamicLineMaxPlayerCountDict.get(mapID, [0, 0]) # 0为不限制人数,默认不限制
    # 除个别地图外,最优先进入上次进入的未关闭分线
    if mapID not in []:
        for _, copyMapObj in PyGameData.g_crossDynamicLineCopyMapInfo.items():
            if copyMapObj.IsMustCopyMapPlayer(playerID):
                tagCopyMapObj = copyMapObj
                break
    # 如果没有进入过,则按功能看是否有特殊指定规则
    if tagCopyMapObj == None:
        if mapID == ChConfig.Def_FBMapID_CrossBattlefield:
            if isCallBattle:
                copyMapPlayerMax = IpyGameDataPY.GetFuncCfg("CrossBattlefieldCall", 2)
                tagCopyMapObj = CrossBattlefield.GetCallPlayerCopymapObj(playerID, serverGroupID, mapID, funcLineID, zoneID, copyMapPlayerMax, includeOffline, tick)
    # 如果还没有取到对应的分流线,则按默认规则处理
    if tagCopyMapObj == None and dynamicShuntType:
        # 非特殊动态规则,走常规逻辑
        dynamicLineLVRangeDict = IpyGameDataPY.GetFuncEvalCfg("CrossDynamicLineMap", 4)
        if mapID in dynamicLineLVRangeDict:
            lvRangeList = dynamicLineLVRangeDict[mapID]
            for lvFuncLineID, lvRange in enumerate(lvRangeList):
                if lvRange[0] <= playerLV <= lvRange[1]:
                    funcLineID = lvFuncLineID
                    copyMapPlayerMin, copyMapPlayerMax = lvRange[2], lvRange[3]
                    GameWorld.DebugLog("进入跨服地图等级自动适配功能线路ID: mapID=%s,playerLV=%s,funcLineID=%s,copyMapPlayerMin=%s,copyMapPlayerMax=%s"
                                       % (mapID, playerLV, funcLineID, copyMapPlayerMin, copyMapPlayerMax))
                    break
        shuntPlayerMax = copyMapPlayerMax
        minCountTimeDict = IpyGameDataPY.GetFuncEvalCfg("CrossDynamicLineMap", 3) # 分流下限人数有效时间配置,单位秒,{dataMapID:秒, ...}
        if mapID in minCountTimeDict:
            playerMinTimeSet = minCountTimeDict[mapID]
            curTime = GameWorld.GetServerTime()
            if openHour == None or openMinute == None:
                GameWorld.ErrLog("副本开启时间未知! mapID=%s,funcLineID=%s,zoneID=%s" % (mapID, funcLineID, zoneID), playerID)
                return
            openDateTimeStr = "%d-%02d-%02d %02d:%02d:00" % (curTime.year, curTime.month, curTime.day, openHour, openMinute)
            openDateTime = GameWorld.ChangeStrToDatetime(openDateTimeStr)
            passTime = curTime - openDateTime
            '''
                                                在线(包含请求中)          <= 单场下限值
                                                在线(包含请求中)+ 离线 <= 单场上限值
                                                前X秒大于 单场下限值 开新一场
                                                任意时刻大于 单场上限值 必开新一场
            '''
            if passTime.seconds <= playerMinTimeSet:
                shuntPlayerMax = copyMapPlayerMin
                includeOffline = False
            else:
                shuntPlayerMax = copyMapPlayerMax
                includeOffline = True
        tagCopyMapObj = __GetCrossDynamicLineInfo(playerID, serverGroupID, mapID, funcLineID, zoneID,
                                                  shuntPlayerMax, copyMapPlayerMax, includeOffline, tick, dynamicShuntType)
    if not tagCopyMapObj:
        PlayerControl.NotifyCodeCross(serverGroupID, playerID, "CrossFBFull")
        GameWorld.ErrLog("找不到可分流的副本线路! mapID=%s,funcLineID=%s,zoneID=%s" % (mapID, funcLineID, zoneID), playerID)
        return
    mapID, copyMapID, openState = mapCopyLineInfo
    if not openState:
    realMapID, copyMapID, openState = tagCopyMapObj.realMapID, tagCopyMapObj.copyMapID, tagCopyMapObj.openState
    if openState == IPY_PlayerDefine.fbosWaitForClose:
        PlayerControl.NotifyCodeCross(serverGroupID, playerID, "CrossFBClose")
        GameWorld.ErrLog("分流的副本线路关闭中! mapID=%s,funcLineID=%s,zoneID=%s,realMapID=%s,copyMapID=%s,openState=%s"
                         % (mapID, funcLineID, zoneID, realMapID, copyMapID, openState), playerID)
        return
    
    playerIDList = [playerID]
    retInfo = [playerIDList, dataMapID, mapID, copyMapID, funcLineID]
    CrossRealmMsg.SendMsgToClientServer(ShareDefine.CrossServerMsg_EnterFBRet, retInfo, [serverGroupID])
    return
    tagCopyMapObj.waitPlayerDict[playerID] = [serverGroupID, tick]
    GameWorld.DebugLog("    分配进入跨服场景: realMapID=%s, copyMapID=%s, openState=%s" % (realMapID, copyMapID, openState), playerID)
    if openState == IPY_PlayerDefine.fbosOpen:
        funcLineID = tagCopyMapObj.funcLineID
        playerIDList = [playerID]
        # 分流地图的地图数据ID直接使用场景ID,因为分流地图实际上是两张不同的地图,所以直接使用场景ID,不然会导致上传跨服玩家数据时坐标为0
        retInfo = [playerIDList, mapID, realMapID, realMapID, copyMapID, funcLineID]
        CrossRealmMsg.SendMsgToClientServer(ShareDefine.CrossServerMsg_EnterFBRet, retInfo, [serverGroupID])
    dataDict = {}
    dataDict.update(msgData)
    dataDict.update({"mapID":mapID, "realMapID":realMapID, "copyMapID":copyMapID, "realFuncLineID":funcLineID, "openState":openState})
    DataRecordPack.SendEventPack("CrossFBRequest", dataDict)
    return tagCopyMapObj
def CrossServerMsg_EnterFBRet(msgData, tick):
    ## 收到跨服服务器动态分配的跨服副本进入信息
    playerIDList, dataMapID, mapID, realMapID, copyMapID, funcLineID = msgData
    
    playerIDList, dataMapID, mapID, copyMapID, funcLineID = msgData
    dynamicLineMapDict = IpyGameDataPY.GetFuncEvalCfg("CrossDynamicLineMap", 1)
    if dataMapID not in dynamicLineMapDict:
        return
    mapPosInfo = dynamicLineMapDict[dataMapID][0]
    posX, posY = mapPosInfo[:2]
    dist = mapPosInfo[2] if len(mapPosInfo) > 2 else 0
    if dist > 0:
        posX, posY = random.randint(posX - dist, posX + dist), random.randint(posY - dist, posY + dist)
    for playerID in playerIDList:
        curPlayer = GameWorld.GetPlayerManager().FindPlayerByID(playerID)
        if not curPlayer:
            continue
        CrossRealmPlayer.SendCrossRealmReg(curPlayer, dataMapID, mapID, dataMapID, copyMapID, posX, posY, lineID=funcLineID)
        CrossRealmPlayer.SendCrossRealmReg(curPlayer, dataMapID, realMapID, mapID, copyMapID, lineID=funcLineID)
        
    return
def __GetCrossDynamicLineInfo(playerID, serverGroupID, dataMapID, funcLineID, zoneID, copyMapPlayerMax, tick):
def __GetCrossDynamicLineInfo(playerID, serverGroupID, mapID, funcLineID, zoneID, shuntPlayerMax, copyMapPlayerMax, includeOffline, tick, dynamicShuntType):
    '''获取跨服分区对应动态分配的副本地图虚拟线路信息, 由于需要支持多地图分流,所以直接由GameServer管理分配
            每个功能线路支持按人数分流,超过最大人数后可开启一条相同功能线路的虚拟线路进行分流,所以同一分区同一地图的功能线路ID可能对应多条虚拟线路
        分流方式:
            DynamicShuntType_Fill = 1    # 填满式分流,按存在的线路人数多的优先填充,都满后开启新线路
            DynamicShuntType_Equally = 2 # 均摊式分流,按存在的线路人数少的优先填充,都满后开启新线路
        分流规则:
                    时间仅决定分流人数,不影响常规分配逻辑
        1. 优先分配到人数小于分流人数的场次
        2. 超过分流人数的场次依次往人数少的场次分配
        3. 当当前已开放的场次都达到人数分流人数,则开启新场次,没有空闲的场,则往未达到人数上限的场次依次分配,直到达到所有场次上限
            关于 shuntPlayerMax 的选择: 可根据副本规则制定
            如前X分钟内可设定一个小于  copyMapPlayerMax 的分流人数值快速铺满各分流场次
            当大于X分钟后则可设置 shuntPlayerMax = copyMapPlayerMax 进行饱和分流
            当所有分流场次达到  shuntPlayerMax 后,可尝试开启新分流线路,进行分流
         shuntPlayerMax < copyMapPlayerMax 的情况,如果没有办法开启新分流线路,则可继续强制根据分流类型分配线路,只要未达到 copyMapPlayerMax 人数,还是可以进入副本的
        shuntPlayerMax >= copyMapPlayerMax 的情况,如果没有办法开启新分流线路,则标识副本所有线路已达到饱和状态,不能再进入副本了
            当  shuntPlayerMax 为 0 时,达标不限制人数上限,及不分流,都在同一条线路,一般跨服副本不建议设置为0,人数太多,不合理
    @param shuntPlayerMax: 分流最大人数限制
    @param copyMapPlayerMax: 实际最大可容纳的人数限制,一般大于等于分流人数限制
    @param includeOffline: 是否包含本线路离线玩家
    @param dynamicShuntType: 分流类型,可选择  填满式分流  或 均摊式分流
    '''
    
    zoneLineKey = (zoneID, funcLineID)
    if dataMapID not in PyGameData.g_crossDynamicLineInfo:
        PyGameData.g_crossDynamicLineInfo[dataMapID] = {}
    zoneLineDict = PyGameData.g_crossDynamicLineInfo[dataMapID] # 跨服动态线路信息 {dataMapID:{(zoneID, funcLineID):[CrossFuncLineInfo, CrossFuncLineInfo, ...], ...}, ...}
    if zoneLineKey not in zoneLineDict:
        zoneLineDict[zoneLineKey] = []
    funcLineObjList = zoneLineDict[zoneLineKey]
    zoneLineDict = PyGameData.g_crossDynamicLineInfo.get(mapID, {})
    funcLineObjList = zoneLineDict.get(zoneLineKey, [])
    isPlayerFullMax = (shuntPlayerMax >= copyMapPlayerMax)
    
    newFuncLineNum = None
    newFuncLineObj = None
    for index, funcLineObj in enumerate(funcLineObjList, 1):
        mapID, copyMapID = funcLineObj.mapID, funcLineObj.copyMapID
        if not mapID:
            newFuncLineNum, newFuncLineObj = index, funcLineObj
            break
    GameWorld.DebugLog("获取动态分流线路: serverGroupID=%s,mapID=%s,funcLineID=%s,zoneID=%s,shuntPlayerMax=%s,copyMapPlayerMax=%s,includeOffline=%s,dynamicShuntType=%s"
                       % (serverGroupID, mapID, funcLineID, zoneID, shuntPlayerMax, copyMapPlayerMax, includeOffline, dynamicShuntType), playerID)
    #GameWorld.DebugLog("    funcLineObjList=%s" % funcLineObjList, playerID)
    canUseShuntLine = False # 是否直接使用分流线路,如果否的话,当人数未达到真正饱和时,则还可直接分配对应分流类型的线路
    minPlayerCount, maxPlayerCount = 0, 0
    minCopyMapObj, maxCopyMapObj = None, None
    for _, funcLineObj in enumerate(funcLineObjList, 1):
        realMapID, copyMapID = funcLineObj.realMapID, funcLineObj.copyMapID
        #GameWorld.DebugLog("    realMapID=%s, copyMapID=%s" % (realMapID, copyMapID))
        if not realMapID:
            continue
        
        key = (mapID, copyMapID)
        key = (realMapID, copyMapID)
        if key not in PyGameData.g_crossDynamicLineCopyMapInfo:
            GameWorld.ErrLog("已经分配的虚拟线路不存在缓存对应关系里!zoneID=%s,funcLineID=%s,mapID=%s,copyMapID=%s"
                             % (zoneID, funcLineID, mapID, copyMapID))
            GameWorld.ErrLog("已经分配的虚拟线路不存在缓存对应关系里!zoneID=%s,funcLineID=%s,realMapID=%s,copyMapID=%s"
                             % (zoneID, funcLineID, realMapID, copyMapID))
            continue
        
        copyMapObj = PyGameData.g_crossDynamicLineCopyMapInfo[key]
        openState = copyMapObj.openState
        canEnter = copyMapObj.OnRequestEnterCrossCopyMap(playerID, serverGroupID, tick, copyMapPlayerMax)
        if canEnter:
            #GameWorld.DebugLog("可进入动态分布的虚拟线路! mapID=%s,copyMapID=%s,openState=%s" % (mapID, copyMapID, openState))
            #GameWorld.DebugLog("    副本中的玩家ID: %s" % copyMapObj.fbPlayerDict)
            #GameWorld.DebugLog("    等待中的玩家ID: %s" % copyMapObj.waitPlayerDict)
            return mapID, copyMapID, openState
        if openState == IPY_PlayerDefine.fbosWaitForClose:
            # 没有限制分流人数的情况,代表都在同一场,这种情况下当副本已经在关闭的状态下,则代表已经结束了,不可再进入
            if not shuntPlayerMax:
                PlayerControl.NotifyCodeCross(serverGroupID, playerID, "CrossFBClose")
                return
            #GameWorld.DebugLog("    虚拟线路等待关闭中! realMapID=%s,copyMapID=%s" % (realMapID, copyMapID))
            continue
        
    dynamicLineMapDict = IpyGameDataPY.GetFuncEvalCfg("CrossDynamicLineMap", 1)
    if dataMapID not in dynamicLineMapDict:
        if not shuntPlayerMax or copyMapObj.IsMustCopyMapPlayer(playerID):
            return copyMapObj
        playerCount = copyMapObj.GetCopyMapPlayerCount(includeOffline, tick)
        if minCopyMapObj == None or playerCount < minPlayerCount:
            minPlayerCount = playerCount
            minCopyMapObj = copyMapObj
        if maxCopyMapObj == None or playerCount > maxPlayerCount:
            maxPlayerCount = playerCount
            maxCopyMapObj = copyMapObj
        # 存在线路未达到规定的分流人数,则可直接使用分流线路
        if playerCount < shuntPlayerMax:
            canUseShuntLine = True
    #GameWorld.DebugLog("    isPlayerFullMax=%s,canUseShuntLine=%s" % (isPlayerFullMax, canUseShuntLine))
    dynamicShuntCopyMap = None # 分流类型决定的分流线路
    # 均摊式
    if dynamicShuntType == DynamicShuntType_Equally:
        dynamicShuntCopyMap = minCopyMapObj
    # 填满式
    elif dynamicShuntType == DynamicShuntType_Fill:
        dynamicShuntCopyMap = maxCopyMapObj
    else:
        return
    mapIDList = dynamicLineMapDict[dataMapID][1]
    shuntCopyMap = None
    if canUseShuntLine:
        shuntCopyMap = dynamicShuntCopyMap
    #GameWorld.DebugLog("    shuntCopyMap=%s" % shuntCopyMap)
    if not shuntCopyMap:
        isLog = isPlayerFullMax
        shuntCopyMap = __OpenNewFuncLine(mapID, zoneID, funcLineID, isLog)
        # 即 shuntPlayerMax < copyMapPlayerMax 的情况
        if not shuntCopyMap and not isPlayerFullMax:
            shuntCopyMap = dynamicShuntCopyMap
    if not shuntCopyMap:
        return
    shuntCopyMap.waitPlayerDict[playerID] = [serverGroupID, tick]
    return shuntCopyMap
def __OpenNewFuncLine(mapID, zoneID, funcLineID, isLog=True):
    ## 新开功能线路分流
    if mapID not in PyGameData.g_crossDynamicLineInfo:
        PyGameData.g_crossDynamicLineInfo[mapID] = {}
    zoneLineDict = PyGameData.g_crossDynamicLineInfo[mapID] # 跨服动态线路信息 {dataMapID:{(zoneID, funcLineID):[CrossFuncLineInfo, CrossFuncLineInfo, ...], ...}, ...}
    zoneLineKey = (zoneID, funcLineID)
    if zoneLineKey not in zoneLineDict:
        zoneLineDict[zoneLineKey] = []
    funcLineObjList = zoneLineDict[zoneLineKey]
    dynamicLineMapDict = IpyGameDataPY.GetFuncEvalCfg("CrossDynamicLineMap", 1)
    dynamicMapIDList = dynamicLineMapDict.get(mapID, [mapID])
    
    openMapID, openCopyMapID = 0, 0
    for mapID in mapIDList:
        maxCopyMapCount = PyGameData.g_crossMapCopyMapCountDict.get(mapID, 0)
        for copyMapID in xrange(maxCopyMapCount):
            if (mapID, copyMapID) not in PyGameData.g_crossDynamicLineCopyMapInfo:
                openMapID, openCopyMapID = mapID, copyMapID
    maxCopyMapCount = PyGameData.g_crossMapCopyMapCountDict.get(dynamicMapIDList[0], 0)
    # 外层为虚拟线路总数遍历,内层为分流地图,这样可以均匀分流到各个分流地图,减少单地图压力
    for copyMapID in xrange(maxCopyMapCount):
        for realMapID in dynamicMapIDList:
            if copyMapID >= PyGameData.g_crossMapCopyMapCountDict.get(realMapID, 0):
                continue
            if (realMapID, copyMapID) not in PyGameData.g_crossDynamicLineCopyMapInfo:
                openMapID, openCopyMapID = realMapID, copyMapID
                break
        if openMapID:
            break
    if not openMapID:
        GameWorld.ErrLog("没有空余的虚拟线路,无法动态开启跨服副本!dataMapID=%s, funcLineID=%s, zoneID=%s, mapIDList=%s"
                         % (dataMapID, funcLineID, zoneID, mapIDList))
        if isLog:
            GameWorld.ErrLog("没有空余的虚拟线路,无法动态开启跨服副本! mapID=%s,zoneID=%s,funcLineID=%s,dynamicMapIDList=%s"
                             % (mapID, zoneID, funcLineID, dynamicMapIDList))
        return
    
    realMapID, copyMapID = openMapID, openCopyMapID
    newFuncLineObj = None
    for funcLineObj in funcLineObjList:
        if not funcLineObj.realMapID:
            newFuncLineObj = funcLineObj
            break
    if newFuncLineObj == None:
        newFuncLineObj = CrossFuncLineInfo()
        funcLineObjList.append(newFuncLineObj)
        newFuncLineNum = len(funcLineObjList)
    mapID, copyMapID = openMapID, openCopyMapID
    newFuncLineObj.mapID = mapID
    newFuncLineNum = 1
    lineNumList = [lineObj.newFuncLineNum for lineObj in funcLineObjList]
    for num in xrange(1, len(lineNumList) + 1):
        if num not in lineNumList:
            newFuncLineNum = num
            break
    GameWorld.DebugLog("    lineNumList=%s,newFuncLineNum=%s" % (lineNumList, newFuncLineNum))
    newFuncLineObj.realMapID = realMapID
    newFuncLineObj.copyMapID = copyMapID
    funcLineDataCache = newFuncLineObj.funcLineDataCache
    newFuncLineObj.newFuncLineNum = newFuncLineNum
    
    key = (mapID, copyMapID)
    copyMapObj = CrossCopyMapInfo(zoneID, funcLineID)
    PyGameData.g_crossDynamicLineCopyMapInfo[key] = copyMapObj
    copyMapObj.waitPlayerDict[playerID] = [serverGroupID, tick]
    openState = copyMapObj.openState
    copyMapObj.realMapID = realMapID
    copyMapObj.copyMapID = copyMapID
    copyMapObj.newFuncLineNum = newFuncLineNum
    
    propertyID = int("%d%03d%d" % (zoneID, funcLineID, newFuncLineNum))
    GameWorld.DebugLog("不存在该分区功能线路ID,重新分配: zoneID=%s,funcLineID=%s,mapID=%s,copyMapID=%s,propertyID=%s"
                       % (zoneID, funcLineID, mapID, copyMapID, propertyID))
    key = (realMapID, copyMapID)
    PyGameData.g_crossDynamicLineCopyMapInfo[key] = copyMapObj
    propertyID = int("%d%03d%02d" % (zoneID, funcLineID, newFuncLineNum))
    GameWorld.Log("    新开分区动态副本功能线路: zoneID=%s,funcLineID=%s,newFuncLineNum=%s,realMapID=%s,copyMapID=%s,propertyID=%s"
                  % (zoneID, funcLineID, newFuncLineNum, realMapID, copyMapID, propertyID))
    
    # 通知地图开启新的地图虚拟分线
    funcLineDataCache = newFuncLineObj.funcLineDataCache
    msgInfo = str([copyMapID, propertyID, funcLineDataCache])
    GameWorld.GetPlayerManager().MapServer_QueryPlayer(0, 0, 0, mapID, "OpenFB", msgInfo, len(msgInfo))
    return mapID, copyMapID, openState
    GameWorld.GetPlayerManager().MapServer_QueryPlayer(0, 0, 0, realMapID, "OpenFB", msgInfo, len(msgInfo))
    return copyMapObj
def SendMapOpenFBEx(realMapID, copyPropertyList):
    ## 通知地图开启副本线路
    # @param realMapID: 地图ID
    # @param copyPropertyList: [[copyMapID, propertyID], ...]
    msgInfo = str(copyPropertyList)
    GameWorld.GetPlayerManager().MapServer_QueryPlayer(0, 0, 0, realMapID, "OpenFBEx", msgInfo, len(msgInfo))
    GameWorld.Log("SendMapOpenFBEx: realMapID=%s,msgInfo=%s" % (realMapID, msgInfo))
    return
def OpenCrossDynamicLineBySys(zoneID, mapID, funcLineIDList, checkExist):
    ## 系统开启跨服动态线路
    GameWorld.Log("    系统开启跨服动态线路: zoneID=%s, mapID=%s, funcLineIDList=%s, checkExist=%s" % (zoneID, mapID, funcLineIDList, checkExist))
    for funcLineID in funcLineIDList:
        if checkExist:
            fincLineObj = None
            zoneLineKey = (zoneID, funcLineID)
            zoneLineDict = PyGameData.g_crossDynamicLineInfo.get(mapID, {})
            funcLineObjList = zoneLineDict.get(zoneLineKey, [])
            for funcLineObj in funcLineObjList:
                if funcLineObj.realMapID:
                    fincLineObj = funcLineObj
                    break
            if fincLineObj:
                GameWorld.ErrLog("已经存在开放中的线路,不重复开启动态副本线路! mapID=%s, funcLineID=%s, zoneID=%s, realMapID=%s, copyMapID=%s"
                                 % (mapID, funcLineID, zoneID, funcLineObj.realMapID, funcLineObj.copyMapID))
                continue
        __OpenNewFuncLine(mapID, zoneID, funcLineID)
    return
def GetCrossDynamicLineZoneID(mapID, realMapID, copyMapID):
    ## 获取跨服动态分配的虚拟线路对应分区ID
    zoneLineDict = PyGameData.g_crossDynamicLineInfo.get(mapID, {})
    for key, funcLineObjList in zoneLineDict.items():
        for funcLineObj in funcLineObjList:
            if funcLineObj.mapID == realMapID and funcLineObj.copyMapID == copyMapID:
            if funcLineObj.realMapID == realMapID and funcLineObj.copyMapID == copyMapID:
                zoneID = key[0]
                return zoneID
    return 0
def OnCrossDynamicLineOpen(mapID, copyMapID):
def OnCrossDynamicLineStateChange(msgList):
    mapID, realMapID, copyMapID, state = msgList[:4]
    if state == IPY_PlayerDefine.fbosWaitForClose:
        funcLineDataCache = msgList[3]
        OnCrossDynamicLineWaitForClose(realMapID, copyMapID, funcLineDataCache)
    elif state == IPY_PlayerDefine.fbosClosed:
        OnCrossDynamicLineClose(realMapID, copyMapID)
    elif state == IPY_PlayerDefine.fbosOpen:
        OnCrossDynamicLineOpen(mapID, realMapID, copyMapID)
    return
def OnCrossDynamicLineOpen(mapID, realMapID, copyMapID):
    ## 动态分配线路的地图虚拟线路启动成功
    
    key = (mapID, copyMapID)
    key = (realMapID, copyMapID)
    if key not in PyGameData.g_crossDynamicLineCopyMapInfo:
        return
    copyMapObj = PyGameData.g_crossDynamicLineCopyMapInfo[key]
    copyMapObj.openState = 1
    copyMapObj.openState = IPY_PlayerDefine.fbosOpen
    funcLineID = copyMapObj.funcLineID
    
    # 通知子服等待中的玩家可以进入副本
@@ -288,53 +551,90 @@
        playerIDList = serverPlayerIDListDict[serverGroupID]
        playerIDList.append(playerID)
        
    dataMapID = GetRecordMapID(mapID)
    GameWorld.Log("动态分配虚拟线路启动成功,通知子服等待玩家可进入: dataMapID=%s,mapID=%s,copyMapID=%s,serverPlayerIDListDict=%s"
                  % (dataMapID, mapID, copyMapID, serverPlayerIDListDict))
    recordMapID = GetRecordMapID(realMapID)
    GameWorld.Log("动态分配虚拟线路启动成功,通知子服等待玩家可进入: recordMapID=%s,mapID=%s,realMapID=%s,copyMapID=%s,serverPlayerIDListDict=%s"
                  % (recordMapID, mapID, realMapID, copyMapID, serverPlayerIDListDict))
    for serverGroupID, playerIDList in serverPlayerIDListDict.items():
        retInfo = [playerIDList, dataMapID, mapID, copyMapID, funcLineID]
        retInfo = [playerIDList, recordMapID, mapID, realMapID, copyMapID, funcLineID]
        CrossRealmMsg.SendMsgToClientServer(ShareDefine.CrossServerMsg_EnterFBRet, retInfo, [serverGroupID])
        
    #GameWorld.DebugLog("    PyGameData.g_crossDynamicLineInfo=%s" % PyGameData.g_crossDynamicLineInfo)
    #GameWorld.DebugLog("    PyGameData.g_crossDynamicLineCopyMapInfo=%s" % PyGameData.g_crossDynamicLineCopyMapInfo)
    return
def OnCrossDynamicLineClose(mapID, copyMapID, funcLineDataCache):
def OnCrossDynamicLineWaitForClose(realMapID, copyMapID, funcLineDataCache):
    ## 动态分配线路的地图虚拟线路关闭
        
    mapID = GetRecordMapID(realMapID)
    GameWorld.Log("动态分配虚拟线路等待关闭 mapID=%s,realMapID=%s,copyMapID=%s,funcLineDataCache=%s" % (mapID, realMapID, copyMapID, funcLineDataCache))
    zoneLineDict = PyGameData.g_crossDynamicLineInfo.get(mapID, {})
    for key, funcLineObjList in zoneLineDict.items():
        for funcLineObj in funcLineObjList:
            if funcLineObj.realMapID == realMapID and funcLineObj.copyMapID == copyMapID:
                funcLineObj.funcLineDataCache = funcLineDataCache
                zoneID, funcLineID = key
                GameWorld.Log("    分区对应功能线路虚拟分线等待关闭: zoneID=%s,mapID=%s,funcLineID=%s" % (zoneID, mapID, funcLineID))
                break
    key = (realMapID, copyMapID)
    if key in PyGameData.g_crossDynamicLineCopyMapInfo:
        copyMapObj = PyGameData.g_crossDynamicLineCopyMapInfo[key]
        copyMapObj.openState = IPY_PlayerDefine.fbosWaitForClose
    #GameWorld.DebugLog("    PyGameData.g_crossDynamicLineInfo=%s" % PyGameData.g_crossDynamicLineInfo)
    #GameWorld.DebugLog("    PyGameData.g_crossDynamicLineCopyMapInfo=%s" % PyGameData.g_crossDynamicLineCopyMapInfo)
    return
def OnCrossDynamicLineClose(mapID, copyMapID):
    ## 动态分配线路的地图虚拟线路关闭
    dataMapID = GetRecordMapID(mapID)
    GameWorld.Log("动态分配虚拟线路关闭 dataMapID=%s,mapID=%s,copyMapID=%s" % (dataMapID, mapID, copyMapID))
    zoneLineDict = PyGameData.g_crossDynamicLineInfo.get(dataMapID, {}) 
    for key, funcLineObjList in zoneLineDict.items():
        for funcLineObj in funcLineObjList:
            if funcLineObj.mapID == mapID and funcLineObj.copyMapID == copyMapID:
                funcLineObj.OnCopyMapClose(funcLineDataCache)
            if funcLineObj.realMapID == mapID and funcLineObj.copyMapID == copyMapID:
                funcLineObj.OnCopyMapClose()
                zoneID, funcLineID = key
                GameWorld.Log("    分区对应功能线路虚拟分线关闭: zoneID=%s,dataMapID%s,funcLineID=%s" % (zoneID, dataMapID, funcLineID))
                if not funcLineObj.funcLineDataCache:
                    funcLineObjList.remove(funcLineObj)
                break
    
    key = (mapID, copyMapID)
    PyGameData.g_crossDynamicLineCopyMapInfo.pop(key, {})
    copyMapObj = PyGameData.g_crossDynamicLineCopyMapInfo.pop(key, None)
    if not copyMapObj:
        return
    #GameWorld.DebugLog("    PyGameData.g_crossDynamicLineInfo=%s" % PyGameData.g_crossDynamicLineInfo)
    #GameWorld.DebugLog("    PyGameData.g_crossDynamicLineCopyMapInfo=%s" % PyGameData.g_crossDynamicLineCopyMapInfo)
    playerCount = 0
    zoneID = copyMapObj.zoneID
    funcLineID = copyMapObj.funcLineID
    playerCountInfo = [playerCount]
    SyncClientServerCrossFBFuncLinePlayerCount(zoneID, mapID, funcLineID, playerCountInfo)
    #如果虚拟分线关闭时,有掉线的玩家,则通知子服重置这些玩家的跨服状态
    for playerID, serverGroupID in copyMapObj.offlinePlayerDict.items():
        CrossRealmMsg.SendMsgToClientServer(ShareDefine.CrossServerMsg_ExitCrossServer, playerID, [serverGroupID])
    return
def OnCrossDynamicMapReset(mapID, copyMapCount):
def OnCrossDynamicMapReset(msgList):
    ## 动态分配线路的地图重置
    realMapID, copyMapCount = msgList
    mapID = GetRecordMapID(realMapID)
    GameWorld.Log("动态分配虚拟线路地图重置 mapID=%s,realMapID=%s,copyMapCount=%s" % (mapID, realMapID, copyMapCount))
    PyGameData.g_crossMapCopyMapCountDict[realMapID] = copyMapCount
    
    dataMapID = GetRecordMapID(mapID)
    GameWorld.Log("动态分配虚拟线路地图重置 dataMapID=%s,mapID=%s,copyMapCount=%s" % (dataMapID, mapID, copyMapCount))
    PyGameData.g_crossMapCopyMapCountDict[mapID] = copyMapCount
    zoneLineDict = PyGameData.g_crossDynamicLineInfo.get(dataMapID, {})
    zoneLineDict = PyGameData.g_crossDynamicLineInfo.get(mapID, {})
    for key, funcLineObjList in zoneLineDict.items():
        for funcLineObj in funcLineObjList:
            if funcLineObj.mapID == mapID:
                funcLineObj.OnCopyMapClose(None)
            if funcLineObj.realMapID == realMapID:
                funcLineObj.OnCopyMapClose()
            
    for key in PyGameData.g_crossDynamicLineCopyMapInfo.keys():
        if key[0] == mapID:
        if key[0] == realMapID:
            PyGameData.g_crossDynamicLineCopyMapInfo.pop(key)
            
    #GameWorld.DebugLog("    PyGameData.g_crossDynamicLineInfo=%s" % PyGameData.g_crossDynamicLineInfo)
@@ -353,12 +653,36 @@
    playerID = curPlayer.GetPlayerID()
    serverGroupID = PlayerControl.GetPlayerServerGroupID(curPlayer)
    copyMapObj.waitPlayerDict.pop(playerID, None)
    copyMapObj.offlinePlayerDict.pop(playerID, None)
    copyMapObj.fbPlayerDict[playerID] = serverGroupID
    if playerID not in copyMapObj.enterPlayerIDList:
        copyMapObj.enterPlayerIDList.append(playerID)
    
    #GameWorld.DebugLog("玩家登录动态分配的跨服地图: GetMapID=%s,GetRealMapID=%s,GetFBID()=%s,serverGroupID=%s" 
    #                   % (curPlayer.GetMapID(), mapID, copyMapID, serverGroupID), playerID)
    #GameWorld.DebugLog("    副本中的玩家ID: %s" % copyMapObj.fbPlayerDict)
    #GameWorld.DebugLog("    等待中的玩家ID: %s" % copyMapObj.waitPlayerDict)
    #GameWorld.DebugLog("    离线中的玩家ID: %s" % copyMapObj.offlinePlayerDict)
    playerCount = len(copyMapObj.fbPlayerDict) # 等待进入的暂时不算
    zoneID = copyMapObj.zoneID
    funcLineID = copyMapObj.funcLineID
    playerCountInfo = [playerCount]
    SyncClientServerCrossFBFuncLinePlayerCount(zoneID, mapID, funcLineID, playerCountInfo)
    return
def SyncClientServerCrossFBFuncLinePlayerCount(zoneID, mapID, funcLineID, playerCountInfo):
    ## 同步子服跨服副本功能线路人数
    ## 注意: 此人数不是一个精确人数值,只是一个大概人数值,不用很精确,暂时只玩家进入时同步人数信息,玩家退出暂不处理
    mapID = GetRecordMapID(mapID)
    if mapID not in ChConfig.Def_NeedCountFBFuncLinePlayerCrossMap:
        return
    zoneIpyData = CrossRealmPlayer.GetCrossZoneIpyDataByZoneID(mapID, zoneID)
    if not zoneIpyData:
        return
    serverGroupIDList = zoneIpyData.GetServerGroupIDList()
    playerCountInfo = [mapID, funcLineID, playerCountInfo]
    CrossRealmMsg.SendMsgToClientServer(ShareDefine.CrossServerMsg_FBPlayerCount, playerCountInfo, serverGroupIDList)
    return
def OnPlayerDisconnectCrossServer(curPlayer):
@@ -374,10 +698,26 @@
    copyMapObj.waitPlayerDict.pop(playerID, None)
    copyMapObj.fbPlayerDict.pop(playerID, None)
    
    #GameWorld.DebugLog("玩家退出动态分配的跨服地图: GetMapID=%s,GetRealMapID=%s,GetFBID()=%s"
    #                   % (curPlayer.GetMapID(), mapID, copyMapID), playerID)
    crossMapID = PlayerControl.GetCrossMapID(curPlayer)
    # 不是主动退出的
    if crossMapID:
        copyMapObj.offlinePlayerDict[playerID] = PlayerControl.GetPlayerServerGroupID(curPlayer)
    #GameWorld.DebugLog("玩家退出动态分配的跨服地图: GetMapID=%s,GetRealMapID=%s,GetFBID()=%s,crossMapID=%s"
    #                   % (curPlayer.GetMapID(), mapID, copyMapID, crossMapID), playerID)
    #GameWorld.DebugLog("    副本中的玩家ID: %s" % copyMapObj.fbPlayerDict)
    #GameWorld.DebugLog("    等待中的玩家ID: %s" % copyMapObj.waitPlayerDict)
    #GameWorld.DebugLog("    离线中的玩家ID: %s" % copyMapObj.offlinePlayerDict)
    return
def CrossServerMsg_FBPlayerCount(msgData):
    ## 收到跨服服务器同步的副本功能线路人数信息
    mapID, funcLineID, playerCountInfo = msgData
    if mapID not in PyGameData.g_crossFBFuncLinePlayerCountInfo:
        PyGameData.g_crossFBFuncLinePlayerCountInfo[mapID] = {}
    fbLinePlayerInfoDict = PyGameData.g_crossFBFuncLinePlayerCountInfo[mapID]
    fbLinePlayerInfoDict[funcLineID] = playerCountInfo
    return
##--------------------------------------------------------------------------------------------------
@@ -388,6 +728,7 @@
#  @param sendCMD: 请求的命令 根据请求类型和请求命令来决定最终操作
#  @return None
def EnterFBLine(curPlayer, queryCallName, sendCMD, tick):
    playerID = curPlayer.GetPlayerID()
    GameWorld.Log("EnterFBLine()...queryCallName=%s,sendCMD=%s" % (queryCallName, sendCMD), curPlayer.GetPlayerID())
    playerManager = GameWorld.GetPlayerManager()
    try:
@@ -413,6 +754,12 @@
    
    # 组队副本, 有队伍的情况才验证其他队员可否进入,否则代表单人进入
    if gameMap.GetMapFBType() == ChConfig.fbtTeam:
        if tagMapID == ChConfig.Def_FBMapID_Love:
            onlyDoubleTeam = IpyGameDataPY.GetFuncCfg("LoveFB", 1)
            if onlyDoubleTeam:
                if PlayerTeam.CheckTeamOnLineCount(curPlayer.GetTeam(), includeTJG=False) != 2:
                    PlayerControl.NotifyCode(curPlayer, "OnlyTwoMemTeamCanEnter", [tagMapID])
                    return
        PlayerTeam.OnEnterFBTeamAsk(curPlayer, PlayerTeam.TeamFBAskType_Enter, tagMapID, tagLineID, tick)
        return
    
@@ -426,10 +773,6 @@
        if not GameWorldFamilyWar.CheckPlayerCanEnterFamilyWarFBMap(curPlayer):
            return
        
    elif tagMapID == ChConfig.Def_FBMapID_FamilyBossMap:
        if not PlayerFamilyBoss.CheckIsFamilyBossFBOpen(curPlayer.GetFamilyID(), tagMapID):
            GameWorld.Log("EnterFBLine mapID=%s is familyBossFB, but is not open!" % tagMapID)
            return
    #守卫人皇 是否已参加
    elif tagMapID == ChConfig.Def_FBMapID_FamilyInvade:
        if curPlayer.GetFamilyID() in PyGameData.g_swrhJoinRecord:
@@ -455,3 +798,49 @@
    playerManager.MapServer_QueryPlayer(curPlayer.GetPlayerID(), ChConfig.queryType_EnterFB, 0, tagMapID,
                queryCallName, sendCMD, len(sendCMD), curPlayer.GetRouteServerIndex())
    return
def Send_CrossServerMsg_EnterVSRoomRet(vsRoomDict, serverGroupIDList=None):
    ## 发送子服跨服对战房间请求进入结果
    # @param vsRoomDict: {roomID:{playerID:playerInfo, ...}, }
    #      playerInfo key
    #            serverGroupID    所属服务器分组ID
    #            regMapInfo        传送跨服注册信息 [registerMap, mapID, dataMapID, copyMapID, posX, posY]
    CrossRealmMsg.SendMsgToClientServer(ShareDefine.CrossServerMsg_EnterVSRoomRet, vsRoomDict, serverGroupIDList)
    return
def CrossServerMsg_EnterVSRoomRet(msgData, tick):
    ## 跨服对战房间请求进入结果
    curServerGroupID = GameWorld.GetServerGroupID()
    GameWorld.DebugLog("=== 跨服PK对战房间请求进入结果  === curServerGroupID=%s" % curServerGroupID)
    vsRoomDict = msgData
    for roomID, playerDict in vsRoomDict.items():
        GameWorld.DebugLog("    roomID=%s,playerDict=%s" % (roomID, playerDict))
        for playerID, playerInfo in playerDict.items():
            if "serverGroupID" in playerInfo:
                serverGroupID = playerInfo["serverGroupID"]
                if serverGroupID != curServerGroupID:
                    GameWorld.DebugLog("        不是本服玩家,不处理!playerID=%s,serverGroupID=%s" % (playerID, serverGroupID))
                    continue
            player = GameWorld.GetPlayerManager().FindPlayerByID(playerID)
            if not player:
                GameWorld.DebugLog("        玩家不在线, playerID=%s" % (playerID))
                continue
            if PlayerControl.GetIsTJG(player):
                GameWorld.DebugLog("        玩家脱机中, playerID=%s" % (playerID))
                continue
            if "regMapInfo" not in playerInfo:
                continue
            regMapInfo = playerInfo["regMapInfo"]
            if len(regMapInfo) != 6:
                continue
            registerMap, mapID, dataMapID, copyMapID, posX, posY = regMapInfo
            PlayerControl.SetVsRoomId(player, roomID, True)
            # 通知地图玩家匹配成功, 上传数据, 准备进入跨服服务器
            CrossRealmPlayer.SendCrossRealmReg(player, registerMap, mapID, dataMapID, copyMapID, posX, posY)
    return