local textFiles = require("scripts/textFiles")

local perPlayerCostumes = require("scripts/perPlayerCostumes")

local playerManager = require("playerManager")
local textplus = require("textplus")
local inspect = require("ext/inspect")
local socket

local battleGeneral,battlePlayer,battleOptions
local onlinePlayNPC,onlinePlayPlayers
local onlineChat,onlineLogs
local randomPowerups

local onlinePlay = {}


onlinePlay.MODE_OFFLINE = 0
onlinePlay.MODE_HOST    = 1
onlinePlay.MODE_CLIENT  = 2

onlinePlay.IMPORTANCE_MINOR = 0 -- use UDP; data may not arrive in order or arrive at all
onlinePlay.IMPORTANCE_MAJOR = 1 -- use TCP; data may take longer to arrive, but will arrive in order and is sort of guaranteed to not fail
onlinePlay.IMPORTANCE_VITAL = 2 -- use TCP; pause the game until we know it has arrived



onlinePlay.unresponsiveTime = 3 -- how long it takes without responses for someone to be considered "unresponsive" (seconds)
onlinePlay.pingFrequency = 2 -- how often pings are updated (seconds)


onlinePlay.disconnectUnresponsiveClients = false

onlinePlay.dontAllowNewClients = false -- if true, the host will not allow anybody to connect

onlinePlay.debugMode = false
onlinePlay.debugArtificialDelayMin = 0
onlinePlay.debugArtificialDelayMax = 0


onlinePlay.portNumber = 24839


onlinePlay.currentMode = onlinePlay.MODE_OFFLINE

onlinePlay.socketLoaded = false

onlinePlay.playerIdx = 0
onlinePlay.hostPlayerIdx = 0

onlinePlay.localTime = 0
onlinePlay.syncedTime = 0
onlinePlay.lastResponseTime = 0


onlinePlay.isReconnecting = false -- is in reconnect process
onlinePlay.hasReconnected = false -- host has responded to reconnect request
onlinePlay.tcpEnabled = false


GameData.onlinePlay = GameData.onlinePlay or {}
local gameData = GameData.onlinePlay

gameData.mode = gameData.mode or onlinePlay.MODE_OFFLINE
gameData.userData = gameData.userData or {}
gameData.messageHistory = gameData.messageHistory or {}
gameData.reconnectNecessary = gameData.reconnectNecessary or false
gameData.reconnectingMap = gameData.reconnectingMap or {}
gameData.playerIdx = gameData.playerIdx or 1
--gameData.hostAddress
--gameData.logFilename


-- You can overwrite this function to return a version number that is distinct to each version of your episode.
-- This is used to prevent players from joining someone with a different version of the game.
function onlinePlay.getVersionNumber()
    return 0
end


local joinSound = Misc.resolveSoundFile("resources/join")


local NEXT_LEVEL_FILENAME_ADDR = 0x00B25720


local udpObject
local selfServerTCPObject
local selfClientTCPObject
local userTCPObjects = {}

local hostReconnectAddress



local reconnectRequestTime
local generalUpdateTime
local playerUpdateTime
local layersUpdateTime


local function getInstalledCostumes()
    local installedCostumes = {}

    for character = 1,5 do
        installedCostumes[character] = playerManager.getCostumes(character)
    end

    return installedCostumes
end

local function checkSaveID()
    if battleGeneral.saveData.saveID == nil then
        battleGeneral.saveData.saveID = battleGeneral.generateRandomID()
        Misc.saveGame()
    end
end


-- Core sending/receiving code
local receiveAndHandleData
local sendJoinRequest
local sendReconnectRequest
local handleData

local acceptTCPClients
local receiveTCPData

do
    local commandMap = {}


    local commandMT = {}

    commandMT.__type = "OnlineCommand"
    commandMT.__index = commandMT


    local validImportanceValues = table.map{onlinePlay.IMPORTANCE_MINOR,onlinePlay.IMPORTANCE_MAJOR,onlinePlay.IMPORTANCE_VITAL}
    local udpImportanceValues = table.map{onlinePlay.IMPORTANCE_MINOR}


    local function calculateChecksum(data,start)
        -- Add up each byte of the data, wrapping around at 256
        local pos = start
        local value = 0

        while (data[pos] ~= nil) do
            value = (value + string.byte(data[pos])) % 256
            pos = pos + 1
        end

        return value
    end

    local function checksumIsValid(data)
        if data[2] == nil then
            return false
        end

        local calculatedCheckSum = calculateChecksum(data,2)
        local expectedCheckSum = string.byte(data[1])

        return (calculatedCheckSum == expectedCheckSum)
    end


    local function getTargetUsers(targetPlayerIdx)
        if targetPlayerIdx > 0 then
            return {onlinePlay.getUserByPlayer(targetPlayerIdx)}
        else
            local list = {}

            for _,user in ipairs(onlinePlay.getUsers()) do
                if user.playerIdx ~= onlinePlay.playerIdx then
                    table.insert(list,user)
                end
            end

            return list
        end
    end

    local function sendTCPData(tcp,data,waitForResponse)
        if waitForResponse then
            tcp:settimeout(2)
            tcp:send(data)
            tcp:settimeout(0)
        else
            tcp:send(data)
        end
    end


    local function sendInternal(self,sourcePlayerIdx,targetPlayerIdx,importance,...)
        -- Sanity checks
        if not validImportanceValues[importance] then
            error("Importance value is not valid",3)
        end

        if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE then
            error("Cannot send a command while unconnected",3)
        end

        if targetPlayerIdx == onlinePlay.playerIdx then
            error("Cannot send a message to self",3)
        end

        -- Construct data to send
        local data = onlinePlay.encodeValue("string",self.name).. onlinePlay.encodeValue("uint8",importance)
        local targetUsers = getTargetUsers(targetPlayerIdx)

        -- Add parameters
        local t = table.pack(...)

        for i = 1,t.n do
            data = data.. onlinePlay.encodeValue("any",t[i])
        end

        -- Add checksum
        data = string.char(calculateChecksum(data,1)).. data

        -- Send it off to whoever's relevant
        if udpImportanceValues[importance] then
            for _,user in ipairs(targetUsers) do
                udpObject:sendto(data,user.ip,user.udpPort)
            end
        else
            if not onlinePlay.tcpEnabled then
                return
            end

            -- Add length to the start (only necessary for TCP, as it's a constant stream of bytes rather than separate datagrams)
            data = onlinePlay.encodeValue("uint16",#data).. data

            for _,user in ipairs(targetUsers) do
                if userTCPObjects[user.playerIdx] ~= nil then
                    sendTCPData(userTCPObjects[user.playerIdx],data,importance == onlinePlay.IMPORTANCE_VITAL)
                end
            end
        end
    end


    function commandMT:sendWithImportance(targetClientIdx,importance,...)
        if not validImportanceValues[importance] then
            error("Importance value is not valid",2)
        end

        sendInternal(self,onlinePlay.playerIdx,targetClientIdx,importance,...)
    end

    function commandMT:send(targetClientIdx,...)
        if self.defaultImportance == nil then
            error("Default importance value is not set",2)
        end

        sendInternal(self,onlinePlay.playerIdx,targetClientIdx,self.defaultImportance,...)
    end


    function onlinePlay.createCommand(name,defaultImportance)
        if commandMap[name] ~= nil then
            error("Command name '".. name.. "' is already in use",2)
        end

        if defaultImportance ~= nil and not validImportanceValues[defaultImportance] then
            error("Importance value is not valid",2)
        end

        if type(name) ~= "string" then
            error("Command name is not valid",2)
        end


        local command = setmetatable({},commandMT)

        command.name = name
        command.defaultImportance = defaultImportance

        commandMap[name] = command

        return command
    end

    function onlinePlay.getCommand(name)
        return commandMap[name]
    end


    function handleData(data,sourceUser,ip,port)
        if not checksumIsValid(data) then
            return
        end

        -- Parse basic properties
        local name,importance
        local pos = 2

        name,pos = onlinePlay.decodeValue("string",data,pos)
        importance,pos = onlinePlay.decodeValue("uint8",data,pos)


        -- Parse parameters
        local paramsList = {}
        local paramsCount = 0

        while (data[pos] ~= nil) do
            paramsCount = paramsCount + 1
            paramsList[paramsCount],pos = onlinePlay.decodeValue("any",data,pos)
        end

        -- Do what we need to with the command
        local command = commandMap[name]
        if command == nil then
            return
        end


        onlinePlay.lastResponseTime = onlinePlay.localTime
        sourceUser.lastResponseTime = onlinePlay.localTime

        -- Run receive code
        if command.onReceive ~= nil then
            command.onReceive(sourceUser.playerIdx,table.unpack(paramsList,1,paramsCount))
        end
    end

    local function delayedHandleDataRoutine(data,sourceUser,ip,port)
        Routine.wait(RNG.random(onlinePlay.debugArtificialDelayMin,onlinePlay.debugArtificialDelayMax),true)
        
        if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
            handleData(data,sourceUser,ip,port)
        end
    end

    local function handleDataWithDelay(data,sourceUser,ip,port)
        if onlinePlay.debugMode and onlinePlay.debugArtificialDelayMax > 0 then
            Routine.run(delayedHandleDataRoutine, data,sourceUser,ip,port)
        else
            handleData(data,sourceUser,ip,port)
        end
    end


    -- Receiving data code
    local function receiveAndHandleUDP()
        while (onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE) do
            local data,ip,port = udpObject:receivefrom()
            local err = ip -- IP is an error if data is nil
    
            if data ~= nil then
                local sourceUser = onlinePlay.getUserByAddress(ip,tostring(port))

                if sourceUser ~= nil then
                    handleDataWithDelay(data,sourceUser,ip,port)
                end
            elseif err ~= "timeout" then
                local text = "Network error (UDP reception): ".. err
    
                onlineLogs.add("<ERROR> ".. text)
                error(text)
            else
                break
            end
        end
    end


    function receiveTCPData(tcp,isLong)
        local lengthData,err,part = tcp:receive((isLong and 4) or 2)

        if lengthData == nil then
            return nil,err,part
        end

        if #lengthData < 2 then
            return nil
        end

        local length = onlinePlay.decodeValue((isLong and "uint32") or "uint16",lengthData,1)
        if length <= 0 then
            return
        end

        local data,err,part = tcp:receive(length)

        if data == nil then
            return nil,err,part
        end

        if #data < length then
            return nil
        end

        return data,err,part
    end

    local function receiveAndHandleTCP()
        --[[if onlinePlay.currentMode == onlinePlay.MODE_CLIENT then
            while (onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE) do
                local data,err,part = receiveTCPData(tcpObject)

                if data == nil then
                    if err == "closed" then
                        onlinePlay.disconnect()
                    elseif err ~= "timeout" and err ~= nil then
                        local text = "Network error (TCP reception): ".. err
    
                        onlineLogs.add("<ERROR> ".. text)
                        error(text)
                    end

                    break
                end

                handleDataWithDelay(data)
            end

            return
        end]]


        for _,user in ipairs(onlinePlay.getUsers()) do
            -- Receive from our own client/server connections
            while (onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE and userTCPObjects[user.playerIdx] ~= nil) do
                local data,err,part = receiveTCPData(userTCPObjects[user.playerIdx])

                if data == nil then
                    if err == "closed" then
                        if user.isHost then
                            onlinePlay.disconnect()
                        elseif onlinePlay.currentMode == onlinePlay.MODE_HOST then
                            user:disconnect()
                        end
                    elseif err ~= "timeout" and err ~= nil then
                        local text = "Network error (TCP reception): ".. err
    
                        onlineLogs.add("<ERROR> ".. text)
                        error(text)
                    end

                    break
                end

                handleDataWithDelay(data,user)
            end
        end
    end


    function receiveAndHandleData()
        if onlinePlay.tcpEnabled then
            acceptTCPClients()
            receiveAndHandleTCP()
        end

        receiveAndHandleUDP()
    end
end


-- Join packets - data sent when a new player connects
do
    local joinPacketFunctions = {}
    local joinPacketMT = {__index = joinPacketFunctions}

    local joinPacketList = {}
    local joinPacketMap = {}


    local function sortJoinPackets(a,b)
        if a.order ~= b.order then
            return (a.order < b.order)
        else
            return (a.index < b.index)
        end
    end

    function joinPacketFunctions:encode(targetPlayerIdx)
        if self.encodeData == nil then
            return nil
        end

        local rawData = table.pack(self.encodeData(targetPlayerIdx))

        local encodedData = onlinePlay.encodeValue("string",self.name).. onlinePlay.encodeValue("uint8",rawData.n)

        for valueIndex = 1,rawData.n do
            encodedData = encodedData.. onlinePlay.encodeValue("any",rawData[valueIndex])
        end

        return encodedData
    end


    function onlinePlay.encodeAllJoinPackets(targetPlayerIdx)
        local packetCount = 0
        local output = ""

        table.sort(joinPacketList,sortJoinPackets)

        for _,joinPacket in ipairs(joinPacketList) do
            local encodedData = joinPacket:encode(targetPlayerIdx)

            if encodedData ~= nil then
                output = output.. encodedData
                packetCount = packetCount + 1
            end
        end

        output = onlinePlay.encodeValue("uint8",packetCount).. output

        return output
    end

    function onlinePlay.decodeJoinPackets(data,pos)
        pos = pos or 1

        local packetCount
        
        packetCount,pos = onlinePlay.decodeValue("uint8",data,pos)

        for packetIndex = 1,packetCount do
            local name,valueCount

            name,pos = onlinePlay.decodeValue("string",data,pos)
            valueCount,pos = onlinePlay.decodeValue("uint8",data,pos)

            local joinPacket = joinPacketMap[name]

            if joinPacket == nil then
                error("Unrecognised join packet name: '".. name.. "'")
            end

            local decodedData = {}

            for valueIndex = 1,valueCount do
                local value

                decodedData[valueIndex],pos = onlinePlay.decodeValue("any",data,pos)
                valueIndex = valueIndex + 1
            end

            if joinPacket.onReceive ~= nil then
                joinPacket.onReceive(table.unpack(decodedData))
            end
        end

        return pos
    end


    function onlinePlay.createJoinPacket(name,order)
        if joinPacketMap[name] ~= nil then
            error("Join packet name '".. name.. "' is already in use",2)
        end

        if type(name) ~= "string" then
            error("Join packet name is not valid",2)
        end

        local joinPacket = setmetatable({},joinPacketMT)

        joinPacket.name = name
        joinPacket.order = order or 0

        table.insert(joinPacketList,joinPacket)
        joinPacketMap[name] = joinPacket

        joinPacket.index = #joinPacketList

        return joinPacket
    end

    function onlinePlay.getJoinPacket(name)
        return joinPacketMap[name]
    end
end


-- User handling
local userList = {}
local userMapByPlayer = {}
local userMapByAddress = {}

local createUserObject
local connectToFellowClient

do
    local userMT = {}

    userMT.__type = "OnlineUser"
    userMT.__index = userMT


    -- Commands
    local disconnectCommand = onlinePlay.createCommand("_userDisconnect",onlinePlay.IMPORTANCE_MAJOR)
    local connectCommand = onlinePlay.createCommand("_userConnect",onlinePlay.IMPORTANCE_MAJOR)


    local function disconnectInternal(user)
        if user.playerIdx <= 0 then
            return
        end

        if user.playerIdx == onlinePlay.playerIdx then
            onlinePlay.disconnect()
            return
        end

        local idx = table.ifind(userList,user)
        if idx == nil then
            return
        end

        if user.ip ~= nil then
            userMapByAddress[user.ip][user.udpPort] = nil
        end

        userMapByPlayer[user.playerIdx] = nil
        table.remove(userList,idx)

        onlinePlay.onDisconnect(user.playerIdx)

        gameData.userData[user.playerIdx] = nil
        user.playerIdx = 0
    end


    function connectCommand.onReceive(sourcePlayerIdx, playerIdx,username,colorIdx,installedCostumes,saveID, ip,udpPort, newPlayerCount)
        if sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
            return
        end

        createUserObject(playerIdx,ip,udpPort)

        gameData.userData[playerIdx] = {
            username = username,
            colorIdx = colorIdx,
            installedCostumes = installedCostumes,
            saveID = saveID,
            ip = ip,
            udpPort = udpPort,
        }
        battleGeneral.gameData.playerCount = newPlayerCount

        if playerIdx < onlinePlay.playerIdx then
            local successful,err = connectToFellowClient(playerIdx)

            if not successful then
                onlineLogs.add("<ERROR> ".. err)
                error(err)
            end
        end

        battleCamera.initCameras()

        onlinePlay.onConnect(playerIdx)
        SFX.play(joinSound)
    end

    function disconnectCommand.onReceive(sourcePlayerIdx, playerIdx)
        if sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
            return
        end

        local user = onlinePlay.getUserByPlayer(playerIdx)
        if user == nil then
            return
        end

        disconnectInternal(user)
    end


    function userMT:disconnect()
        if onlinePlay.currentMode ~= onlinePlay.MODE_HOST then
            error("Can only disconnect clients as a host",2)
        end

        if self.playerIdx == onlinePlay.playerIdx then
            error("Cannot disconnect self",2)
        end

        local idx = table.ifind(userList,self)
        if idx == nil then
            return
        end

        -- Do regular disconnect stuff
        disconnectCommand:send(0, self.playerIdx)
        disconnectInternal(self)

        -- Close TCP connection
        local userTCPObject = userTCPObjects[self.playerIdx]

        if userTCPObject ~= nil then
            userTCPObject:close()
            userTCPObjects[self.playerIdx] = nil
        end
    end

    
    function createUserObject(playerIdx,ip,udpPort)
        local user = setmetatable({},userMT)
    
        user.playerIdx = playerIdx
    
        user.lastResponseTime = onlinePlay.localTime
    
        user.pingedTime = nil
        user.pingCounter = 0
        user.pingValue = 0

        user.isHost = (playerIdx == onlinePlay.hostPlayerIdx)
        user.isUnresponsive = false


        if ip ~= nil then
            user.ip = ip
            user.udpPort = tostring(udpPort)

            userMapByAddress[user.ip] = userMapByAddress[user.ip] or {}
            userMapByAddress[user.ip][user.udpPort] = user
        end


        userMapByPlayer[playerIdx] = user

    
        table.insert(userList,user)
    end


    -- Functions
    function onlinePlay.getUsers()
        return userList
    end

    function onlinePlay.getUserByPlayer(playerIdx)
        return userMapByPlayer[playerIdx]
    end

    function onlinePlay.getUserByAddress(ip,udpPort)
        local map = userMapByAddress[ip]
        if map == nil then
            return nil
        end

        return map[udpPort]
    end

    function onlinePlay.getUserCount()
        return #userList
    end


    function onlinePlay.getUserData(index)
        return gameData.userData[index]
    end
    
    function onlinePlay.isConnected(index)
        return (gameData.userData[index] ~= nil)
    end


    -- Initial joining
    local joinHeader = "SMBX2 onlinePlay"


    function connectToFellowClient(playerIndex)
        local user = onlinePlay.getUserByPlayer(playerIndex)
        local username = onlinePlay.getUserData(playerIndex).username

        local newClientTCP,err = socket.tcp()
        if err ~= nil then
            return false,textFiles.funcs.replace(textFiles.onlineErrors.fellowClientCreateError,{ERROR = err,NAME = username})
        end

        newClientTCP:settimeout(5)

        local _,err = newClientTCP:connect(user.ip,onlinePlay.portNumber + playerIndex - 1)
        if err ~= nil then
            newClientTCP:close()
            return false,textFiles.funcs.replace(textFiles.onlineErrors.fellowClientConnectError,{ERROR = err,NAME = username})
        end

        -- Send a message to the other user
        local s = joinHeader

        s = s.. onlinePlay.encodeValue("uint8",onlinePlay.playerIdx)
        s = onlinePlay.encodeValue("uint16",#s).. s

        -- Send data
        local bytesSent,err = newClientTCP:send(s)

        if err ~= nil then
            newClientTCP:close()
            return false,textFiles.funcs.replace(textFiles.onlineErrors.fellowClientSendError,{ERROR = err,NAME = username})
        end

        userTCPObjects[playerIndex] = newClientTCP
        newClientTCP:settimeout(0)

        return true
    end


    function sendJoinRequest(hostAddress,username,colorIdx)
        -- Get UDP IP/port
        local ip,port = udpObject:getsockname()

        if ip == nil then
            return false,textFiles.onlineErrors.noConnect
        end

        -- Set timeout for this
        selfClientTCPObject:settimeout(8)

        -- Encode various data
        local s = joinHeader

        local costumeList = perPlayerCostumes.getAllCostumes(1)
        local installedCostumes = getInstalledCostumes()

        s = s.. onlinePlay.encodeValue("boolean",false)
        s = s.. onlinePlay.encodeValue("uint16",onlinePlay.getVersionNumber())
        s = s.. onlinePlay.encodeValue("string",Misc.episodeName())
        s = s.. onlinePlay.encodeValue("string",Level.filename())
        s = s.. onlinePlay.encodeValue("string",username)
        s = s.. onlinePlay.encodeValue("uint8",colorIdx)
        s = s.. onlinePlay.encodeValue("uint8",player.character)
        s = s.. onlinePlay.encodeValue("table",costumeList)
        s = s.. onlinePlay.encodeValue("table",installedCostumes)
        s = s.. onlinePlay.encodeValue("string",battleGeneral.saveData.saveID)
        s = s.. onlinePlay.encodeValue("string",port)
        s = onlinePlay.encodeValue("uint16",#s).. s

        -- Send data
        local bytesSent,err = selfClientTCPObject:send(s)

        -- Was there an error?
        if err == "timeout" or err == "closed" then
            return false,textFiles.onlineErrors.noConnect
        elseif err ~= nil then
            return false,textFiles.funcs.replace(textFiles.onlineErrors.connectError,{ERROR = err})
        end

        -- The message was sent properly! Get a reponse.
        local data,err,part = receiveTCPData(selfClientTCPObject,true)

        if data == nil then
            if err == "timeout" or err == "closed" then
                return false,textFiles.onlineErrors.noConnect
            elseif err == nil then
                return false,textFiles.onlineErrors.malformed
            else
                return false,textFiles.funcs.replace(textFiles.onlineErrors.connectError,{ERROR = err})
            end
        end

        -- Find the type of response we got
        local errorReason = data:match("^error (.+)$")
        if errorReason ~= nil then
            return false,textFiles.onlineErrors[errorReason] or textFiles.onlineErrors.unknown
        end

        local _,paramsStart = data:find("^connect ")
        if paramsStart == nil then
            return false,textFiles.onlineErrors.malformed
        end

        -- Decode data
        local pos = paramsStart + 1

        local playerCount

        onlinePlay.hostPlayerIdx,pos = onlinePlay.decodeValue("uint8",data,pos)
        onlinePlay.playerIdx,pos     = onlinePlay.decodeValue("uint8",data,pos)
        playerCount,pos              = onlinePlay.decodeValue("uint8",data,pos)
        onlinePlay.syncedTime,pos    = onlinePlay.decodeValue("number",data,pos)

        userTCPObjects[onlinePlay.hostPlayerIdx] = selfClientTCPObject

        battleGeneral.gameData.playerCount = playerCount

        gameData.hostPlayerIdx = onlinePlay.hostPlayerIdx
        gameData.playerIdx = onlinePlay.playerIdx

        battleGeneral.gameData.playerCharacters[onlinePlay.playerIdx] = player.character
        battleGeneral.gameData.playerCostumes[onlinePlay.playerIdx] = costumeList

        Player(onlinePlay.playerIdx).character = player.character
        perPlayerCostumes.setAllCostumes(onlinePlay.playerIdx,costumeList)

        for playerIndex = 1,playerCount do
            local isConnected

            isConnected,pos = onlinePlay.decodeValue("boolean",data,pos)

            if isConnected then
                local username,colorIdx,installedCostumes
                local userIP,userUDPPort
                local saveID

                username,pos = onlinePlay.decodeValue("string",data,pos)
                colorIdx,pos = onlinePlay.decodeValue("uint8",data,pos)
                installedCostumes,pos = onlinePlay.decodeValue("table",data,pos)

                if playerIndex ~= onlinePlay.playerIdx then
                    if playerIndex == onlinePlay.hostPlayerIdx then
                        userIP = hostAddress
                    else
                        userIP,pos = onlinePlay.decodeValue("string",data,pos)
                    end

                    userUDPPort,pos = onlinePlay.decodeValue("string",data,pos)
                    saveID,pos = onlinePlay.decodeValue("string",data,pos)
                else
                    saveID = battleGeneral.saveData.saveID
                end

                gameData.userData[playerIndex] = {
                    username = username,
                    colorIdx = colorIdx,
                    installedCostumes = installedCostumes,
                    saveID = saveID,
                    ip = userIP,
                    udpPort = userUDPPort,
                }

                createUserObject(playerIndex,userIP,userUDPPort)

                if playerIndex ~= onlinePlay.hostPlayerIdx and playerIndex < onlinePlay.playerIdx then
                    local successful,err = connectToFellowClient(playerIndex)

                    if not successful then
                        return false,err
                    end
                end
            end
        end

        local joinPacketData = data:sub(pos)

        battleCamera.initCameras()

        return true,joinPacketData
    end

    function sendReconnectRequest()
        -- Set timeout for this
        selfClientTCPObject:settimeout(8)

        -- Encode various data
        local s = joinHeader

        s = s.. onlinePlay.encodeValue("boolean",true)
        s = s.. onlinePlay.encodeValue("uint8",onlinePlay.playerIdx)
        s = onlinePlay.encodeValue("uint16",#s).. s

        -- Send data
        local bytesSent,err = selfClientTCPObject:send(s)

        if err ~= nil then
            return false
        end

        -- The message was sent properly! Get a reponse.
        local data,err,part = receiveTCPData(selfClientTCPObject,false)

        if data == nil then
            return false
        end

        -- Handle the response
        if data ~= "connect" then
            return false
        end

        userTCPObjects[onlinePlay.hostPlayerIdx] = selfClientTCPObject

        return true
    end


    local function findAvailablePlayerSlot()
        -- Look for slots of any disonnected players
        local playerCount = battleGeneral.gameData.playerCount

        for i = 1,playerCount do
            if not onlinePlay.isConnected(i) then
                return i
            end
        end

        -- New player slot!
        return playerCount + 1
    end

    local function usernameInUse(username)
        if username == "" then
            return false
        end

        for playerIdx = 1,battleGeneral.gameData.playerCount do
            local userData = onlinePlay.getUserData(playerIdx)

            if userData ~= nil and userData.username == username then
                return true
            end
        end

        return false
    end

    local function colorIsInUse(colorIdx)
        for playerIdx = 1,battleGeneral.gameData.playerCount do
            local userData = onlinePlay.getUserData(playerIdx)

            if userData ~= nil and userData.colorIdx == colorIdx then
                return true
            end
        end

        return false
    end

    local function pickRandomUnusedColor()
        -- Create a list of all the unused colors
        local unusedColors = {}

        for colorIdx = 1,#battleGeneral.playerColors do
            if not colorIsInUse(colorIdx) then
                table.insert(unusedColors,colorIdx)
            end
        end

        -- Pick a random one (or nil, if there are no unused colors)
        return RNG.irandomEntry(unusedColors)
    end


    local function sendErrorToClient(clientTCPObject,reason)
        local s = "error ".. reason
        s = onlinePlay.encodeValue("uint32",#s).. s

        clientTCPObject:settimeout(2)
        clientTCPObject:send(s)
        clientTCPObject:close()
    end

    local function handleNewTCPClient(clientTCPObject)
        clientTCPObject:settimeout(4)
        selfServerTCPObject:settimeout(4)

        -- Find IP/port
        local ip,port = clientTCPObject:getpeername()
        if ip == nil then
            clientTCPObject:close()
            return
        end

        -- Wait for data from them
        local data,err,part = receiveTCPData(clientTCPObject)

        if data == nil then
            clientTCPObject:close()
            return
        end

        -- Decode sent data
        local pos = 1

        if data:sub(pos,pos + #joinHeader - 1) ~= joinHeader then
            clientTCPObject:close()
            return
        end

        pos = pos + #joinHeader

        -- Client-to-client connections
        if onlinePlay.currentMode == onlinePlay.MODE_CLIENT then
            local playerIdx
            playerIdx,pos = onlinePlay.decodeValue("uint8",data,pos)

            userTCPObjects[playerIdx] = clientTCPObject

            clientTCPObject:settimeout(0)
            selfServerTCPObject:settimeout(0)

            return
        end


        -- Handle reconnecting
        local isReconnecting
        isReconnecting,pos = onlinePlay.decodeValue("boolean",data,pos)

        if isReconnecting then
            -- Find who sent it
            local playerIdx
            playerIdx,pos = onlinePlay.decodeValue("uint8",data,pos)

            local sourceUser = onlinePlay.getUserByPlayer(playerIdx)

            if sourceUser == nil then
                local s = "error"
                s = onlinePlay.encodeValue("uint16",#s).. s
                clientTCPObject:send(s)
                return
            end

            -- Assign the TCP object to the client
            userTCPObjects[sourceUser.playerIdx] = clientTCPObject
            onlinePlay.lastResponseTime = onlinePlay.localTime

            -- Send a reponse
            local s = "connect"
            s = onlinePlay.encodeValue("uint16",#s).. s
            clientTCPObject:send(s)

            clientTCPObject:settimeout(0)
            selfServerTCPObject:settimeout(0)

            return
        end

        -- If we are currently reconnecting, don't allow a new client!
        if onlinePlay.isReconnecting then
            sendErrorToClient(clientTCPObject,"disallowed")
            return
        end

        -- Find player slot
        local newIndex = findAvailablePlayerSlot()
        
        if newIndex > battleGeneral.maxOnlinePlayers then
            sendErrorToClient(clientTCPObject,"maxPlayers")
            return
        end

        -- Parse version ID
        local versionNumber
        versionNumber,pos = onlinePlay.decodeValue("uint16",data,pos)

        if versionNumber ~= onlinePlay.getVersionNumber() then
            sendErrorToClient(clientTCPObject,"differentVersion")
            return
        end

        -- Parse episode name
        local episodeName
        episodeName,pos = onlinePlay.decodeValue("string",data,pos)

        if episodeName ~= Misc.episodeName() then
            sendErrorToClient(clientTCPObject,"differentEpisode")
            return
        end

        -- Parse level filename
        levelFilename,pos = onlinePlay.decodeValue("string",data,pos)

        if levelFilename ~= Level.filename() then
            sendErrorToClient(clientTCPObject,"differentLevel")
            return
        end

        -- Parse username
        local username
        username,pos = onlinePlay.decodeValue("string",data,pos)

        if username:find("^%s+$") then
            sendErrorToClient(clientTCPObject,"invalidName")
            return
        end

        if usernameInUse(username) then
            sendErrorToClient(clientTCPObject,"nameInUse")
            return
        end

        -- Parse colour
        local colorIdx
        colorIdx,pos = onlinePlay.decodeValue("uint8",data,pos)

        if battleGeneral.playerColors[colorIdx] == nil then
            sendErrorToClient(clientTCPObject,"invalidColor")
            return
        end

        if colorIsInUse(colorIdx) then
            colorIdx = pickRandomUnusedColor() or colorIdx
        end

        -- Don't allow new users under certain conditions
        if onlinePlay.dontAllowNewClients then
            sendErrorToClient(clientTCPObject,"disallowed")
            return
        end

        -- Parse character and costume
        local character,costumeList
        character,pos = onlinePlay.decodeValue("uint8",data,pos)
        costumeList,pos = onlinePlay.decodeValue("table",data,pos)

        if character < 1 or character > 5 then
            sendErrorToClient(clientTCPObject,"invalidCharacter")
            return
        end

        battleGeneral.gameData.playerCharacters[newIndex] = character
        battleGeneral.gameData.playerCostumes[newIndex] = costumeList

        Player(newIndex).character = character
        perPlayerCostumes.setAllCostumes(newIndex,costumeList)
        
        -- Parse their list of installed costumes
        local installedCostumes
        installedCostumes,pos = onlinePlay.decodeValue("table",data,pos)

        -- Parse their save ID
        local saveID
        saveID,pos = onlinePlay.decodeValue("string",data,pos)

        -- Parse port number
        local udpPort
        udpPort,pos = onlinePlay.decodeValue("string",data,pos)


        -- Set up new player
        local newPlayerCount = math.max(newIndex,battleGeneral.gameData.playerCount)

        battleGeneral.gameData.playerCount = newPlayerCount

        gameData.userData[newIndex] = {
            username = username,
            colorIdx = colorIdx,
            installedCostumes = installedCostumes,
            saveID = saveID,
            ip = ip,
            udpPort = udpPort,
        }

        userTCPObjects[newIndex] = clientTCPObject
        createUserObject(newIndex,ip,udpPort)

        -- We've accepted them; send stuff back
        local s = "connect "

        s = s.. onlinePlay.encodeValue("uint8",onlinePlay.hostPlayerIdx)
        s = s.. onlinePlay.encodeValue("uint8",newIndex)
        s = s.. onlinePlay.encodeValue("uint8",newPlayerCount)
        s = s.. onlinePlay.encodeValue("number",onlinePlay.syncedTime)

        for playerIndex = 1,newPlayerCount do
            local userData = onlinePlay.getUserData(playerIndex)

            s = s.. onlinePlay.encodeValue("boolean",userData ~= nil)

            if userData ~= nil then
                s = s.. onlinePlay.encodeValue("string",userData.username)
                s = s.. onlinePlay.encodeValue("uint8",userData.colorIdx)
                s = s.. onlinePlay.encodeValue("table",userData.installedCostumes)

                if playerIndex ~= newIndex then
                    local user = onlinePlay.getUserByPlayer(playerIndex)

                    if user.isHost then
                        s = s.. onlinePlay.encodeValue("string",gameData.udpPort)
                    else
                        s = s.. onlinePlay.encodeValue("string",user.ip)
                        s = s.. onlinePlay.encodeValue("string",user.udpPort)
                    end

                    s = s.. onlinePlay.encodeValue("string",userData.saveID)
                end
            end
        end

        s = s.. onlinePlay.encodeAllJoinPackets(newIndex)

        s = onlinePlay.encodeValue("uint32",#s).. s

        clientTCPObject:settimeout(0)
        selfServerTCPObject:settimeout(0)
        clientTCPObject:send(s)

        -- Connect them
        onlinePlay.lastResponseTime = onlinePlay.localTime

        for _,user in ipairs(onlinePlay.getUsers()) do
            if user.playerIdx ~= onlinePlay.playerIdx and user.playerIdx ~= newIndex then
                connectCommand:send(user.playerIdx, newIndex,username,colorIdx,installedCostumes,saveID, ip,udpPort, newPlayerCount)
            end
        end

        --onlinePlayPlayers.sendAllPlayerUpdates(newIndex)

        battleCamera.initCameras()

        onlinePlay.onConnect(newIndex)
        SFX.play(joinSound)
    end


    function acceptTCPClients()
        while (true) do
            local clientTCPObject,err = selfServerTCPObject:accept()

            if clientTCPObject ~= nil then
                -- Register this client TCP object
                handleNewTCPClient(clientTCPObject)
                selfServerTCPObject:settimeout(0)
            elseif err ~= "timeout" then
                local text = "Network error (TCP acception): ".. err
    
                onlineLogs.add("<ERROR> ".. text)
                error(text)
            else
                break
            end
        end

        selfServerTCPObject:settimeout(0)
    end
end



local function bindTCP(host, port, backlog) -- taken from socket.lua
    if host == "*" then host = "0.0.0.0" end
    local addrinfo, err = socket.dns.getaddrinfo(host);
    if not addrinfo then return nil, err end
    local sock, res
    err = "no info on address"
    for i, alt in ipairs(addrinfo) do
        if alt.family == "inet" then
            sock, err = socket.tcp()
        else
            sock, err = socket.tcp6()
        end
        if not sock then return nil, err end
        sock:setoption("reuseaddr", true)
        res, err = sock:bind(alt.addr, port)
        if not res then 
            sock:close()
        else 
            res, err = sock:listen(backlog)
            if not res then 
                sock:close()
            else
                return sock
            end
        end 
    end
    return nil, err
end


local function connect()
    onlinePlay.localTime = 0
    onlinePlay.lastResponseTime = onlinePlay.localTime

    gameData.logFilename = onlineLogs.open(gameData.logFilename)
end

local function closeConnectionObjects()
    if udpObject ~= nil then
        udpObject:close()
        udpObject = nil
    end

    if selfServerTCPObject ~= nil then
        selfServerTCPObject:close()
        selfServerTCPObject = nil
    end

    if selfClientTCPObject ~= nil then
        selfClientTCPObject:close()
        selfClientTCPObject = nil

        userTCPObjects[onlinePlay.hostPlayerIdx] = nil
    end

    for playerIndex = 1,battleGeneral.maxOnlinePlayers do
        if userTCPObjects[playerIndex] ~= nil then
            userTCPObjects[playerIndex]:close()
            userTCPObjects[playerIndex] = nil
        end
    end
end


function onlinePlay.host(username,colorIdx)
    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        return
    elseif socket == nil then
        return textFiles.onlineErrors.socket
    end

    checkSaveID()

    -- Set up stuff
    local newUDP,err = socket.udp()
    if err ~= nil then
        closeConnectionObjects()
        return textFiles.funcs.replace(textFiles.onlineErrors.udp,{ERROR = err})
    end

    udpObject = newUDP
    udpObject:settimeout(0)

    local newTCP,err = bindTCP("*",onlinePlay.portNumber)
    if err ~= nil then
        closeConnectionObjects()
        return textFiles.funcs.replace(textFiles.onlineErrors.tcp,{ERROR = err})
    end

    selfServerTCPObject = newTCP
    selfServerTCPObject:settimeout(0)

    local success,err = newUDP:setsockname("*",gameData.udpPort or 0)
    if err ~= nil then
        closeConnectionObjects()
        return textFiles.funcs.replace(textFiles.onlineErrors.sockName,{ERROR = err})
    end


    -- Start hosting
    onlinePlay.currentMode = onlinePlay.MODE_HOST


    if username ~= nil then
        onlinePlay.playerIdx = 1
        onlinePlay.hostPlayerIdx = onlinePlay.playerIdx

        gameData.userData[onlinePlay.playerIdx] = {
            username = username,
            colorIdx = colorIdx,
            installedCostumes = getInstalledCostumes(),
            saveID = battleGeneral.saveData.saveID,
        }

        _,gameData.udpPort = udpObject:getsockname()

        gameData.playerIdx = onlinePlay.playerIdx
        battleGeneral.gameData.playerCount = 1

        createUserObject(onlinePlay.playerIdx)
    else -- reconnecting
        onlinePlay.playerIdx = gameData.playerIdx
        onlinePlay.hostPlayerIdx = onlinePlay.playerIdx
        onlinePlay.isReconnecting = gameData.reconnectNecessary

        createUserObject(onlinePlay.playerIdx)

        for playerIndex = 1,battleGeneral.gameData.playerCount do
            local userData = onlinePlay.getUserData(playerIndex)

            if playerIndex ~= onlinePlay.playerIdx and userData ~= nil then
                createUserObject(playerIndex,userData.ip,userData.udpPort)
            end
        end
    end

    onlinePlay.syncedTime = 0

    onlinePlay.tcpEnabled = true

    connect()

    onlinePlay.onInitialise()
    battleCamera.initCameras()

    if username ~= nil then
        onlinePlay.onConnect(onlinePlay.playerIdx)
        SFX.play(joinSound)
    end
end

function onlinePlay.joinHost(hostAddress,username,colorIdx)
    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        return
    elseif socket == nil then
        return textFiles.onlineErrors.socket
    end

    checkSaveID()

    -- Set up stuff
    local newUDP,err = socket.udp()
    if err ~= nil then
        closeConnectionObjects()
        return textFiles.funcs.replace(textFiles.onlineErrors.udp,{ERROR = err})
    end

    udpObject = newUDP
    udpObject:settimeout(0)

    --[[local newServerTCP,err = bindTCP("*",onlinePlay.portNumber)
    if err ~= nil then
        closeConnectionObjects()
        return textFiles.funcs.replace(textFiles.onlineErrors.tcp,{ERROR = err})
    end

    selfServerTCPObject = newServerTCP
    selfServerTCPObject:settimeout(0)]]

    local newClientTCP,err = socket.tcp()
    if err ~= nil then
        closeConnectionObjects()
        return textFiles.funcs.replace(textFiles.onlineErrors.tcp,{ERROR = err})
    end

    selfClientTCPObject = newClientTCP

    local success,err = newUDP:setsockname("*",gameData.udpPort or 0)
    if err ~= nil then
        closeConnectionObjects()
        return textFiles.funcs.replace(textFiles.onlineErrors.sockName,{ERROR = err})
    end


    local joinPacketData

    if username ~= nil then
        -- Connecting for the first time
        selfClientTCPObject:settimeout(1.5)

        local _,err = selfClientTCPObject:connect(hostAddress,onlinePlay.portNumber)
        if err ~= nil then
            closeConnectionObjects()

            if err == "timeout" or err == "closed" then
                return textFiles.onlineErrors.noConnect
            else
                return textFiles.funcs.replace(textFiles.onlineErrors.connectError,{ERROR = err})
            end
        end

        local successful,err = sendJoinRequest(hostAddress,username,colorIdx)
        if not successful then
            closeConnectionObjects()
            return err
        end

        -- If successful, sendJoinRequest returns join packet data instead of an error
        joinPacketData = err

        -- Create our own TCP server
        local newServerTCP,err = bindTCP("*",onlinePlay.portNumber + onlinePlay.playerIdx - 1)
        if err ~= nil then
            closeConnectionObjects()
            return textFiles.funcs.replace(textFiles.onlineErrors.tcp,{ERROR = err})
        end

        selfServerTCPObject = newServerTCP
        selfServerTCPObject:settimeout(0)

        onlinePlay.tcpEnabled = true
    end

    selfClientTCPObject:settimeout(0)


    -- We're connected!
    onlinePlay.currentMode = onlinePlay.MODE_CLIENT
    hostReconnectAddress = hostAddress or gameData.hostAddress

    if username ~= nil then
        _,gameData.udpPort = udpObject:getsockname()
    else -- reconnecting
        onlinePlay.hostPlayerIdx = gameData.hostPlayerIdx
        onlinePlay.playerIdx = gameData.playerIdx
        
        onlinePlay.isReconnecting = true
        onlinePlay.hasReconnected = false

        onlinePlay.syncedTime = 0

        for playerIndex = 1,battleGeneral.gameData.playerCount do
            local userData = onlinePlay.getUserData(playerIndex)

            if userData ~= nil then
                createUserObject(playerIndex,userData.ip,userData.udpPort)
            end
        end
    end

    connect()

    if joinPacketData ~= nil then
        onlinePlay.decodeJoinPackets(joinPacketData)
    end

    onlinePlay.onInitialise()
    battleCamera.initCameras()
    
    if username ~= nil then
        onlinePlay.onConnect(onlinePlay.playerIdx)
        SFX.play(joinSound)
    end
end

function onlinePlay.disconnect()
    if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE then
        return
    end


    -- Clean up things
    for _,p in ipairs(Player.get()) do
        local data = battlePlayer.getPlayerData(p)

        data.onlineKeys = {}
    end

    if onlinePlay.currentMode == onlinePlay.MODE_HOST then
        for _,user in ipairs(onlinePlay.getUsers()) do
            if user.playerIdx ~= onlinePlay.playerIdx then
                user:disconnect()
            end
        end
    end

    
    onlinePlay.onDisconnect(onlinePlay.playerIdx,"left")
    onlineLogs.close()

    userList = {}
    userMapByPlayer = {}
    userMapByAddress = {}

    reconnectRequestTime = nil
    generalUpdateTime = nil
    playerUpdateTime = nil
    layersUpdateTime = nil

    gameData.logFilename = nil
    gameData.userData = {}
    gameData.playerIdx = 1
    gameData.udpPort = nil
    battleGeneral.gameData.playerCount = 1



    onlinePlay.currentMode = onlinePlay.MODE_OFFLINE
    onlinePlay.isReconnecting = false
    onlinePlay.hasReconnected = false
    onlinePlay.tcpEnabled = false

    closeConnectionObjects()

    onlinePlay.onUninitialise()
    battleCamera.initCameras()
end


-- Encoding/decoding things
do
    -- [COOKING SHOW VOICE] luckily, I prepared some earlier!
    local powersOfTwo = {}
    for i = 0,64,8 do
        powersOfTwo[i] = 2^i
    end


    local function decodeError(typeString,str,start,err)
        local text = "Invalid ".. typeString.. ": ".. err.. "\nPos: ".. start.. ", string: ".. inspect(str).. ", starting: ".. inspect(str:sub(start,start+5))

        error(text,3)
    end

    -- Encode/decode functions
    local encodeFuncs = {}
    local decodeFuncs = {}

    local valueTypeToID = {
        ["number"] = 1,
        ["string"] = 2,
        ["boolean"] = 3,
        ["table"] = 4,
        ["nil"] = 5,
        ["Vector2"] = 6,
        ["Vector3"] = 7,
        ["Vector4"] = 8,
    }
    local valueIDToType = {
        [1] = "number",
        [2] = "string",
        [3] = "boolean",
        [4] = "table",
        [5] = "nil",
        [6] = "Vector2",
        [7] = "Vector3",
        [8] = "Vector4",
    }


    -- Standard Lua types
    function encodeFuncs.number(value)
        return tostring(value).. " "
    end
    function decodeFuncs.number(str,start)
        local nextSpace = str:find(" ",start,true)
        if nextSpace == nil then
            decodeError("number",str,start,"no space included")
        end

        local number = tonumber(str:sub(start,nextSpace - 1))
        if number == nil then
            decodeError("number",str,start,"malformed")
        end

        return number,nextSpace + 1
    end

    function encodeFuncs.boolean(value)
        if value then
            return "T"
        else
            return "F"
        end
    end
    function decodeFuncs.boolean(str,start)
        return (str:sub(start,start) == "T"),start + 1
    end

    encodeFuncs["nil"] = function(value)
        return ""
    end
    decodeFuncs["nil"] = function(str,start)
        return nil,start
    end

    function encodeFuncs.string(value)
        return onlinePlay.encodeValue("uint16",#value).. value
    end
    function decodeFuncs.string(str,start)
        local bytes,newStart = onlinePlay.decodeValue("uint16",str,start)
        local stringEnd = newStart + bytes

        return str:sub(newStart,stringEnd - 1),stringEnd
    end

    function encodeFuncs.table(value)
        local list = {}
        local count = 0

        for k,v in pairs(value) do
            count = count + 1
            list[count] = k
        end

        local str = onlinePlay.encodeValue("uint16",count)

        for _,k in ipairs(list) do
            local v = value[k]

            str = str.. onlinePlay.encodeValue("any",k).. onlinePlay.encodeValue("any",v)
        end
        
        return str
    end
    function decodeFuncs.table(str,start)
        local count,newStart = onlinePlay.decodeValue("uint16",str,start)
        start = newStart

        local tbl = {}

        for i = 1,count do
            local k,newStart = onlinePlay.decodeValue("any",str,start)
            local v,newerStart = onlinePlay.decodeValue("any",str,newStart)

            tbl[k] = v

            start = newerStart
        end

        return tbl,start
    end

    -- Lists (i.e., a table with only sequential numbered keys starting from 1)
    function encodeFuncs.list(value)
        local str = onlinePlay.encodeValue("uint16",#value)

        for _,v in ipairs(value) do
            str = str.. onlinePlay.encodeValue("any",v)
        end

        return str
    end
    function decodeFuncs.list(str,start)
        local count
        start = onlinePlay.decodeValue("uint16",str,start)

        local tbl = {}

        for i = 1,count do
            local v
            v,start = onlinePlay.decodeValue("any",str,start)

            tbl[i] = v
        end

        return tbl,start
    end

    -- Vectors
    function encodeFuncs.Vector2(value)
        return encodeFuncs.number(value[1]).. encodeFuncs.number(value[2])
    end
    function decodeFuncs.Vector2(str,start)
        local x,y
        x,start = decodeFuncs.number(str,start)
        y,start = decodeFuncs.number(str,start)

        return vector.v2(x,y),start
    end

    function encodeFuncs.Vector3(value)
        return encodeFuncs.number(value[1]).. encodeFuncs.number(value[2]).. encodeFuncs.number(value[3])
    end
    function decodeFuncs.Vector3(str,start)
        local x,y,z
        x,start = decodeFuncs.number(str,start)
        y,start = decodeFuncs.number(str,start)
        z,start = decodeFuncs.number(str,start)

        return vector.v3(x,y,z),start
    end

    function encodeFuncs.Vector4(value)
        return encodeFuncs.number(value[1]).. encodeFuncs.number(value[2]).. encodeFuncs.number(value[3]).. encodeFuncs.number(value[4])
    end
    function decodeFuncs.Vector4(str,start)
        local x,y,z,w
        x,start = decodeFuncs.number(str,start)
        y,start = decodeFuncs.number(str,start)
        z,start = decodeFuncs.number(str,start)
        w,start = decodeFuncs.number(str,start)

        return vector.v4(x,y,z,w),start
    end

    -- Unsigned integers
    function encodeFuncs.uint8(value)
        value = math.floor(value + 0.5)

        if value < 0 then
            error("Invalid integer: value (".. value.. ") is less than minimum (0)",3)
        elseif value > 0xFF then
            error("Invalid integer: value (".. value.. ") is greater than maximum (255)",3)
        end

        return string.char(value)
    end
    function decodeFuncs.uint8(str,start)
        return string.byte(str,start),(start + 1)
    end

    function encodeFuncs.uint16(value)
        value = math.floor(value + 0.5)

        if value < 0 then
            error("Invalid integer: value (".. value.. ") is less than minimum (0)",3)
        elseif value > 0xFFFF then
            error("Invalid integer: value (".. value.. ") is greater than maximum (65535)",3)
        end

        return string.char(value % 0x100).. string.char(math.floor(value/0x100))
    end
    function decodeFuncs.uint16(str,start)
        local value = string.byte(str,start) + string.byte(str,start + 1)*0x100

        return value,(start + 2)
    end

    function encodeFuncs.uint32(value)
        value = math.floor(value + 0.5)

        if value < 0 then
            error("Invalid integer: value (".. value.. ") is less than minimum (0)",3)
        elseif value > 0xFFFFFFFF then
            error("Invalid integer: value (".. value.. ") is greater than maximum (4294967295)",3)
        end

        return string.char(value % 0x100).. string.char(math.floor(value/0x100) % 0x100).. string.char(math.floor(value/0x10000) % 0x100).. string.char(math.floor(value/0x1000000) % 0x100)
    end
    function decodeFuncs.uint32(str,start)
        local value = string.byte(str,start) + string.byte(str,start + 1)*0x100 + string.byte(str,start + 2)*0x10000 + string.byte(str,start + 3)*0x1000000

        return value,(start + 4)
    end

    -- Signed integers
    function encodeFuncs.sint8(value)
        value = math.floor(value + 0.5)

        if value < -0x80 then
            error("Invalid integer: value (".. value.. ") is less than minimum (-128)",3)
        elseif value >= 0x80 then
            error("Invalid integer: value (".. value.. ") is greater than maximum (127)",3)
        end

        if value < 0 then
            value = 0x100 + value
        end

        return string.char(value)
    end
    function decodeFuncs.sint8(str,start)
        local value = string.byte(str,start)

        if value >= 0x80 then
            value = value - 0x100
        end

        return value,(start + 1)
    end

    function encodeFuncs.sint16(value)
        value = math.floor(value + 0.5)

        if value < -0x8000 then
            error("Invalid integer: value (".. value.. ") is less than minimum (-32768)",3)
        elseif value >= 0x8000 then
            error("Invalid integer: value (".. value.. ") is greater than maximum (32767)",3)
        end

        if value < 0 then
            value = 0x10000 + value
        end

        return string.char(value % 0x100).. string.char(math.floor(value/0x100))
    end
    function decodeFuncs.sint16(str,start)
        local value = string.byte(str,start) + string.byte(str,start + 1)*0x100

        if value >= 0x8000 then
            value = value - 0x10000
        end

        return value,(start + 2)
    end

    function encodeFuncs.sint32(value)
        value = math.floor(value + 0.5)

        if value < -0x80000000 then
            error("Invalid integer: value (".. value.. ") is less than minimum (-2147483648)",3)
        elseif value >= 0x80000000 then
            error("Invalid integer: value (".. value.. ") is greater than maximum (2147483647)",3)
        end

        if value < 0 then
            value = 0x100000000 + value
        end

        return string.char(value % 0x100).. string.char(math.floor(value/0x100) % 0x100).. string.char(math.floor(value/0x10000) % 0x100).. string.char(math.floor(value/0x1000000) % 0x100)
    end
    function decodeFuncs.sint32(str,start)
        local value = string.byte(str,start) + string.byte(str,start + 1)*0x100 + string.byte(str,start + 2)*0x10000 + string.byte(str,start + 3)*0x1000000

        if value >= 0x80000000 then
            value = value - 0x100000000
        end

        return value,(start + 4)
    end

    -- "Any" type
    function encodeFuncs.any(value)
        local typeString = type(value)
        local typeID = valueTypeToID[typeString]

        if typeID == nil then
            error("Could not encode type: ".. typeString)
        end

        return string.char(typeID).. onlinePlay.encodeValue(typeString,value)
    end
    function decodeFuncs.any(str,start)
        local typeID = string.byte(str:sub(start,start))
        local typeString = valueIDToType[typeID]

        if typeString == nil then
            decodeError("type ID",str,start,"no type string for given ID: ".. tostring(typeID))
        end

        local value,newStart = onlinePlay.decodeValue(typeString,str,start + 1)

        return value,newStart
    end


    function onlinePlay.encodeValue(typeString,value)
        local func = encodeFuncs[typeString]
        if func ~= nil then
            return func(value)
        end


        local intBytes = typeString:match("^int(%d+)$")

        if intBytes ~= nil then
            -- Check if it's valid
            value = math.floor(value + 0.5)

            local max = powersOfTwo[intBytes*8]*0.5
            if value >= max then
                error("Invalid integer: too large")
            elseif value < -max then
                error("Invalid integer: too small")
            end


            local limit = (intBytes - 1)
            local str = ""

            if value < 0 then
                value = powersOfTwo[intBytes*8] + value
            end
            
            for i = limit,0,-1 do
                local byte = math.floor(value/powersOfTwo[i*8]) % 0x100

                str = str.. string.char(byte)
            end

            return str
        end

        error("Unrecognised encoding type: ".. typeString,2)
    end

    function onlinePlay.decodeValue(typeString,str,start)
        start = start or 1

        local func = decodeFuncs[typeString]
        if func ~= nil then
            local value,newStart = func(str,start)

            return value,newStart
        end


        local intBytes = typeString:match("^int(%d+)$")

        if intBytes ~= nil then
            local limit = (intBytes - 1)
            local value = 0

            local pos = start

            for i = limit,0,-1 do
                local byte = string.byte(str:sub(pos,pos))

                if byte == nil then
                    decodeError(typeString,str,start,"too short")
                end

                value = value + byte*powersOfTwo[i*8]
                pos = pos + 1
            end

            if value >= powersOfTwo[intBytes*8]*0.5 then
                value = value - powersOfTwo[intBytes*8]
            end

            return value,start + intBytes
        end

        error("Unrecognised decoding type: ".. typeString,2)
    end


    function onlinePlay.encodeObject(v,properties)
        local defaultPropertiesCounter = 0
        local output = ""

        for propIndex,prop in ipairs(properties) do
            if type(prop[1]) == "function" then -- use encoder function
                output = output.. string.char(defaultPropertiesCounter).. prop[1](v)
                defaultPropertiesCounter = 0
            else
                local value,defaultValue

                if type(prop[2]) == "string" then
                    value = v[prop[2]]
                    defaultValue = prop[3]
                else
                    value = v:mem(prop[2],prop[3])
                    defaultValue = prop[4]
                end

                if defaultValue == nil or value ~= defaultValue then
                    output = output.. string.char(defaultPropertiesCounter).. onlinePlay.encodeValue(prop[1],value)
                    defaultPropertiesCounter = 0
                else
                    defaultPropertiesCounter = defaultPropertiesCounter + 1
                end
            end
        end

        if defaultPropertiesCounter > 0 then
            output = output.. string.char(defaultPropertiesCounter)
        end

        return output
    end

    function onlinePlay.decodeObject(v,str,start,properties)
        local defaultPropertiesCounter = 0
        local shouldCheckDefaultCount = true

        for propIndex,prop in ipairs(properties) do
            if shouldCheckDefaultCount then
                defaultPropertiesCounter = string.byte(str[start])
                shouldCheckDefaultCount = (defaultPropertiesCounter == 0)
                start = start + 1
            elseif defaultPropertiesCounter == 0 then
                shouldCheckDefaultCount = true
            end

            if type(prop[1]) == "function" then -- use decoder function
                start = prop[2](v,str,start)
            else
                if type(prop[2]) == "string" then
                    if defaultPropertiesCounter == 0 then
                        v[prop[2]],start = onlinePlay.decodeValue(prop[1],str,start)
                    else
                        v[prop[2]] = prop[3]
                    end
                else
                    if defaultPropertiesCounter == 0 then
                        local value,newStart = onlinePlay.decodeValue(prop[1],str,start)

                        v:mem(prop[2],prop[3],value)
                        start = newStart
                    else
                        v:mem(prop[2],prop[3],prop[4])
                    end
                end

                defaultPropertiesCounter = math.max(0,defaultPropertiesCounter - 1)
            end
        end

        return start
    end
end


-- Block hitting
do
    local blockHitCommand = onlinePlay.createCommand("_block_hit",onlinePlay.IMPORTANCE_MAJOR)
    local blockRemoveCommand = onlinePlay.createCommand("_block_remove",onlinePlay.IMPORTANCE_MAJOR)

    local canAlwaysRemoveBlock
    local canAlwaysHitBlock

    function onlinePlay.onBlockHit(eventObj,b,fromTop,playerObj)
        if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE or canAlwaysHitBlock == b then
            return
        end

        local culpritIdx = 0
        local ownerIdx = 1

        if playerObj ~= nil then
            culpritIdx = playerObj.idx
            ownerIdx = culpritIdx
        end


        if ownerIdx ~= onlinePlay.playerIdx then
            eventObj.cancelled = true
            return
        end

        if b.contentID > 0 then
            b.contentID = randomPowerups.pickBlockContent(b,playerObj or randomPowerups.guessHittingPlayer(b))
        end

        blockHitCommand:send(0, b.idx,b.contentID,culpritIdx,fromTop)
    end

    function blockHitCommand.onReceive(sourcePlayerIdx, blockIdx,contentID,culpritIdx,fromTop)
        if (blockIdx < 1 or blockIdx > Block.count()) or (culpritIdx < 0 or culpritIdx > Player.count()) then
            return
        end

        local b = Block(blockIdx)

        canAlwaysHitBlock = b
        b.contentID = contentID

        if culpritIdx > 0 then
            b:hit(fromTop,Player(culpritIdx))
        else
            b:hitWithoutPlayer(fromTop)
        end

        canAlwaysHitBlock = nil
    end


    function onlinePlay.onBlockRemove(eventObj,b,makeEffects,delete)
        if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE or canAlwaysRemoveBlock == b then
            return
        end

        if onlinePlay.currentMode == onlinePlay.MODE_CLIENT then
            eventObj.cancelled = true
            return
        end

        blockRemoveCommand:send(0, b.idx,makeEffects,delete)
    end

    function blockRemoveCommand.onReceive(sourcePlayerIdx, blockIdx,makeEffects,delete)
        if sourcePlayerIdx ~= onlinePlay.hostPlayerIdx or blockIdx < 1 or blockIdx > Block.count() then
            return
        end

        local b = Block(blockIdx)

        canAlwaysRemoveBlock = b

        if delete then
            b:delete()
        else
            b:remove(makeEffects)
        end

        canAlwaysRemoveBlock = nil
    end
end


-- Layer and event syncing
local triggerEventCommand = onlinePlay.createCommand("_triggerEvent",onlinePlay.IMPORTANCE_MAJOR)

local layersResponseCommand = onlinePlay.createCommand("_layersResponse",onlinePlay.IMPORTANCE_MINOR)
local layersRequestCommand = onlinePlay.createCommand("_layersRequest",onlinePlay.IMPORTANCE_MINOR)

local layersJoinPacket = onlinePlay.createJoinPacket("_layers")

do
    local layerData = {}

    function onlinePlay.getLayerData(layerName)
        local data = layerData[layerName]

        if data == nil then
            data = {}
            layerData[layerName] = data

            data.x = 0 -- relative to where it started
            data.y = 0

            data.desiredX = nil
            data.desiredY = nil
            data.storedSpeedX = nil
            data.storedSpeedY = nil

            data.wasUpdated = false -- used in layersResponseCommand
        end

        return data
    end


    local allowedTriggerEventName

    function triggerEventCommand.onReceive(sourcePlayerIdx, eventName)
        if onlinePlay.currentMode == onlinePlay.MODE_HOST or sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
            return
        end

        allowedTriggerEventName = eventName
        triggerEvent(eventName)
        allowedTriggerEventName = nil
    end

    function onlinePlay.onEventDirect(eventObj,eventName)
        if onlinePlay.isReconnecting or lunatime.tick() <= 1 then
            return
        end

        if onlinePlay.currentMode == onlinePlay.MODE_CLIENT then
            -- Allow it if it comes from the host
            if allowedTriggerEventName == eventName then
                allowedTriggerEventName = nil
                return
            end

            -- Otherwise, cancel the event
            eventObj.cancelled = true
        end
    end

    function onlinePlay.onPostEventDirect(eventName)
        if onlinePlay.isReconnecting or lunatime.tick() <= 1 then
            return
        end
        
        if onlinePlay.currentMode == onlinePlay.MODE_HOST then
            -- Tell everybody else
            triggerEventCommand:send(0, eventName)
        end
    end


    function layersRequestCommand.onReceive(sourcePlayerIdx)
        -- Encode data for layers
        local layerProperties = ""

        for _,layer in ipairs(Layer.get()) do
            local data = onlinePlay.getLayerData(layer.name)

            if layer.isHidden or data.x ~= 0 or data.y ~= 0 or layer.speedX ~= 0 or layer.speedY ~= 0 then
                -- Encode name to identify it
                layerProperties = layerProperties.. onlinePlay.encodeValue("string",layer.name)

                -- Include a bunch of relevant properties for the client
                layerProperties = layerProperties.. onlinePlay.encodeValue("boolean",layer.isHidden)
                layerProperties = layerProperties.. onlinePlay.encodeValue("number",layer.speedX)
                layerProperties = layerProperties.. onlinePlay.encodeValue("number",layer.speedY)
                layerProperties = layerProperties.. onlinePlay.encodeValue("number",data.x)
                layerProperties = layerProperties.. onlinePlay.encodeValue("number",data.y)
            end
        end

        layersResponseCommand:send(sourcePlayerIdx, layerProperties)
    end

    function layersResponseCommand.onReceive(sourcePlayerIdx, layerProperties)
        local pos = 1

        while (layerProperties[pos] ~= nil) do
            local name
            name,pos = onlinePlay.decodeValue("string",layerProperties,pos)

            local layer = Layer.get(name)

            if layer ~= nil then
                local data = onlinePlay.getLayerData(name)

                local isHidden
                isHidden,pos = onlinePlay.decodeValue("boolean",layerProperties,pos)

                data.storedSpeedX,pos = onlinePlay.decodeValue("number",layerProperties,pos)
                data.storedSpeedY,pos = onlinePlay.decodeValue("number",layerProperties,pos)
                data.desiredX,pos = onlinePlay.decodeValue("number",layerProperties,pos)
                data.desiredY,pos = onlinePlay.decodeValue("number",layerProperties,pos)

                if isHidden and not layer.isHidden then
                    layer:hide(true)
                elseif not isHidden and layer.isHidden then
                    layer:show(true)
                end

                data.wasUpdated = true
            end
        end

        for _,layer in ipairs(Layer.get()) do
            local data = onlinePlay.getLayerData(layer.name)

            if data.wasUpdated then
                data.wasUpdated = false
            else
                if layer.isHidden then
                    layer:show(true)
                end

                if layer.speedX ~= 0 or layer.speedY ~= 0 then
                    layer:stop()
                end
            end
        end
    end



    local function updateLayer(layer)
        local data = onlinePlay.getLayerData(layer.name)

        if data.desiredX ~= nil and data.desiredY ~= nil then
            layer.speedX = data.desiredX - data.x
            layer.speedY = data.desiredY - data.y
            data.desiredX = nil
            data.desiredY = nil
        elseif data.storedSpeedX ~= nil or data.storedSpeedY ~= nil then
            layer.speedX = data.storedSpeedX or 0
            layer.speedY = data.storedSpeedY or 0
            data.storedSpeedX = nil
            data.storedSpeedY = nil
        end

        data.x = data.x + layer.speedX
        data.y = data.y + layer.speedY
    end

    local layerMoveStates = table.map{FORCEDSTATE_NONE,FORCEDSTATE_PIPE,FORCEDSTATE_ONTONGUE,FORCEDSTATE_SWALLOWED}

    local function layersArePaused()
        for _,p in ipairs(Player.get()) do
            if not layerMoveStates[p.forcedState] then
                return true
            end
        end

        return false
    end


    function layersJoinPacket.encodeData(targetPlayerIdx)
        local layerProperties = ""

        for _,layer in ipairs(Layer.get()) do
            local data = onlinePlay.getLayerData(layer.name)

            -- Encode name to identify it
            layerProperties = layerProperties.. onlinePlay.encodeValue("string",layer.name)

            -- Include a bunch of relevant properties for the client
            layerProperties = layerProperties.. onlinePlay.encodeValue("boolean",layer.isHidden)
            layerProperties = layerProperties.. onlinePlay.encodeValue("number",layer.speedX)
            layerProperties = layerProperties.. onlinePlay.encodeValue("number",layer.speedY)
            layerProperties = layerProperties.. onlinePlay.encodeValue("number",data.x)
            layerProperties = layerProperties.. onlinePlay.encodeValue("number",data.y)
        end

        return layerProperties
    end

    function layersJoinPacket.onReceive(layerProperties)
        local pos = 1

        while (pos <= #layerProperties) do
            local name,isHidden

            name,pos = onlinePlay.decodeValue("string",layerProperties,pos)

            local layer = Layer.get(name)
            local data = onlinePlay.getLayerData(name)

            isHidden,pos = onlinePlay.decodeValue("boolean",layerProperties,pos)

            data.storedSpeedX,pos = onlinePlay.decodeValue("number",layerProperties,pos)
            data.storedSpeedY,pos = onlinePlay.decodeValue("number",layerProperties,pos)
            data.desiredX,pos = onlinePlay.decodeValue("number",layerProperties,pos)
            data.desiredY,pos = onlinePlay.decodeValue("number",layerProperties,pos)

            if isHidden and not layer.isHidden then
                layer:hide(true)
            elseif not isHidden and layer.isHidden then
                layer:show(true)
            end
        end
    end


    function onlinePlay.updateLayers()
        local isPaused = layersArePaused()

        for _,layer in ipairs(Layer.get()) do
            if not isPaused or not layer.pauseDuringEffect then
                updateLayer(layer)
            end
        end
    end
end


-- Custom variables
local sendCustomVariables

do
    local variableMT = {}
    variableMT.__type = "OnlineVariable"

    onlinePlay.variableList = {}
    onlinePlay.variableMap  = {}

    onlinePlay.activeVariables = {}
    onlinePlay.activeVariableCount = 0


    local variableSendCommand = onlinePlay.createCommand("_variableSend",onlinePlay.IMPORTANCE_MINOR)
    local variableAllCommand = onlinePlay.createCommand("_variableAll",onlinePlay.IMPORTANCE_MINOR)


    function variableMT.__index(self,key)
        if key == "value" then
            return self._value
        elseif key == "active" then
            return self._active
        elseif key == "name" then
            return self._name
        elseif key == "typeString" then
            return self._typeString
        else
            return rawget(variableMT,key)
        end
    end

    function variableMT.__newindex(self,key,value)
        if key == "value" then
            self._value = value
        elseif key == "active" then
            if onlinePlay.currentMode == onlinePlay.MODE_CLIENT then
                error("Cannot set custom variable's 'active' as a client",2)
            end

            if not value and self._active then
                local index = table.ifind(onlinePlay.activeVariables,self)

                if index ~= nil then
                    onlinePlay.activeVariableCount = onlinePlay.activeVariableCount - 1
                    table.remove(onlinePlay.activeVariables,index)
                end
            elseif value and not self._active then
                onlinePlay.activeVariableCount = onlinePlay.activeVariableCount + 1
                onlinePlay.activeVariables[onlinePlay.activeVariableCount] = self
            end

            self._active = value
        elseif key == "name" or key == "typeString" then
            error("Property '".. key.. "' is read-only",2)
        else
            rawset(self,key,value)
        end
    end


    local function encodeVariable(variable)
        return onlinePlay.encodeValue("string",variable.name).. onlinePlay.encodeValue(variable.typeString,variable.value)
    end

    local function decodeVariable(params,start)
        local name
        name,start = onlinePlay.decodeValue("string",params,start)

        local variable = onlinePlay.variableMap[name]
        if variable == nil then
            error("Variable '".. name.. "' does not exist on client end")
        end

        -- Update its value
        variable._value,start = onlinePlay.decodeValue(variable.typeString,params,start)

        return variable,start
    end


    -- Sends the current value to everyone immediately, even if it is inactive.
    local validImportanceValues = table.map{onlinePlay.IMPORTANCE_MINOR,onlinePlay.IMPORTANCE_MAJOR,onlinePlay.IMPORTANCE_VITAL}

    function variableMT:send(importance)
        if onlinePlay.currentMode ~= onlinePlay.MODE_HOST then
            error("Can only send variable's value as a host",2)
        end

        if importance == nil then
            importance = onlinePlay.IMPORTANCE_MAJOR
        elseif not validImportanceValues[importance] then
            error("Invalid importance value",2)
        end

        variableSendCommand:sendWithImportance(0,importance, encodeVariable(self))
    end


    function onlinePlay.createVariable(name,typeString,active,value)
        if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
            error("Cannot create custom variable while playing online",2)
        end

        -- Sanity checking
        if type(name) ~= "string" then
            error("Variable name must be a string",2)
        elseif onlinePlay.variableMap[name] ~= nil then
            error("Variable name '".. name.. "' is already in use",2)
        end

        if type(typeString) ~= "string" then
            error("Type must be either nil or a string",2)
        end

        if type(active) ~= "boolean" then
            error("Active value must be a boolean",2)
        end

        -- Setting up the variable
        local variable = setmetatable({},variableMT)

        variable._name = name

        variable._typeString = typeString
        variable._value = value

        variable.active = active

        table.insert(onlinePlay.variableList,variable)
        onlinePlay.variableMap[name] = variable
        
        return variable
    end


    function sendCustomVariables(playerIdx)
        local str = onlinePlay.encodeValue("uint16",onlinePlay.activeVariableCount)

        for _,variable in ipairs(onlinePlay.activeVariables) do
            str = str.. encodeVariable(variable)
        end

        variableAllCommand:send(playerIdx, str)
    end

    
    function variableAllCommand.onReceive(sourcePlayerIdx, str)
        -- Only accept from the host
        if onlinePlay.currentMode == onlinePlay.MODE_HOST or sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
            return
        end

        -- Clear active variables
        for i = 1,onlinePlay.activeVariableCount do
            local variable = onlinePlay.activeVariables[i]

            variable._active = false

            onlinePlay.activeVariables[i] = nil
        end

        onlinePlay.activeVariableCount = 0

        -- Parse each new one
        local count,start = onlinePlay.decodeValue("uint16",str,1)
        local variable

        for i = 1,count do
            variable,start = decodeVariable(str,start)

            -- Make it active
            onlinePlay.activeVariableCount = onlinePlay.activeVariableCount + 1
            onlinePlay.activeVariables[onlinePlay.activeVariableCount] = variable

            variable._active = true
        end
    end

    function variableSendCommand.onReceive(sourcePlayerIdx, str)
        if onlinePlay.currentMode == onlinePlay.MODE_HOST or sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
            return
        end

        decodeVariable(str,1)
    end
end


-- Misc commands
local loadLevelCommand = onlinePlay.createCommand("_loadLevel",onlinePlay.IMPORTANCE_VITAL)

local generalResponseCommand = onlinePlay.createCommand("_generalResponse",onlinePlay.IMPORTANCE_MINOR)
local generalRequestCommand = onlinePlay.createCommand("_generalRequest",onlinePlay.IMPORTANCE_MINOR)

local pingCommand = onlinePlay.createCommand("_ping",onlinePlay.IMPORTANCE_MINOR)
local pongCommand = onlinePlay.createCommand("_pong",onlinePlay.IMPORTANCE_MINOR)

do
    -- Load level
    function loadLevelCommand.onReceive(sourcePlayerIdx, filename,mode,playerTeams,ruleset)
        -- Only accept from the host
        if onlinePlay.currentMode == onlinePlay.MODE_HOST or sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
            return
        end

        battleGeneral.gameData.teamsEnabled = (playerTeams ~= nil)
        battleGeneral.gameData.decidedPlayerTeams = playerTeams or {}

        battleGeneral.gameData.onlineRuleset = ruleset
        battleGeneral.gameData.mode = mode

        Level.load(filename)
    
        Misc.unpause()
    end

    -- "General" updates
    function generalRequestCommand.onReceive(sourcePlayerIdx)
        if onlinePlay.currentMode == onlinePlay.MODE_CLIENT then
            return
        end

        generalResponseCommand:send(sourcePlayerIdx, onlinePlay.syncedTime,RNG.seed--[[,LegacyRNG.seed]])
        sendCustomVariables(sourcePlayerIdx)
    end

    function generalResponseCommand.onReceive(sourcePlayerIdx, syncedTime,rngSeed--[[,legacyRNGSeed]])
        -- Only accept from the host
        if onlinePlay.currentMode == onlinePlay.MODE_HOST or sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
            return
        end

        onlinePlay.syncedTime = syncedTime
        --LegacyRNG.seed = legacyRNGSeed
        RNG.seed = rngSeed
    end


    -- Ping
    function pingCommand.onReceive(sourcePlayerIdx, counter)
        if onlinePlay.currentMode == onlinePlay.MODE_HOST then
            -- Send other users' ping
            local otherUserValues = {}

            for _,user in ipairs(onlinePlay.getUsers()) do
                if user.playerIdx ~= sourcePlayerIdx then
                    otherUserValues[user.playerIdx] = {ping = user.pingValue,isUnresponsive = user.isUnresponsive}
                end
            end

            pongCommand:send(sourcePlayerIdx, counter,otherUserValues)
        else
            -- Only allow from host
            if sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
                return
            end

            pongCommand:send(sourcePlayerIdx, counter)
        end
    end

    function pongCommand.onReceive(sourcePlayerIdx, counter,otherUserValues)
        -- Only accept from the host, or if we are the host
        if onlinePlay.currentMode == onlinePlay.MODE_CLIENT and sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
            return
        end

        local user = onlinePlay.getUserByPlayer(sourcePlayerIdx)
        if user == nil then
            return
        end

        if user.pingedTime == nil or counter ~= user.pingCounter then
            return
        end

        local ping = (onlinePlay.localTime - user.pingedTime)

        if onlinePlay.currentMode == onlinePlay.MODE_HOST then
            user.pingValue = ping
            return
        end

        -- The host also sent back other users' pings
        for _,user in ipairs(onlinePlay.getUsers()) do
            if user.playerIdx == onlinePlay.playerIdx then
                user.pingValue = ping
            else
                local values = otherUserValues[user.playerIdx]
                
                if values ~= nil then
                    user.isUnresponsive = values.isUnresponsive
                    user.pingValue = values.ping
                end
            end
        end
    end
end

-- Reconnecting
local reconnectRequestCommand = onlinePlay.createCommand("_reconnectRequest",onlinePlay.IMPORTANCE_MINOR)
local reconnectResponseCommand = onlinePlay.createCommand("_reconnectResponse",onlinePlay.IMPORTANCE_MINOR)
local reconnectReadyCommand = onlinePlay.createCommand("_reconnectReady",onlinePlay.IMPORTANCE_MAJOR)

local handleReconnecting

do
    function reconnectRequestCommand.onReceive(sourcePlayerIdx)
        if onlinePlay.currentMode == onlinePlay.MODE_CLIENT then
            return
        end

        if not onlinePlay.isReconnecting or not onlinePlay.isConnected(sourcePlayerIdx) then
            return
        end

        if not gameData.reconnectingMap[sourcePlayerIdx] then
            return
        end

        gameData.reconnectingMap[sourcePlayerIdx] = nil
        reconnectResponseCommand:send(sourcePlayerIdx)
    end

    function reconnectResponseCommand.onReceive(sourcePlayerIdx)
        if onlinePlay.currentMode == onlinePlay.MODE_HOST or sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
            return
        end

        if not onlinePlay.isReconnecting then
            return
        end

        -- Now that we know that the host can respond, connect via TCP
        selfClientTCPObject:settimeout(5)

        local _,err = selfClientTCPObject:connect(hostReconnectAddress,onlinePlay.portNumber)
        if err ~= nil then
            onlinePlay.disconnect()
            return
        end

        local successful = sendReconnectRequest()
        if not successful then
            onlinePlay.disconnect()
            return
        end

        selfClientTCPObject:settimeout(0)

        -- Create our own TCP server
        local newServerTCP,err = bindTCP("*",onlinePlay.portNumber + onlinePlay.playerIdx - 1)
        if err ~= nil then
            onlinePlay.disconnect()
            return
        end

        selfServerTCPObject = newServerTCP
        selfServerTCPObject:settimeout(0)


        onlinePlay.hasReconnected = true
        onlinePlay.tcpEnabled = true
    end

    function reconnectReadyCommand.onReceive(sourcePlayerIdx)
        if onlinePlay.currentMode == onlinePlay.MODE_HOST or sourcePlayerIdx ~= onlinePlay.hostPlayerIdx then
            return
        end

        if not onlinePlay.isReconnecting then
            return
        end

        for _,user in ipairs(onlinePlay.getUsers()) do
            if not user.isHost and user.playerIdx < onlinePlay.playerIdx then
                connectToFellowClient(user.playerIdx)
            end
        end

        onlinePlay.isReconnecting = false
    end


    local function everyoneIsReady()
        for i = 1,battleGeneral.gameData.playerCount do
            if i ~= onlinePlay.playerIdx and onlinePlay.isConnected(i) and (gameData.reconnectingMap[i] or userTCPObjects[i] == nil) then
                return false
            end
        end

        return true
    end
    

    local reconnectRequestFrequency = 1/10

    function handleReconnecting()
        receiveAndHandleData()

        if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE or not onlinePlay.isReconnecting then
            return
        end
        
        if onlinePlay.currentMode == onlinePlay.MODE_HOST then
            if everyoneIsReady() then
                reconnectReadyCommand:send(0)
                onlinePlay.isReconnecting = false
            end
        else
            if not onlinePlay.hasReconnected and (reconnectRequestTime == nil or reconnectRequestTime <= (onlinePlay.localTime - reconnectRequestFrequency)) then
                reconnectRequestCommand:send(onlinePlay.hostPlayerIdx)
                reconnectRequestTime = onlinePlay.localTime
            end
        end
    end
end



local generalUpdateFrequency = 1/12
local playerUpdateFrequency = 1/30
local layersUpdateFrequency = 1/4

local function onlineUpdate()
    local t = Routine.pauseDeltaTime

    if lunatime.drawtick() <= 1 then
        return
    end

    onlinePlay.localTime = onlinePlay.localTime + t

    if onlinePlay.isReconnecting then
        handleReconnecting()
        return
    end

    onlinePlay.syncedTime = onlinePlay.syncedTime + t

    receiveAndHandleData()

    if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE or onlinePlay.isReconnecting then
        return
    end

    -- Regular updates
    if playerUpdateTime == nil or playerUpdateTime <= (onlinePlay.localTime - playerUpdateFrequency) then
        onlinePlayPlayers.sendOwnPlayerUpdate(0)
        playerUpdateTime = onlinePlay.localTime
    end

    if onlinePlay.currentMode == onlinePlay.MODE_CLIENT then
        if layersUpdateTime == nil or layersUpdateTime <= (onlinePlay.localTime - layersUpdateFrequency) then
            layersRequestCommand:send(onlinePlay.hostPlayerIdx)
            layersUpdateTime = onlinePlay.localTime
        end

        if generalUpdateTime == nil or generalUpdateTime <= (onlinePlay.localTime - generalUpdateFrequency) then
            generalRequestCommand:send(onlinePlay.hostPlayerIdx)
            generalUpdateTime = onlinePlay.localTime
        end
    end

    -- Handling for each user
    for _,user in ipairs(onlinePlay.getUsers()) do
        if user.playerIdx ~= onlinePlay.playerIdx then
            if user.isHost or onlinePlay.currentMode == onlinePlay.MODE_HOST then
                -- Pinging
                if user.pingedTime == nil or user.pingedTime <= (onlinePlay.localTime - onlinePlay.pingFrequency) then
                    user.pingCounter = user.pingCounter + 1
                    user.pingedTime = onlinePlay.localTime

                    pingCommand:send(user.playerIdx, user.pingCounter)
                end
            end

            if onlinePlay.currentMode == onlinePlay.MODE_HOST then
                local timeSinceReceived = (onlinePlay.localTime - user.lastResponseTime)

                user.isUnresponsive = (timeSinceReceived >= onlinePlay.unresponsiveTime)
            end
        end
    end

    onlinePlayNPC.handleNPCs()
end


function onlinePlay.onInputUpdate()
    onlineUpdate()
end


function onlinePlay.onCameraDraw()
    if not onlinePlay.debugMode or onlinePlay.currentMode == onlinePlay.MODE_OFFLINE then
        return
    end

    onlinePlayNPC.drawNPCDebugText()

    -- Layer debug
    --[[local y = camera.height - Layer.count()*18 - 8

    for _,layer in ipairs(Layer.get()) do
        local data = onlinePlay.getLayerData(layer.name)

        Text.print(string.format("%s (%.2f, %.2f)", layer.name,data.x,data.y),8,y)
        y = y + 18
    end]]
end


function onlinePlay.onStart()
    -- Handle reconnecting
    local err

    if gameData.mode == onlinePlay.MODE_HOST then
        err = onlinePlay.host()
    elseif gameData.mode == onlinePlay.MODE_CLIENT then
        err = onlinePlay.joinHost()
    end

    gameData.mode = onlinePlay.MODE_OFFLINE

    if err ~= nil then
        error(err)
    end
end

function onlinePlay.onExitLevel()
    if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
        gameData.mode = onlinePlay.currentMode
    end


    local filename = mem(NEXT_LEVEL_FILENAME_ADDR,FIELD_STRING)
    if filename == "" then
        filename = Level.filename()
    end

    if onlinePlay.currentMode == onlinePlay.MODE_HOST then
        -- Setup reconnecting map
        gameData.reconnectNecessary = false
        gameData.reconnectingMap = {}

        for playerIdx = 1,battleGeneral.gameData.playerCount do
            if playerIdx ~= onlinePlay.playerIdx and onlinePlay.isConnected(playerIdx) then
                gameData.reconnectingMap[playerIdx] = true
                gameData.reconnectNecessary = true
            end
        end

        -- Tell the clients where we're headed
        local playerTeams = (battleGeneral.gameData.teamsEnabled and battleGeneral.gameData.decidedPlayerTeams) or nil

        loadLevelCommand:send(0, filename,battleGeneral.gameData.mode,playerTeams,battleOptions.getFullRuleset())

        socket.sleep(0.5)

        if onlinePlay.debugMode then
            socket.sleep(onlinePlay.debugArtificialDelayMax)
        end
    else
        gameData.hostAddress = hostReconnectAddress
    end

    onlineLogs.add("<Level load to: ".. filename.. ">")
end


function onlinePlay.onInitAPI()
    registerEvent(onlinePlay,"onInputUpdate","onInputUpdate",false)
    registerEvent(onlinePlay,"onCameraDraw","onCameraDraw",false)
    registerEvent(onlinePlay,"onDrawEnd")

    registerEvent(onlinePlay,"onBlockHit")
    registerEvent(onlinePlay,"onBlockRemove")

    registerEvent(onlinePlay,"onEventDirect")
    registerEvent(onlinePlay,"onPostEventDirect")
    registerEvent(onlinePlay,"onTick","updateLayers",false)

    registerEvent(onlinePlay,"onStart")
    registerEvent(onlinePlay,"onExitLevel","onExitLevel",false)

    registerCustomEvent(onlinePlay,"onReceive")
    registerCustomEvent(onlinePlay,"onConnect")
    registerCustomEvent(onlinePlay,"onDisconnect")
    registerCustomEvent(onlinePlay,"onInitialise")
    registerCustomEvent(onlinePlay,"onUninitialise")


    battleGeneral = require("scripts/battleGeneral")
    battlePlayer = require("scripts/battlePlayer")
    battleOptions = require("scripts/battleOptions")
    battleCamera = require("scripts/battleCamera")

    randomPowerups = require("scripts/randomPowerups")

    onlinePlayNPC = require("scripts/onlinePlay_npc")
    onlinePlayPlayers = require("scripts/onlinePlay_players")
    onlineChat = require("scripts/onlineChat")
    onlineLogs = require("scripts/onlineLogs")


    pcall(function() socket = require("socket.core") end)
    onlinePlay.socketLoaded = (socket ~= nil)
end


return onlinePlay