--***************************************************************************** --* File: lua/modules/ui/lobby/lobby.lua --* Author: Chris Blackwell --* Summary: Game selection UI --* --* Copyright © 2005 Gas Powered Games, Inc. All rights reserved. --***************************************************************************** local UIUtil = import('/lua/modules/ui/uiutil.lua') local MenuCommon = import('/lua/modules/ui/menus/menucommon.lua') local Prefs = import('/lua/modules/ui/prefs.lua') local OnlineProvider = import('/lua/modules/multiplayer/onlineprovider.lua') local MapUtil = import('/lua/modules/ui/maputil.lua') local Group = import('/lua/modules/maui/group.lua').Group local ItemList = import('/lua/modules/maui/itemlist.lua').ItemList local LayoutHelpers = import('/lua/modules/maui/layouthelpers.lua') local Bitmap = import('/lua/modules/maui/bitmap.lua').Bitmap local Button = import('/lua/modules/maui/button.lua').Button local Edit = import('/lua/modules/maui/edit.lua').Edit local LobbyComm = import('/lua/modules/ui/lobby/lobbyComm.lua') local teamOpts = import('/lua/modules/ui/lobby/lobbyOptions.lua').teamOptions local globalOpts = import('/lua/modules/ui/lobby/lobbyOptions.lua').globalOpts local gameColors = import('/lua/modules/ui/gameColors.lua').GameColors local factionID = import('/lua/modules/ui/lobby/factions.lua').factionID local commandQueueIndex = 0 local commandQueue = {} local factionBmps = { "/faction_icon-sm/uef_ico.dds", "/faction_icon-sm/aeon_ico.dds", "/faction_icon-sm/cybran_ico.dds", "/faction_icon-sm/random_ico.dds", } local teamNumbers = { "FFA", "1", "2", "3", "4", } local Strings = LobbyComm.Strings local lobbyComm = false local wantToBeObserver = false local localPlayerName = "" local gameName = "" local hostID = false local singlePlayer = false local GUI = false local localPlayerID = false local gameInfo = false local pmDialog = false local limbo = {} local slotMenuStrings = { open = "Open", close = "Close", closed = "Closed", occupy = "Occupy", pm = "Private Message", remove = "Remove", } local slotMenuData = { open = { host = { 'ailist', -- 'open', -- 'close', 'occupy', }, client = { 'occupy', }, }, player = { host = { 'pm', 'remove', }, client = { 'pm', }, }, ai = { host = { 'ailist', 'remove', }, client = { }, }, } local function DoPrivateChat(targetID, playerName) if not GUI then return end if pmDialog then pmDialog:Destroy() pmDialog = false end --TODO this needs its own dialog? pmDialog = Bitmap(GUI, UIUtil.SkinnableFile('/dialogs/dialog/panel_bmp.dds'), "Rename Dialog") LayoutHelpers.AtCenterIn(pmDialog, GUI) pmDialog.Depth:Set(GetFrame(GUI:GetRootFrame():GetTargetHead()):GetTopmostDepth() + 1) local label = UIUtil.CreateText(pmDialog, LOCF("Send Private Message To %s", playerName), 14, UIUtil.bodyFont) LayoutHelpers.AtLeftTopIn(label, pmDialog, 30, 115) local cancelButton = UIUtil.CreateButtonStd(pmDialog, '/dialogs/standard-small_btn/standard-small', "", 10) LayoutHelpers.Below(cancelButton, label) cancelButton.OnClick = function(self, modifiers) pmDialog:Destroy() pmDialog = false end --TODO this should be in layout local chatEdit = Edit(pmDialog) LayoutHelpers.AtLeftTopIn(chatEdit, pmDialog, 40, 50) chatEdit.Width:Set(340) chatEdit.Height:Set(chatEdit:GetFontHeight()) chatEdit:SetFont(UIUtil.bodyFont, 24) chatEdit:SetForegroundColor(UIUtil.fontColor) chatEdit:ShowBackground(false) chatEdit:AcquireFocus() chatEdit.OnEnterPressed = function(self, text) PrivateChat(targetID, text) pmDialog:Destroy() pmDialog = false end end local function GetSlotMenuTables(stateKey, hostKey) local keys = {} local strings = {} if not slotMenuData[stateKey] then ERROR("Invalid slot menu state selected: " .. stateKey) end if not slotMenuData[stateKey][hostKey] then ERROR("Invalid slot menu host key selected: " .. hostKey) end for index, key in slotMenuData[stateKey][hostKey] do if key == 'ailist' then local aitypes = import('/lua/modules/ui/lobby/aitypes.lua').aitypes for aiindex, aidata in aitypes do table.insert(keys, aidata.key) table.insert(strings, aidata.name) end else table.insert(keys, key) table.insert(strings, slotMenuStrings[key]) end end return keys, strings end local function DoSlotBehavior(slot, key, name) if key == 'open' then elseif key == 'close' then elseif key == 'occupy' then if IsPlayer(localPlayerID) then if lobbyComm:IsHost() then HostTryMovePlayer(hostID, FindSlotForID(localPlayerID), slot) else lobbyComm:SendData(hostID, {Type = 'MovePlayer', CurrentSlot = FindSlotForID(localPlayerID), RequestedSlot = slot}) end elseif IsObserver(localPlayerID) then if lobbyComm:IsHost() then HostConvertObserverToPlayer(hostID, localPlayerName, FindObserverSlotForID(localPlayerID), slot) else lobbyComm:SendData(hostID, {Type = 'RequestConvertToPlayer', RequestedName = localPlayerName, ObserverSlot = FindObserverSlotForID(localPlayerID), PlayerSlot = slot}) end else -- must be in limbo, so request getting out if lobbyComm:IsHost() then MoveDepartedSoulToPlayer(hostID, slot) else lobbyComm:SendData(hostID, {Type = 'RequestMoveDepartedSoulToPlayer', PlayerSlot = slot}) end end elseif key == 'pm' then if gameInfo.PlayerOptions[slot].Human then DoPrivateChat(gameInfo.PlayerOptions[slot].OwnerID, gameInfo.PlayerOptions[slot].PlayerName) end elseif key == 'remove' then if gameInfo.PlayerOptions[slot].Human then UIUtil.QuickDialog(GUI, "Are you sure?", "Kick Player", function() lobbyComm:EjectPlayer(gameInfo.PlayerOptions[slot].OwnerID, "KickedByHost") end, "", nil, nil, nil, true) else if lobbyComm:IsHost() then HostRemoveAI( slot) else lobbyComm:SendData( hostID, { Type = 'ClearSlot', Slot = slot } ) end end else if lobbyComm:IsHost() then HostTryAddPlayer(hostID, slot, name, false, key) end end end function Reset() lobbyComm = false wantToBeObserver = false localPlayerName = "" gameName = "" hostID = false singlePlayer = false GUI = false localPlayerID = false gameInfo = { GameOptions = {}, PlayerOptions = {}, Observers = {}, } end -- Create a new unconnected lobby. function CreateLobby(protocol, localPort, desiredPlayerName, localPlayerUID, natTraversalProvider) Reset() if GUI then WARN('CreateLobby called but I already have one setup...?') GUI:Destroy() end GUI = UIUtil.CreateScreenGroup(GetFrame(0), "CreateLobby ScreenGroup") GUI.optionControls = {} GUI.slots = {} -- don't parent background to screen group so it doesn't get destroyed until we leave the menus local background = MenuCommon.SetupBackground(GetFrame(0)) local function OnAbort() MenuCommon.MenuCleanup() GUI:Destroy() GUI = false ExitApplication() end GUI.connectdialog = UIUtil.ShowInfoDialog(GUI, Strings.TryingToConnect, Strings.AbortConnect, OnAbort) InitLobbyComm(protocol, localPort, desiredPlayerName, localPlayerUID, natTraversalProvider) -- Store off the validated playername localPlayerName = lobbyComm:GetLocalPlayerName() end -- create the lobby as a host function HostGame(desiredGameName, scenarioFileName, inSinglePlayer) singlePlayer = inSinglePlayer if not singlePlayer then gameInfo.Timeouts = 3 end gameName = lobbyComm:MakeValidGameName(desiredGameName) CreateUI(LobbyComm.maxPlayerSlots) lobbyComm.desiredScenario = scenarioFileName lobbyComm:HostGame() end -- join an already existing lobby function JoinGame(address, asObserver, playerName, uid) wantToBeObserver = asObserver CreateUI(LobbyComm.maxPlayerSlots) lobbyComm:JoinGame(address, playerName, uid); end function ConnectToPeer(boxedAddress,port,name,boxedID) lobbyComm:ConnectToPeer(boxedAddress,port,name,boxedID) end function DisconnectFromPeer(boxedID) lobbyComm:DisconnectFromPeer(boxedID) end function FindSlotForID(id) for k,player in gameInfo.PlayerOptions do if player.OwnerID == id and player.Human then return k end end return nil end function FindObserverSlotForID(id) for k,observer in gameInfo.Observers do if observer.OwnerID == id then return k end end return nil end function IsLocallyOwned(slot) return (gameInfo.PlayerOptions[slot].OwnerID == localPlayerID) end function IsPlayer(id) return FindSlotForID(id) != nil end function IsObserver(id) return FindObserverSlotForID(id) != nil end function IsInLimbo(id) return limbo[id] != nil end -- update the data in a player slot function SetSlotInfo(slot, playerInfo) local isLocallyOwned if IsLocallyOwned(slot) then EnableSlot(slot) isLocallyOwned = true else DisableSlot(slot) isLocallyOwned = false end local hostKey if lobbyComm:IsHost() then hostKey = 'host' else hostKey = 'client' end if not playerInfo.Human and lobbyComm:IsHost() then end local slotState if not playerInfo.Human then slotState = 'ai' elseif not isLocallyOwned then slotState = 'player' end GUI.slots[slot].name:ClearItems() if slotState then GUI.slots[slot].name:Enable() local slotKeys, slotStrings = GetSlotMenuTables(slotState, hostKey) GUI.slots[slot].name.slotKeys = slotKeys if table.getn(slotKeys) > 0 then GUI.slots[slot].name:AddItems(slotStrings) GUI.slots[slot].name:Enable() else GUI.slots[slot].name.slotKeys = nil GUI.slots[slot].name:Disable() end else -- no slotState indicate this must be ourself, and you can't do anything to yourself GUI.slots[slot].name.slotKeys = nil GUI.slots[slot].name:Disable() end GUI.slots[slot].name:Show() GUI.slots[slot].name:SetTitleText(LOC(playerInfo.PlayerName)) GUI.slots[slot].faction:Show() GUI.slots[slot].faction:SetItem(playerInfo.Faction) OnlineProvider.ChangeOption(string.format("faction %s %d %s", playerInfo.PlayerName, slot, playerInfo.Faction)) GUI.slots[slot].color:Show() GUI.slots[slot].color:SetItem(playerInfo.PlayerColor) OnlineProvider.ChangeOption(string.format("color %s %d %s", playerInfo.PlayerName, slot, playerInfo.PlayerColor)) GUI.slots[slot].team:Show() GUI.slots[slot].team:SetItem(playerInfo.Team) OnlineProvider.ChangeOption(string.format("team %s %d %s", playerInfo.PlayerName, slot, playerInfo.Team)) OnlineProvider.ChangeOption(string.format("startspot %s %d %s", playerInfo.PlayerName, slot, slot)) if GUI.slots[slot].ready then if playerInfo.Human then GUI.slots[slot].ready:Show() GUI.slots[slot].ready:SetCheck(playerInfo.Ready, true) else GUI.slots[slot].ready:Hide() end end if GUI.slots[slot].pingGroup then if isLocallyOwned or not playerInfo.Human then GUI.slots[slot].pingGroup:Hide() else GUI.slots[slot].pingGroup:Show() end end end function ClearSlotInfo(slot) local hostKey if lobbyComm:IsHost() then hostKey = 'host' else hostKey = 'client' end local slotKeys, slotStrings = GetSlotMenuTables('open', hostKey) -- set the text appropriately GUI.slots[slot].name:ClearItems() GUI.slots[slot].name:SetTitleText(LOC(slotMenuStrings.open)) GUI.slots[slot].name:SetTitleTextColor(UIUtil.fontColor) GUI.slots[slot].name.slotKeys = slotKeys GUI.slots[slot].name:AddItems(slotStrings) GUI.slots[slot].name:Enable() -- hide these to clear slot of visible data GUI.slots[slot].faction:Hide() GUI.slots[slot].color:Hide() GUI.slots[slot].team:Hide() GUI.slots[slot].multiSpace:Hide() if GUI.slots[slot].pingGroup then GUI.slots[slot].pingGroup:Hide() end end function IsColorFree(colorIndex) for id,player in gameInfo.PlayerOptions do if player.PlayerColor == colorIndex then return false end end return true end function GetPlayerCount() local numPlayers = 0 for k,player in gameInfo.PlayerOptions do if player.Team >= 0 then numPlayers = numPlayers + 1 end end return numPlayers end local function AssignRandomFactions(gameInfo) local randomFactionID = table.getn(factionID) + 1 for index, player in gameInfo.PlayerOptions do if player.Faction >= randomFactionID then player.Faction = math.random(1, table.getn(factionID)) end end end local function AssignAINames(gameInfo) local aiNames = import('/lua/modules/ui/lobby/aiNames.lua').ainames local nameSlotsTaken = {} for index, faction in factionID do nameSlotsTaken[index] = {} end for index, player in gameInfo.PlayerOptions do if player.Human == false then local factionNames = aiNames[factionID[player.Faction]] local ranNum repeat ranNum = math.random(1, table.getn(factionNames)) until nameSlotsTaken[player.Faction][ranNum] == nil nameSlotsTaken[player.Faction][ranNum] = true local newName = factionNames[ranNum] player.PlayerName = newName .. " (" .. player.PlayerName .. ")" end end end -- call this whenever the lobby needs to exit and not go in to the game function ReturnToMenu() lobbyComm:Destroy() lobbyComm = false OnlineProvider.UnregisterChat() if OnlineProvider.CheckOnline() then MenuCommon.MenuCleanup() GUI:Destroy() GUI = false ExitApplication() else GUI:Destroy() GUI = false if singlePlayer then import('/lua/modules/ui/menus/main.lua').CreateUI() else import('/lua/modules/ui/lobby/gameselect.lua').CreateUI() end end end function SystemMessage(text) lobbyComm:BroadcastData( { Type = "SystemMessage", Text = text, } ) if GUI.chatDisplay then AddChatText(text) --GUI.chatDisplay:AddItem(text) end end function PublicChat(text) lobbyComm:BroadcastData( { Type = "PublicChat", Text = text, } ) if GUI.chatDisplay then AddChatText("["..localPlayerName.."] " .. text) --GUI.chatDisplay:AddItem("["..localPlayerName.."] " .. text) end end function PrivateChat(targetID,text) lobbyComm:SendData( targetID, { Type = 'PrivateChat', Text = text, } ) if GUI.chatDisplay then AddChatText("<<"..localPlayerName..">> " .. text) --GUI.chatDisplay:AddItem("<<"..localPlayerName..">> " .. text) end end function UpdateAvailableSlots( numAvailStartSpots ) -- if number of available slots has changed, update it local closedCount = 0 for k,v in GUI.slots do if not v.closed then closedCount = closedCount + 1 end end if numAvailStartSpots and numAvailStartSpots != closedCount then for i = 1, LobbyComm.maxPlayerSlots do if i <= numAvailStartSpots then if GUI.slots[i].closed then GUI.slots[i].closed = false GUI.slots[i]:Show() if not gameInfo.PlayerOptions[i] then ClearSlotInfo(i) end EnableSlot(i) end else if not GUI.slots[i].closed then if gameInfo.PlayerOptions[i] then if gameInfo.PlayerOptions[i].Human then MovePlayerToLimbo(gameInfo.PlayerOptions[i].OwnerID) else HostRemoveAI(i) end end DisableSlot(i) GUI.slots[i]:Hide() GUI.slots[i].closed = true end end end end end local function UpdateGame() if lobbyComm:IsHost() then GUI.changeMapButton:Show() GUI.launchGameButton:Show() if GUI.allowObservers then GUI.allowObservers:Show() end else GUI.changeMapButton:Hide() GUI.launchGameButton:Hide() if GUI.allowObservers then GUI.allowObservers:Hide() end end if GUI.becomeObserver then if not gameInfo.GameOptions.AllowObservers or IsObserver(localPlayerID) then GUI.becomeObserver:Hide() else GUI.becomeObserver:Show() end end if GUI.observerList then -- clear every update and repopulate GUI.observerList:DeleteAllItems() if table.getn(gameInfo.Observers) > 0 then for index, observer in gameInfo.Observers do GUI.observerList:AddItem(observer.ObserverName) end end end local scenarioInfo = nil if gameInfo.GameOptions.ScenarioFile and (gameInfo.GameOptions.ScenarioFile != "") then scenarioInfo = MapUtil.LoadScenario(gameInfo.GameOptions.ScenarioFile) end local numPlayers = GetPlayerCount() local numAvailStartSpots = nil if scenarioInfo then local armyTable = MapUtil.GetArmies(scenarioInfo) if armyTable then numAvailStartSpots = table.getn(armyTable) end end UpdateAvailableSlots(numAvailStartSpots) for i = 1, LobbyComm.maxPlayerSlots do if not GUI.slots[i].closed then if gameInfo.PlayerOptions[i] then SetSlotInfo(i, gameInfo.PlayerOptions[i]) else ClearSlotInfo(i) end end end if scenarioInfo and scenarioInfo.map and (scenarioInfo.map != "") then --LOG('*** SettingMap :', gameInfo.GameOptions.ScenarioFile) OnlineProvider.ChangeMap(scenarioInfo.map) local previewMap = LoadMapPreview(scenarioInfo.map) GUI.mapView:SetMap(scenarioInfo.preview,previewMap) GUI.mapName:SetText(LOC(scenarioInfo.name)) GUI.mapSize:SetText(LOCF("Map Size: %dkm x %dkm", scenarioInfo.size[1]/50, scenarioInfo.size[2]/50)) GUI.mapNumPlayers:SetText(LOCF("Max Players: %d", table.getsize(scenarioInfo.Configurations.standard.teams[1].armies))) ShowMapPositions(GUI.mapView,scenarioInfo,numPlayers) end if not lobbyComm:IsHost() then --LOG('GOT OPTS: ')--, repr(gameInfo.GameOptions)) for k,v in gameInfo.GameOptions do --LOG(k .. ' = ' .. v) local control = GUI.optionControls[k] if control then control:Disable() control.UpdateValue(v) end end end end -- slot less than 1 means try to find a slot function HostTryAddPlayer( senderID, slot, requestedPlayerName, human, aiPersonality ) local newSlot = slot if not slot or slot < 1 then newSlot = -1 for i = 1, LobbyComm.maxPlayerSlots do if gameInfo.PlayerOptions[i] == nil then newSlot = i break end end else if newSlot > LobbyComm.maxPlayerSlots then newSlot = -1 end end -- if no slot available, and human, try to make them an observer if newSlot == -1 then PrivateChat( senderID, "Not slots available, attempting to make you an observer" ) if human then HostTryAddObserver(senderID, requestedPlayerName) end return end local playerName = lobbyComm:MakeValidPlayerName(senderID,requestedPlayerName) gameInfo.PlayerOptions[newSlot] = LobbyComm.GetDefaultPlayerOptions(playerName) gameInfo.PlayerOptions[newSlot].Human = human gameInfo.PlayerOptions[newSlot].OwnerID = senderID if not human and aiPersonality then gameInfo.PlayerOptions[newSlot].AIPersonality = aiPersonality end -- figure out a reasonable default color for colorIndex,colorVal in gameColors.PlayerColors do if IsColorFree(colorIndex) then gameInfo.PlayerOptions[newSlot].PlayerColor = colorIndex break end end lobbyComm:BroadcastData( { Type = 'SlotAssigned', Slot = newSlot, Options = gameInfo.PlayerOptions[newSlot], } ) UpdateGame() end function HostTryMovePlayer(senderID, currentSlot, requestedSlot) LOG("SenderID: " .. senderID .. " currentSlot: " .. currentSlot .. " requestedSlot: " .. requestedSlot) if gameInfo.PlayerOptions[requestedSlot] then LOG("HostTryMovePlayer: requested slot " .. requestedSlot .. " already occupied") return end if requestedSlot > LobbyComm.maxPlayerSlots or requestedSlot < 1 then LOG("HostTryMovePlayer: requested slot " .. requestedSlot .. " is out of range") return end gameInfo.PlayerOptions[requestedSlot] = gameInfo.PlayerOptions[currentSlot] gameInfo.PlayerOptions[currentSlot] = nil ClearSlotInfo(currentSlot) lobbyComm:BroadcastData( { Type = 'SlotMove', OldSlot = currentSlot, NewSlot = requestedSlot, Options = gameInfo.PlayerOptions[requestedSlot], } ) UpdateGame() end function HostTryAddObserver( senderID, requestedObserverName ) if not gameInfo.GameOptions.AllowObservers then -- PrivateChat( senderID, "Observers are not allowed in this game." ) lobbyComm:EjectPlayer( senderID, "NoObservers" ) return end local index = -1 for i = 1, LobbyComm.maxObservers do if gameInfo.Observers[i] == nil then index = i break end end if index == -1 then PrivateChat( senderID, "All observer slots are full." ) return end local observerName = lobbyComm:MakeValidPlayerName(senderID,requestedObserverName) gameInfo.Observers[index] = { ObserverName = observerName, OwnerID = senderID, } lobbyComm:BroadcastData( { Type = 'ObserverAdded', Slot = index, Options = gameInfo.Observers[index], } ) SystemMessage(LOCF("%s has joined as an observer.",observerName)) UpdateGame() end function HostConvertPlayerToObserver(senderID, name, playerSlot) -- make sure player exists if not gameInfo.PlayerOptions[playerSlot] then return end -- find a free observer slot local index = -1 for i = 1, LobbyComm.maxObservers do if gameInfo.Observers[i] == nil then index = i break end end if index == -1 then SystemMessage(LOCF("No free observer slots to move player")) return end gameInfo.Observers[index] = { ObserverName = name, OwnerID = senderID, } gameInfo.PlayerOptions[playerSlot] = nil ClearSlotInfo(playerSlot) lobbyComm:BroadcastData( { Type = 'ConvertPlayerToObserver', OldSlot = playerSlot, NewSlot = index, Options = gameInfo.Observers[index], } ) SystemMessage(LOCF("%s has switched from a player to an observer.", name)) UpdateGame() end function HostConvertObserverToPlayer(senderID, name, fromObserverSlot, toPlayerSlot) if gameInfo.Observers[fromObserverSlot] == nil then return end if gameInfo.PlayerOptions[toPlayerSlot] != nil then return end gameInfo.PlayerOptions[toPlayerSlot] = LobbyComm.GetDefaultPlayerOptions(name) gameInfo.PlayerOptions[toPlayerSlot].OwnerID = senderID for colorIndex,colorVal in gameColors.PlayerColors do if IsColorFree(colorIndex) then gameInfo.PlayerOptions[toPlayerSlot].PlayerColor = colorIndex break end end gameInfo.Observers[fromObserverSlot] = nil lobbyComm:BroadcastData( { Type = 'ConvertObserverToPlayer', OldSlot = fromObserverSlot, NewSlot = toPlayerSlot, Options = gameInfo.PlayerOptions[toPlayerSlot], } ) SystemMessage(LOCF("%s has switched from an observer to player.", name)) UpdateGame() end function MovePlayerToLimbo(id) local slot = FindSlotForID(id) if not slot then WARN("Client id " .. id .. " is not a player") return end if limbo[id] then WARN("Client id " .. id .. " is already in limbo") return end limbo[id] = gameInfo.PlayerOptions[slot] gameInfo.PlayerOptions[slot] = nil ClearSlotInfo(slot) lobbyComm:BroadcastData( { Type = 'ClearSlot', Slot = slot, } ) SystemMessage(LOCF("%s had been moved to limbo and may choose to become an observer or player", limbo[id].PlayerName)) UpdateGame() end function MoveObserverToLimbo(id) local slot = FindObserverSlotForID(id) if not slot then WARN("Client id " .. id .. " is not an observer") return end if limbo[id] then WARN("Client id " .. id .. " is already in limbo") return end limbo[id] = gameInfo.Observers[slot] gameInfo.Observers[slot] = nil lobbyComm:BroadcastData( { Type = 'ClearObserver', Slot = slot, } ) SystemMessage(LOCF("%s had been moved to limbo and may choose to become an observer or player", limbo[id].ObserverName)) UpdateGame() end function MoveDepartedSoulToPlayer(id, playerSlot) if not limbo[id] then WARN("Client id " .. id .. " is not in limbo") return end if gameInfo.PlayerOptions[playerSlot] then WARN("Slot ".. playerSlot .. " already contains a player") return end -- handle add a little different if the departed soul was previously an observer (we need to create some data) if limbo[id].PlayerName then gameInfo.PlayerOptions[playerSlot] = limbo[id] else gameInfo.PlayerOptions[playerSlot] = LobbyComm.GetDefaultPlayerOptions(limbo[id].ObserverName) gameInfo.PlayerOptions[playerSlot].OwnerID = limbo[id].OwnerID for colorIndex,colorVal in gameColors.PlayerColors do if IsColorFree(colorIndex) then gameInfo.PlayerOptions[playerSlot].PlayerColor = colorIndex break end end end limbo[id] = nil lobbyComm:BroadcastData( { Type = 'SlotAssigned', Slot = playerSlot, Options = gameInfo.PlayerOptions[playerSlot], } ) SystemMessage(LOCF("%s has left limbo and become a player.", gameInfo.PlayerOptions[playerSlot].PlayerName)) UpdateGame() end function MoveDepartedSoulToObserver(id) if not limbo[id] then WARN("Client id " .. id .. " is not in limbo") return end local index = -1 for i = 1, LobbyComm.maxObservers do if gameInfo.Observers[i] == nil then index = i break end end if index == -1 then WARN("All observer slots are full.") return -1 end -- handle add a little different if the departed soul was previously an observer (we need to create some data) if limbo[id].PlayerName then gameInfo.Observers[index] = { ObserverName = limbo[id].PlayerName, OwnerID = limbo[id].OwnerID, } else gameInfo.Observers[index] = limbo[id] end limbo[id] = nil lobbyComm:BroadcastData( { Type = 'ObserverAdded', Slot = index, Options = gameInfo.Observers[index], } ) SystemMessage(LOCF("%s has left limbo and become an observer.", gameInfo.Observers[index].ObserverName)) UpdateGame() end -- go through limbo table and convert departed souls to observers if possible -- if not, eject the client function ConvertLimboPlayersToObservers() for id, clientInfo in limbo do if MoveDepartedSoulToObserver(id) == -1 then lobbyComm:EjectPlayer(id, 'NoLaunchLimbo') end end end function HostRemoveAI( slot ) if gameInfo.PlayerOptions[slot].Human then WARN('Use EjectPlayer to remove humans') return end ClearSlotInfo(slot) gameInfo.PlayerOptions[slot] = nil lobbyComm:BroadcastData( { Type = 'ClearSlot', Slot = slot, } ) UpdateGame() end function GetPlayersNotReady() local notReady = false for k,v in gameInfo.PlayerOptions do if v.Human and not v.Ready then if not notReady then notReady = {} end table.insert(notReady,v.PlayerName) end end return notReady end -- create UI won't typically be called directly by another module function CreateUI(maxPlayers) local Checkbox = import('/lua/modules/maui/checkbox.lua').Checkbox local Text = import('/lua/modules/maui/text.lua').Text local MapPreview = import('/lua/modules/ui/controls/mappreview.lua').MapPreview local MultiLineText = import('/lua/modules/maui/multilinetext.lua').MultiLineText local Combo = import('/lua/modules/ui/controls/combo.lua').Combo local StatusBar = import('/lua/modules/maui/statusbar.lua').StatusBar local BitmapCombo = import('/lua/modules/ui/controls/combo.lua').BitmapCombo local ItemList = import('/lua/modules/maui/itemlist.lua').ItemList local Prefs = import('/lua/modules/ui/prefs.lua') local Tooltip = import('/lua/modules/ui/game/tooltip.lua') if (GUI.connectdialog != false) then MenuCommon.MenuCleanup() GUI.connectdialog:Destroy() GUI.connectdialog = false end -- control layout if not GUI then GUI = UIUtil.CreateScreenGroup(GetFrame(0), "Lobby CreateUI ScreenGroup") end local title if OnlineProvider.CheckOnline() then title = LOCF("%s GAME LOBBY","GPGNet") GUI.background = MenuCommon.SetupBackground(GetFrame(0)) GUI.exitButton = MenuCommon.CreateExitMenuButton(GUI, GUI.background, "") else if singlePlayer then title = "" else title = "" end GUI.background = MenuCommon.SetupBackground(GetFrame(0)) GUI.exitButton = MenuCommon.CreateExitMenuButton(GUI, GUI.background, "", "Lobby_Back") end --------------------------------------------------------------------------- -- Set up main control panels --------------------------------------------------------------------------- -- TODO this will be replaced by a fixed size background GUI.panel = Bitmap(GUI, UIUtil.SkinnableFile("/lobby/lan-game-lobby/panel_bmp.dds")) LayoutHelpers.AtCenterIn(GUI.panel, GUI) local titleText = UIUtil.CreateText(GUI.panel, title, 30, UIUtil.titleFont) LayoutHelpers.AtLeftTopIn(titleText, GUI.panel, 38, 10) --TODO panels will get layout GUI.teamOptionsPanel = Group(GUI.panel, "teamOptionsPanel") LayoutHelpers.AtLeftTopIn(GUI.teamOptionsPanel, GUI.panel, 34, 48) GUI.teamOptionsPanel.Width:Set(706) GUI.teamOptionsPanel.Height:Set(38) GUI.playerPanel = Group(GUI.panel, "playerPanel") LayoutHelpers.AtLeftTopIn(GUI.playerPanel, GUI.panel, 34, 83) GUI.playerPanel.Width:Set(706) GUI.playerPanel.Height:Set(307) GUI.observerPanel = Group(GUI.panel, "observerPanel") LayoutHelpers.AtLeftTopIn(GUI.observerPanel, GUI.panel, 34, 394) GUI.observerPanel.Width:Set(706) GUI.observerPanel.Height:Set(124) GUI.chatPanel = Group(GUI.panel, "chatPanel") LayoutHelpers.AtLeftTopIn(GUI.chatPanel, GUI.panel, 34, 521) GUI.chatPanel.Width:Set(706) GUI.chatPanel.Height:Set(167) GUI.mapPanel = Group(GUI.panel, "mapPanel") LayoutHelpers.AtLeftTopIn(GUI.mapPanel, GUI.panel, 746, 56) GUI.mapPanel.Width:Set(238) GUI.mapPanel.Height:Set(352) GUI.optionsPanel = Group(GUI.panel, "optionsPanel") LayoutHelpers.AtLeftTopIn(GUI.optionsPanel, GUI.panel, 746, 413) GUI.optionsPanel.Width:Set(238) GUI.optionsPanel.Height:Set(272) GUI.launchPanel = Group(GUI.panel, "controlGroup") LayoutHelpers.AtLeftTopIn(GUI.launchPanel, GUI.panel, 746, 694) GUI.launchPanel.Width:Set(238) GUI.launchPanel.Height:Set(66) --------------------------------------------------------------------------- -- set up map panel --------------------------------------------------------------------------- local mapOverlay = Bitmap(GUI.mapPanel, UIUtil.SkinnableFile("/lobby/lan-game-lobby/map-pane-border_bmp.dds")) LayoutHelpers.AtLeftTopIn(mapOverlay, GUI.panel, 745, 51) mapOverlay:DisableHitTest() GUI.mapView = MapPreview(GUI.mapPanel) LayoutHelpers.AtCenterIn(GUI.mapView, mapOverlay) GUI.mapView.Width:Set(195) GUI.mapView.Height:Set(195) mapOverlay.Depth:Set(function() return GUI.mapView.Depth() + 10 end) GUI.mapName = UIUtil.CreateText(GUI.mapPanel, "", 12, UIUtil.titleFont) GUI.mapName:SetColor(UIUtil.bodyColor) LayoutHelpers.CenteredBelow(GUI.mapName, mapOverlay) GUI.changeMapButton = UIUtil.CreateButtonStd(GUI.mapPanel, '/lobby/lan-game-lobby/small', "", 10, 0) LayoutHelpers.AtBottomIn(GUI.changeMapButton, GUI.mapPanel, 10) LayoutHelpers.AtHorizontalCenterIn(GUI.changeMapButton, GUI.mapPanel) GUI.mapSize = UIUtil.CreateText(GUI.mapPanel, "", 12, UIUtil.bodyFont) LayoutHelpers.CenteredBelow(GUI.mapSize, GUI.mapName) GUI.mapNumPlayers = UIUtil.CreateText(GUI.mapPanel, "", 12, UIUtil.bodyFont) LayoutHelpers.CenteredBelow(GUI.mapNumPlayers, GUI.mapSize) -- hide unless we're the game host GUI.changeMapButton:Hide() Tooltip.AddButtonTooltip(GUI.changeMapButton, 'lob_select_map') GUI.changeMapButton.OnClick = function(self) local mapSelectDialog local function selectBehavior(selectedScenario) Prefs.SetToCurrentProfile('LastScenario', selectedScenario.file) mapSelectDialog:Destroy() SetGameOption('ScenarioFile',selectedScenario.file) end local function exitBehavior() mapSelectDialog:Destroy() end mapSelectDialog = import('/lua/modules/ui/dialogs/mapselect.lua').CreateDialog( selectBehavior, exitBehavior, GUI.panel, gameInfo.GameOptions.ScenarioFile, GetPlayerCount() ) end --------------------------------------------------------------------------- -- set up launch panel --------------------------------------------------------------------------- GUI.launchGameButton = UIUtil.CreateButtonStd(GUI.launchPanel, '/lobby/lan-game-lobby/large', "Launch", 18) LayoutHelpers.AtCenterIn(GUI.launchGameButton, GUI.launchPanel) Tooltip.AddButtonTooltip(GUI.launchGameButton, 'Lobby_Launch') -- hide unless we're the game host GUI.launchGameButton:Hide() GUI.launchGameButton.OnClick = function(self) if not singlePlayer then local notReady = GetPlayersNotReady() if notReady then for k,v in notReady do SystemMessage(LOCF("%s isn't ready.",v)) end return end end -- make sure there are some players (could all be in limbo or observers?) local totalPlayers = 0 for slot, player in gameInfo.PlayerOptions do if player then totalPlayers = totalPlayers + 1 end end if totalPlayers == 0 then SystemMessage(LOC("There are no players assigned to player slots, can not continue")) return end -- if there are players in limbo, ask if you really want to continue local limboCount = 0 for key, val in limbo do limboCount = limboCount + 1 end -- the acutal launch behavior is in here so it can be a callback from any dialog boxes that need to appear local function DoLaunch() ConvertLimboPlayersToObservers() -- assign random factions just as game is launched AssignRandomFactions(gameInfo) AssignAINames(gameInfo) LOG('Launching with gameInfo: ', repr(gameInfo)) -- Tell everyone else to launch and then launch ourselves. lobbyComm:BroadcastData( { Type = 'Launch', GameInfo = gameInfo } ) lobbyComm:LaunchGame(gameInfo) end if limboCount != 0 then local doRet = false UIUtil.QuickDialog(GUI, "There are clients in limbo. They can still choose to play or be observers. If you launch, they will be converted to observers, if possible. Do you still wish to launch?", "", DoLaunch, "",nil, nil, nil, true) else DoLaunch() end end --------------------------------------------------------------------------- -- set up chat display --------------------------------------------------------------------------- GUI.chatEdit = Edit(GUI.chatPanel) LayoutHelpers.AtLeftTopIn(GUI.chatEdit, GUI.panel, 78, 655) GUI.chatEdit.Width:Set(650) GUI.chatEdit.Height:Set(14) GUI.chatEdit:SetFont(UIUtil.bodyFont, 14) GUI.chatEdit:SetForegroundColor(UIUtil.fontColor) GUI.chatEdit:SetHighlightBackgroundColor(UIUtil.highlightColor) GUI.chatEdit:SetHighlightForegroundColor(UIUtil.fontColor) GUI.chatDisplay = ItemList(GUI.chatPanel) GUI.chatDisplay:SetFont(UIUtil.bodyFont, 14) GUI.chatDisplay:SetColors(UIUtil.fontColor(), "00000000", UIUtil.fontColor(), "00000000") LayoutHelpers.AtLeftTopIn(GUI.chatDisplay, GUI.panel, 45, 530) GUI.chatDisplay.Bottom:Set(function() return GUI.chatEdit.Top() - 10 end) GUI.chatDisplay.Right:Set(function() return GUI.chatPanel.Right() - 39 end) GUI.chatDisplay.Height:Set(function() return GUI.chatDisplay.Bottom() - GUI.chatDisplay.Top() end) GUI.chatDisplay.Width:Set(GUI.chatPanel.Width) GUI.chatDisplayScroll = UIUtil.CreateVertScrollbarFor(GUI.chatDisplay) OnlineProvider.RegisterChatDisplay(GUI.chatDisplay) GUI.chatEdit.OnEnterPressed = function(self, text) if text != "" then OnlineProvider.SendMessage(text) table.insert(commandQueue, 1, text) commandQueueIndex = 0 if GUI.chatDisplay then --this next section just removes /commmands from broadcasting. local startindex = 0 local endindex = 0 startindex, endindex = string.find(text, "/") if startindex ~= 1 then PublicChat(text) end end end end GUI.chatEdit.OnNonTextKeyPressed = function(self, keyCode) if commandQueue and table.getsize(commandQueue) > 0 then if keyCode == 38 then if commandQueue[commandQueueIndex + 1] then commandQueueIndex = commandQueueIndex + 1 self:SetText(commandQueue[commandQueueIndex]) end end if keyCode == 40 then if commandQueueIndex != 1 then if commandQueue[commandQueueIndex - 1] then commandQueueIndex = commandQueueIndex - 1 self:SetText(commandQueue[commandQueueIndex]) end else commandQueueIndex = 0 self:ClearText() end end end end --------------------------------------------------------------------------- -- Option helper functions --------------------------------------------------------------------------- local function CreateOptionCombo(parent, optionData, width) local combo = Combo(parent) combo.Width:Set(width) local itemArray = {} combo.keyMap = {} for index, val in optionData.values do itemArray[index] = val.text combo.keyMap[val.key] = index end local defValue = Prefs.GetFromCurrentProfile(optionData.pref) or optionData.default combo:AddItems(itemArray, defValue) OnlineProvider.ChangeOption(string.format("gameoption %s %s", optionData.label, optionData.values[defValue].text)) combo.OnClick = function(self, index, text) if optionData.pref then Prefs.SetToCurrentProfile(optionData.pref, index) end SetGameOption(optionData.key,optionData.values[index].key) OnlineProvider.ChangeOption(string.format("gameoption %s %s", optionData.label, optionData.values[index].text)) end GUI.optionControls[optionData.key] = combo combo.UpdateValue = function(key) combo:SetItem(combo.keyMap[key]) end return combo end --------------------------------------------------------------------------- -- set up team option buttons --------------------------------------------------------------------------- GUI.teamOptionsLabel = UIUtil.CreateText(GUI.teamOptionsPanel, "Team Options", 18, UIUtil.titleFont) LayoutHelpers.AtLeftTopIn(GUI.teamOptionsLabel, GUI.panel, 45, 57) local prev = GUI.teamOptionsLabel local posTable = { 274, 515, } for index, val in teamOpts do local label = UIUtil.CreateText(GUI.teamOptionsPanel, val.label, 12, UIUtil.titleFont) LayoutHelpers.AtLeftTopIn(label, GUI.panel, posTable[index], 59) local pref = val.pref if pref then Tooltip.AddControlTooltip(label, pref) end local combo = CreateOptionCombo(GUI.teamOptionsPanel, val, 160) LayoutHelpers.AtLeftTopIn(combo, GUI.panel, posTable[index] + 60, 57) prev = combo end --------------------------------------------------------------------------- -- set up options --------------------------------------------------------------------------- GUI.optionsLabel = UIUtil.CreateText(GUI.teamOptionsPanel, "Options", 14, UIUtil.titleFont) LayoutHelpers.AtLeftTopIn(GUI.optionsLabel, GUI.optionsPanel, 5, 5) prev = GUI.optionsLabel local offset = 3 for index, val in globalOpts do local bg = Bitmap(GUI.optionsPanel) bg:SetSolidColor('00000000') bg.Width:Set(function() return GUI.optionsPanel.Width() - 10 end) bg.Height:Set(20) local label = UIUtil.CreateText(bg, val.label, 12, UIUtil.bodyFont) LayoutHelpers.AtLeftTopIn(label, bg) local pref = val.pref if pref then Tooltip.AddControlTooltip(label, pref) end local lastControl = prev local curOffset = offset bg.Top:Set(function() return lastControl.Bottom() + curOffset end) if offset == 3 then offset = 6 end LayoutHelpers.AtHorizontalCenterIn(bg, GUI.optionsPanel) local combo = CreateOptionCombo(bg, val, 120) combo.Right:Set(bg.Right) combo.Top:Set(label.Top) prev = label end -- prev at this point is last control GUI.advancedOptionsButton = UIUtil.CreateButtonStd(GUI.optionsPanel, '/lobby/lan-game-lobby/small', "Advanced", 12) LayoutHelpers.AtLeftTopIn(GUI.advancedOptionsButton, GUI.panel, 793, 583) GUI.advancedOptionsButton.Onclick = function(self, modifiers) Tooltip.AddButtonTooltip(GUI.advancedOptionsButton, 'Lobby_Advanced') GUI.modOptionsButton = UIUtil.CreateButtonStd(GUI.optionsPanel, '/lobby/lan-game-lobby/small', "Mods", 12) LayoutHelpers.Below(GUI.modOptionsButton, GUI.advancedOptionsButton, -5) GUI.modOptionsButton.OnClick = function(self, modifiers) import('/lua/modules/ui/dialogs/modmanager.lua').CreateDialog(GUI, true) end Tooltip.AddButtonTooltip(GUI.modOptionsButton, 'Lobby_Mods') end GUI.loadButton = UIUtil.CreateButtonStd(GUI.optionsPanel, '/lobby/lan-game-lobby/small', "Load", 12) LayoutHelpers.Below(GUI.loadButton, GUI.modOptionsButton, -5) GUI.loadButton.OnClick = function(self, modifiers) import('/lua/modules/ui/dialogs/saveload.lua').CreateLoadDialog(GUI) end Tooltip.AddButtonTooltip(GUI.loadButton, 'Lobby_Load') if UIUtil.BETA_FLAG then GUI.loadButton:Disable() end --------------------------------------------------------------------------- -- set up player grid --------------------------------------------------------------------------- -- set up player "slots" which is the line representing a player and player specific options local prev = nil local slotColumnSizes = { player = {x = 43, width = 358}, color = {x = 412, width = 59}, faction = {x = 480, width = 59}, team = {x = 548, width = 60}, ping = {x = 615, width = 62}, ready = {x = 681, width = 51}, } GUI.labelGroup = Group(GUI.playerPanel) GUI.labelGroup.Width:Set(690) GUI.labelGroup.Height:Set(31) LayoutHelpers.AtLeftTopIn(GUI.labelGroup, GUI.playerPanel, 5, 5) GUI.nameLabel = UIUtil.CreateText(GUI.labelGroup, "Player Name", 14, UIUtil.titleFont) LayoutHelpers.AtLeftIn(GUI.nameLabel, GUI.panel, slotColumnSizes.player.x) LayoutHelpers.AtVerticalCenterIn(GUI.nameLabel, GUI.labelGroup) Tooltip.AddControlTooltip(GUI.nameLabel, 'lob_slot') GUI.colorLabel = UIUtil.CreateText(GUI.labelGroup, "Color", 14, UIUtil.titleFont) LayoutHelpers.AtLeftIn(GUI.colorLabel, GUI.panel, slotColumnSizes.color.x) LayoutHelpers.AtVerticalCenterIn(GUI.colorLabel, GUI.labelGroup) Tooltip.AddControlTooltip(GUI.colorLabel, 'lob_color') GUI.factionLabel = UIUtil.CreateText(GUI.labelGroup, "Faction", 14, UIUtil.titleFont) LayoutHelpers.AtLeftIn(GUI.factionLabel, GUI.panel, slotColumnSizes.faction.x) LayoutHelpers.AtVerticalCenterIn(GUI.factionLabel, GUI.labelGroup) Tooltip.AddControlTooltip(GUI.factionLabel, 'lob_faction') GUI.teamLabel = UIUtil.CreateText(GUI.labelGroup, "Team", 14, UIUtil.titleFont) LayoutHelpers.AtLeftIn(GUI.teamLabel, GUI.panel, slotColumnSizes.team.x) LayoutHelpers.AtVerticalCenterIn(GUI.teamLabel, GUI.labelGroup) Tooltip.AddControlTooltip(GUI.teamLabel, 'lob_team') GUI.pingLabel = UIUtil.CreateText(GUI.labelGroup, "Ping", 14, UIUtil.titleFont) LayoutHelpers.AtLeftIn(GUI.pingLabel, GUI.panel, slotColumnSizes.ping.x) LayoutHelpers.AtVerticalCenterIn(GUI.pingLabel, GUI.labelGroup) GUI.readyLabel = UIUtil.CreateText(GUI.labelGroup, "Ready", 14, UIUtil.titleFont) LayoutHelpers.AtLeftIn(GUI.readyLabel, GUI.panel, slotColumnSizes.ready.x) LayoutHelpers.AtVerticalCenterIn(GUI.readyLabel, GUI.labelGroup) for i= 1, LobbyComm.maxPlayerSlots do -- capture the index in the current closure so it's accessible on callbacks local curRow = i GUI.slots[i] = Group(GUI.playerPanel, "playerSlot " .. tostring(i)) GUI.slots[i].closed = false --TODO these need layout from art when available GUI.slots[i].Width:Set(GUI.labelGroup.Width) GUI.slots[i].Height:Set(GUI.labelGroup.Height) GUI.slots[i]._slot = i local bg = GUI.slots[i] GUI.slots[i].name = Combo(bg, 16, 10, true) LayoutHelpers.AtVerticalCenterIn(GUI.slots[i].name, GUI.slots[i]) LayoutHelpers.AtLeftIn(GUI.slots[i].name, GUI.panel, slotColumnSizes.player.x) GUI.slots[i].name.Width:Set(slotColumnSizes.player.width) GUI.slots[i].name.row = i -- left deal with name clicks GUI.slots[i].name.OnClick = function(self, index, text) DoSlotBehavior(self.row, self.slotKeys[index], text) end GUI.slots[i].color = BitmapCombo(bg, gameColors.PlayerColors, 1, true) LayoutHelpers.AtLeftIn(GUI.slots[i].color, GUI.panel, slotColumnSizes.color.x) LayoutHelpers.AtVerticalCenterIn(GUI.slots[i].color, GUI.slots[i]) GUI.slots[i].color.Width:Set(slotColumnSizes.color.width) GUI.slots[i].color.row = i GUI.slots[i].color.OnClick = function(self, index) if not lobbyComm:IsHost() then lobbyComm:SendData(hostID, { Type = 'RequestColor', Color = index, Slot = self.row } ) gameInfo.PlayerOptions[self.row].PlayerColor = index gameInfo.PlayerOptions[self.row].ArmyColor = index UpdateGame() else if IsColorFree(index) then lobbyComm:BroadcastData( { Type = 'SetColor', Color = index, Slot = self.row } ) gameInfo.PlayerOptions[self.row].PlayerColor = index gameInfo.PlayerOptions[self.row].ArmyColor = index UpdateGame() else self:SetItem( gameInfo.PlayerOptions[self.row].PlayerColor ) end end end GUI.slots[i].color.row = i GUI.slots[i].faction = BitmapCombo(bg, factionBmps, table.getn(factionBmps)) LayoutHelpers.AtLeftIn(GUI.slots[i].faction, GUI.panel, slotColumnSizes.faction.x) LayoutHelpers.AtVerticalCenterIn(GUI.slots[i].faction, GUI.slots[i]) GUI.slots[i].faction.Width:Set(slotColumnSizes.faction.width) GUI.slots[i].faction.OnClick = function(self, index) SetPlayerOption(self.row,'Faction',index) end GUI.slots[i].faction.row = i GUI.slots[i].team = Combo(bg) LayoutHelpers.AtLeftIn(GUI.slots[i].team, GUI.panel, slotColumnSizes.team.x) LayoutHelpers.AtVerticalCenterIn(GUI.slots[i].team, GUI.slots[i]) GUI.slots[i].team.Width:Set(slotColumnSizes.team.width) GUI.slots[i].team:AddItems(teamNumbers, 1) GUI.slots[i].team.row = i GUI.slots[i].team.OnClick = function(self, index, text) SetPlayerOption(self.row,'Team',index) end if not singlePlayer then GUI.slots[i].pingGroup = Group(bg) GUI.slots[i].pingGroup.Width:Set(slotColumnSizes.ping.width) GUI.slots[i].pingGroup.Height:Set(GUI.slots[curRow].Height) LayoutHelpers.AtLeftIn(GUI.slots[i].pingGroup, GUI.panel, slotColumnSizes.ping.x) LayoutHelpers.AtVerticalCenterIn(GUI.slots[i].pingGroup, GUI.slots[i]) GUI.slots[i].pingText = UIUtil.CreateText(GUI.slots[i].pingGroup, "0", 14, UIUtil.bodyFont) LayoutHelpers.AtBottomIn(GUI.slots[i].pingText, GUI.slots[i].pingGroup) LayoutHelpers.AtHorizontalCenterIn(GUI.slots[i].pingText, GUI.slots[i].pingGroup) GUI.slots[i].pingStatus = StatusBar(GUI.slots[i].pingGroup, 0, 1000, false, false, UIUtil.SkinnableFile('/game/unit_bmp/bar-back_bmp.dds'), UIUtil.SkinnableFile('/game/unit_bmp/bar-01_bmp.dds'), true) LayoutHelpers.AtTopIn(GUI.slots[i].pingStatus, GUI.slots[i].pingGroup) LayoutHelpers.AtLeftIn(GUI.slots[i].pingStatus, GUI.slots[i].pingGroup, 5) LayoutHelpers.AtRightIn(GUI.slots[i].pingStatus, GUI.slots[i].pingGroup, 5) GUI.slots[i].pingStatus.Bottom:Set(GUI.slots[curRow].pingText.Top) end -- depending on if this is single player or multiplayer this displays different info GUI.slots[i].multiSpace = Group(bg, "multiSpace " .. tonumber(i)) GUI.slots[i].multiSpace.Width:Set(slotColumnSizes.ready.width) GUI.slots[i].multiSpace.Height:Set(GUI.slots[curRow].Height) LayoutHelpers.AtLeftIn(GUI.slots[i].multiSpace, GUI.panel, slotColumnSizes.ready.x) GUI.slots[i].multiSpace.Top:Set(GUI.slots[curRow].Top) if not singlePlayer then GUI.slots[i].ready = UIUtil.CreateCheckboxStd(GUI.slots[i].multiSpace, '/dialogs/check-box_btn/radio') GUI.slots[i].ready.row = i LayoutHelpers.AtVerticalCenterIn(GUI.slots[curRow].ready, GUI.slots[curRow].multiSpace) LayoutHelpers.AtLeftIn(GUI.slots[curRow].ready, GUI.slots[curRow].multiSpace) GUI.slots[i].ready.OnCheck = function(self, checked) SetPlayerOption(self.row,'Ready',checked) end end if i == 1 then LayoutHelpers.Below(GUI.slots[i], GUI.labelGroup, -5) else LayoutHelpers.Below(GUI.slots[i], GUI.slots[i - 1], 3) end end function EnableSlot(slot) GUI.slots[slot].team:Enable() GUI.slots[slot].color:Enable() GUI.slots[slot].faction:Enable() if GUI.slots[slot].ready then GUI.slots[slot].ready:Enable() end end function DisableSlot(slot) GUI.slots[slot].team:Disable() GUI.slots[slot].color:Disable() GUI.slots[slot].faction:Disable() if GUI.slots[slot].ready then GUI.slots[slot].ready:Disable() end end -- Initially clear all slots for slot = 1, maxPlayers do ClearSlotInfo(slot) end --------------------------------------------------------------------------- -- set up observer grid --------------------------------------------------------------------------- GUI.allowObservers = nil GUI.observerList = nil if not singlePlayer then GUI.observerLabel = UIUtil.CreateText(GUI.observerPanel, "Observers", 14, UIUtil.bodyFont) LayoutHelpers.AtLeftTopIn(GUI.observerLabel, GUI.observerPanel, 5, 5) GUI.allowObserversLabel = UIUtil.CreateText(GUI.observerPanel, "Allow", 14, UIUtil.bodyFont) LayoutHelpers.AtTopIn(GUI.allowObserversLabel, GUI.observerPanel, 5) LayoutHelpers.AtRightIn(GUI.allowObserversLabel,GUI.observerPanel, 5) GUI.allowObservers = UIUtil.CreateCheckboxStd(GUI.observerPanel, '/dialogs/check-box_btn/radio') GUI.allowObservers.Right:Set(GUI.allowObserversLabel.Left) LayoutHelpers.AtTopIn(GUI.allowObservers, GUI.observerPanel) GUI.allowObservers:SetCheck(true) SetGameOption("AllowObservers",true) GUI.allowObservers.OnCheck = function(self, checked) if not checked then -- LobFuncs.allowObservers(false) -- kick all the observers as well for k,v in gameInfo.Observers do lobbyComm:EjectPlayer(v.OwnerID,"NoObservers") end end SetGameOption("AllowObservers",checked) end GUI.allowObservers.OnHide = function(self, hidden) GUI.allowObserversLabel:SetHidden(hidden) end GUI.allowObservers:Hide() GUI.becomeObserver = UIUtil.CreateButtonStd(GUI.observerPanel, '/lobby/lan-game-lobby/toggle', "Observe", 10, 0) LayoutHelpers.LeftOf(GUI.becomeObserver, GUI.allowObservers) GUI.becomeObserver.OnClick = function(self, modifiers) if IsPlayer(localPlayerID) then if lobbyComm:IsHost() then HostConvertPlayerToObserver(hostID, localPlayerName, FindSlotForID(localPlayerID)) else lobbyComm:SendData(hostID, {Type = 'RequestConvertToObserver', RequestedName = localPlayerName, RequestedSlot = FindSlotForID(localPlayerID)}) end else if lobbyComm:IsHost() then MoveDepartedSoulToObserver(hostID) else lobbyComm:SendData(hostID, {Type = 'RequestMoveDepartedSoulToObserver'}) end end end GUI.observerList = ItemList(GUI.observerPanel, "observer list") GUI.observerList:SetFont(UIUtil.bodyFont, 14) GUI.observerList:SetColors(UIUtil.fontColor, "00000000", UIUtil.fontOverColor, UIUtil.highlightColor) LayoutHelpers.Below(GUI.observerList, GUI.observerLabel, 10) GUI.observerList.Bottom:Set(function() return GUI.observerPanel.Bottom() - 5 end) GUI.observerList.Right:Set(function() return GUI.observerPanel.Right() - 30 end) UIUtil.CreateVertScrollbarFor(GUI.observerList) end --------------------------------------------------------------------------- -- other logic, including lobby callbacks --------------------------------------------------------------------------- GUI.posGroup = false -- control behvaior GUI.exitButton.OnClick = function(self) ReturnToMenu() end -- get ping times GUI.pingThread = ForkThread( function() while true and lobbyComm do for slot,player in gameInfo.PlayerOptions do if player.Human and player.OwnerID != localPlayerID then local peer = lobbyComm:GetPeer(player.OwnerID) local ping = math.floor(peer.ping) GUI.slots[slot].pingText:SetText(tostring(ping)) GUI.slots[slot].pingStatus:SetValue(ping) end end WaitSeconds(1) end end ) end function AddChatText(text) local textBoxWidth = GUI.chatDisplay.Width() local wrapped = import('/lua/modules/maui/text.lua').WrapText(text, textBoxWidth, function(curText) return GUI.chatDisplay:GetStringAdvance(curText) end) for i, line in wrapped do GUI.chatDisplay:AddItem(line) end GUI.chatDisplay:ScrollToBottom() end function ShowMapPositions(mapCtrl, scenario, numPlayers) if nil == scenario.starts then scenario.starts = true end if GUI.posGroup then GUI.posGroup:Destroy() GUI.posGroup = false end if not scenario.starts then return end if not scenario.size then LOG("Lobby: Can't show map positions as size field isn't in scenario yet (must be resaved with new editor!)") return end GUI.posGroup = Group(mapCtrl) LayoutHelpers.FillParent(GUI.posGroup, mapCtrl) local startPos = MapUtil.GetStartPositions(scenario) local cHeight = GUI.posGroup:Height() local cWidth = GUI.posGroup:Width() local mWidth = scenario.size[1] local mHeight = scenario.size[2] local teamSpawnTypeIndex = GUI.optionControls['TeamSpawn']:GetItem() local teamSpawnType = teamOpts[1].values[teamSpawnTypeIndex].key local playerArmyArray = MapUtil.GetArmies(scenario) for slot, army in playerArmyArray do local pos = startPos[army] local marker = UIUtil.CreateText(GUI.posGroup, "", 14, UIUtil.bodyFont) marker:SetDropShadow(true) LayoutHelpers.AtLeftTopIn(marker, GUI.posGroup, (pos[1] / mWidth) * cWidth, (pos[2] / mHeight) * cHeight) if teamSpawnType == 'random' then marker:SetColor("yellow") marker:SetText("?") else if gameInfo.PlayerOptions[slot] then marker:SetColor(gameColors.PlayerColors[gameInfo.PlayerOptions[slot].PlayerColor]) marker:SetText(tostring(slot)) else marker:SetColor("yellow") marker:SetText("X") end end -- TODO deal with team spawn when we flesh it out end end -- LobbyComm Callbacks function InitLobbyComm(protocol, localPort, desiredPlayerName, localPlayerUID, natTraversalProvider) lobbyComm = LobbyComm.CreateLobbyComm(protocol, localPort, desiredPlayerName, localPlayerUID, natTraversalProvider) lobbyComm.Connecting = function(self) local function OnCancel() lobbyComm:Destroy() if GUI.pingThead then KillThread(GUI.pingThread) end if GUI.keepAliveThread then KillThread(GUI.keepAliveThread) end OnlineProvider.UnregisterChat() ReturnToMenu() return end GUI.connectingDialog = UIUtil.ShowInfoDialog(GUI.panel, Strings.Connecting, "", OnCancel) end lobbyComm.ConnectionFailed = function(self, reason) LOG("CONNECTION FAILED ",reason) if GUI.connectingDialog then GUI.connectingDialog:Destroy() end GUI.connectionFailedDialog = UIUtil.ShowInfoDialog(GUI.panel, LOCF(Strings.ConnectionFailed, reason), "", ReturnToMenu) end lobbyComm.LaunchFailed = function(self,reasonKey) if GUI.chatDisplay then AddChatText(LOC(Strings[reasonKey])) --GUI.chatDisplay:AddItem(LOC(Strings[reasonKey])) end end lobbyComm.Ejected = function(self,reason) LOG("EJECTED ",reason) if GUI.connectingDialog then GUI.connectingDialog:Destroy() end GUI.connectionFailedDialog = UIUtil.ShowInfoDialog(GUI.panel, LOCF(Strings.Ejected, reason), ReturnToMenu) end lobbyComm.ConnectionToHostEstablished = function(self,myID,theHostID) if GUI.connectingDialog then GUI.connectingDialog:Destroy() end hostID = theHostID localPlayerID = myID if wantToBeObserver then -- Ok, I'm connected to the host. Now request to become an observer lobbyComm:SendData( hostID, { Type = 'AddObserver', RequestedObserverName = localPlayerName, } ) else -- Ok, I'm connected to the host. Now request to become a player lobbyComm:SendData( hostID, { Type = 'AddPlayer', RequestedSlot = -1, RequestedPlayerName = localPlayerName, Human = true, } ) end end lobbyComm.DataReceived = function(self,data) --LOG('DATA RECEIVED: ', repr(data)) -- Messages anyone can receive if data.Type == 'PlayerOption' then if gameInfo.PlayerOptions[data.Slot].OwnerID != data.SenderID then WARN("Attempt to set option on unowned slot.") return end gameInfo.PlayerOptions[data.Slot][data.Key] = data.Value UpdateGame() elseif data.Type == 'PublicChat' then if GUI.chatDisplay then AddChatText("["..data.SenderName.."] "..data.Text) --GUI.chatDisplay:AddItem("["..data.SenderName.."] "..data.Text) end elseif data.Type == 'PrivateChat' then if GUI.chatDisplay then AddChatText("<<"..data.SenderName..">> "..data.Text) --GUI.chatDisplay:AddItem("<<"..data.SenderName..">> "..data.Text) end end if lobbyComm:IsHost() then -- Host only messages if data.Type == 'GetGameInfo' then lobbyComm:SendData( data.SenderID, {Type = 'GameInfo', GameInfo = gameInfo} ) elseif data.Type == 'AddPlayer' then -- create empty slot if possible and give it to the player HostTryAddPlayer( data.SenderID, data.RequestedSlot, data.RequestedPlayerName, data.Human, data.AIPersonality ) elseif data.Type == 'MovePlayer' then -- attempt to move a player from current slot to empty slot HostTryMovePlayer(data.SenderID, data.CurrentSlot, data.RequestedSlot) elseif data.Type == 'AddObserver' then -- create empty slot if possible and give it to the observer HostTryAddObserver( data.SenderID, data.RequestedObserverName ) elseif data.Type == 'RequestConvertToObserver' then HostConvertPlayerToObserver(data.SenderID, data.RequestedName, data.RequestedSlot) elseif data.Type == 'RequestConvertToPlayer' then HostConvertObserverToPlayer(data.SenderID, data.RequestedName, data.ObserverSlot, data.PlayerSlot) elseif data.Type == 'RequestMoveDepartedSoulToPlayer' then MoveDepartedSoulToPlayer(data.SenderID, data.PlayerSlot) elseif data.Type == 'RequestMoveDepartedSoulToObserver' then MoveDepartedSoulToObserver(data.SenderID) elseif data.Type == 'RequestColor' then if IsColorFree(data.Color) then -- Color is available, let everyone else know gameInfo.PlayerOptions[data.Slot].PlayerColor = data.Color lobbyComm:BroadcastData( { Type = 'SetColor', Color = data.Color, Slot = data.Slot } ) UpdateGame() else -- Sorry, it's not free. Force the player back to the color we have for him. lobbyComm:SendData( data.SenderID, { Type = 'SetColor', Color = gameInfo.PlayerOptions[data.Slot].PlayerColor, Slot = data.Slot } ) end elseif data.Type == 'ClearSlot' then if gameInfo.PlayerOptions[data.Slot].OwnerID == data.SenderID then HostRemoveAI(data.Slot) else WARN("Attempt to clear unowned slot") end end else -- Non-host only messages if data.Type == 'SystemMessage' then if GUI.chatDisplay then AddChatText(data.Text) --GUI.chatDisplay:AddItem(data.Text) end elseif data.Type == 'SlotAssigned' then if data.Options.OwnerID == localPlayerID and data.Options.Human then -- The new slot is for us. Request the full game info from the host localPlayerName = data.Options.PlayerName -- validated by server lobbyComm:SendData( hostID, {Type = "GetGameInfo"} ) else -- The new slot was someone else, just add that info. gameInfo.PlayerOptions[data.Slot] = data.Options end UpdateGame() elseif data.Type == 'SlotMove' then if data.Options.OwnerID == localPlayerID and data.Options.Human then localPlayerName = data.Options.PlayerName -- validated by server lobbyComm:SendData( hostID, {Type = "GetGameInfo"} ) else gameInfo.PlayerOptions[data.OldSlot] = nil gameInfo.PlayerOptions[data.NewSlot] = data.Options end ClearSlotInfo(data.OldSlot) UpdateGame() elseif data.Type == 'ObserverAdded' then if data.Options.OwnerID == localPlayerID then -- The new slot is for us. Request the full game info from the host localPlayerName = data.Options.ObserverName -- validated by server lobbyComm:SendData( hostID, {Type = "GetGameInfo"} ) else -- The new slot was someone else, just add that info. gameInfo.Observers[data.Slot] = data.Options end UpdateGame() elseif data.Type == 'ConvertObserverToPlayer' then if data.Options.OwnerID == localPlayerID then lobbyComm:SendData( hostID, {Type = "GetGameInfo"} ) else gameInfo.Observers[data.OldSlot] = nil gameInfo.PlayerOptions[data.NewSlot] = data.Options end UpdateGame() elseif data.Type == 'ConvertPlayerToObserver' then if data.Options.OwnerID == localPlayerID then lobbyComm:SendData( hostID, {Type = "GetGameInfo"} ) else gameInfo.Observers[data.NewSlot] = data.Options gameInfo.PlayerOptions[data.OldSlot] = nil end ClearSlotInfo(data.OldSlot) UpdateGame() elseif data.Type == 'SetColor' then gameInfo.PlayerOptions[data.Slot].PlayerColor = data.Color gameInfo.PlayerOptions[data.Slot].ArmyColor = data.Color UpdateGame() elseif data.Type == 'GameInfo' then -- Note: this nukes whatever options I may have set locally gameInfo = data.GameInfo --LOG('Got GameInfo: ', repr(gameInfo)) UpdateGame() elseif data.Type == 'GameOption' then gameInfo.GameOptions[data.Key] = data.Value UpdateGame() elseif data.Type == 'Launch' then lobbyComm:LaunchGame(data.GameInfo) elseif data.Type == 'ClearSlot' then ClearSlotInfo(data.Slot) gameInfo.PlayerOptions[data.Slot] = nil elseif data.Type == 'ClearObserver' then gameInfo.Observers[data.Slot] = nil UpdateGame() end end end lobbyComm.SystemMessage = function(self, text) if GUI.chatDisplay then AddChatText(text) end end lobbyComm.GameLaunched = function(self) OnlineProvider.Launching() OnlineProvider.UnregisterChat() if GUI.pingThead then KillThread(GUI.pingThread) end if GUI.keepAliveThread then KillThread(GUI.keepAliveThread) end GUI:Destroy() GUI = false MenuCommon.MenuCleanup() lobbyComm:Destroy() lobbyComm = false end lobbyComm.Hosting = function(self) localPlayerID = lobbyComm:GetLocalPlayerID() hostID = localPlayerID -- Give myself the first slot gameInfo.PlayerOptions[1] = LobbyComm.GetDefaultPlayerOptions(localPlayerName) gameInfo.PlayerOptions[1].OwnerID = localPlayerID gameInfo.PlayerOptions[1].Human = true -- set default lobby values for index, option in teamOpts do local defValue = Prefs.GetFromCurrentProfile(option.pref) or option.default SetGameOption(option.key,option.values[defValue].key) end for index, option in globalOpts do local defValue = Prefs.GetFromCurrentProfile(option.pref) or option.default SetGameOption(option.key,option.values[defValue].key) end if self.desiredScenario and self.desiredScenario != "" then Prefs.SetToCurrentProfile('LastScenario', self.desiredScenario) SetGameOption('ScenarioFile',self.desiredScenario) else local scen = Prefs.GetFromCurrentProfile('LastScenario') if scen and scen != "" then SetGameOption('ScenarioFile',scen) end end UpdateGame() GUI.keepAliveThread = ForkThread( -- Eject players who haven't sent a heartbeat in a while function() while true and lobbyComm do local peers = lobbyComm:GetPeers() for k,peer in peers do if peer.quiet > LobbyComm.quietTimeout then local slot = FindSlotForID(peer.id) if slot then SystemMessage(LOCF(Strings.TimedOut,gameInfo.PlayerOptions[slot].PlayerName)) end lobbyComm:EjectPlayer(peer.id,'TimedOutToHost') end end WaitSeconds(1) end end ) end lobbyComm.PeerDisconnected = function(self,peerName,peerID) --LOG('PeerDisconnected : ', peerName, ' ', peerID) --LOG('GameInfo = ', repr(gameInfo)) local slot = FindSlotForID(peerID) if slot then ClearSlotInfo( slot ) gameInfo.PlayerOptions[slot] = nil else slot = FindObserverSlotForID(peerID) if slot then gameInfo.Observers[slot] = nil UpdateGame() else limbo[peerID] = nil end end end lobbyComm.GameConfigRequested = function(self) return { Options = gameInfo.GameOptions, HostedBy = localPlayerName, PlayerCount = GetPlayerCount(), GameName = gameName, } end end function SetPlayerOption(slot, key, val) if not IsLocallyOwned(slot) then WARN("Hey you can't set a player option on a slot you don't own.") return end gameInfo.PlayerOptions[slot][key] = val lobbyComm:BroadcastData( { Type = 'PlayerOption', Key = key, Value = val, Slot = slot, } ) end function SetGameOption(key, val) if lobbyComm:IsHost() then gameInfo.GameOptions[key] = val UpdateGame() else WARN('Attempt to set game option by a non-host') return end lobbyComm:BroadcastData( { Type = 'GameOption', Key = key, Value = val, } ) end function DebugDump() if lobbyComm then lobbyComm:DebugDump() end end