local battlePlayer,battleCamera,battleMenu
local onlinePlay,onlinePlayNPC,onlineChat

local perPlayerCostumes = require("scripts/perPlayerCostumes")

local onlinePlayPlayers = {}


local NPC_STRUCT_SIZE = 0x158
local NPC_ADDR = readmem(0x00B259E8, FIELD_DWORD) + 128*NPC_STRUCT_SIZE


local HITSIDE_NONE = 0
local HITSIDE_TOP = 1
local HITSIDE_RIGHT = 2
local HITSIDE_BOTTOM = 3
local HITSIDE_LEFT = 4
local HITSIDE_UNKNOWN = 5


local nonHarmfulProjectileNPCIDs = table.map{134,137,154,155,156,157,166}

local characterGrabTopTime = {
    [CHARACTER_MARIO] = 12,
    [CHARACTER_LUIGI] = 12,
    [CHARACTER_PEACH] = 16,
    [CHARACTER_TOAD] = 8,
}


local playerUpdateCommand
local jumpedCommand
local grabNPCCommand,releaseNPCCommand
local warpEnterCommand

local playersJoinPacket


local function canSendOwnKeys()
    if battleMenu.currentMenu ~= nil and battleMenu.controllingPlayer.idx == onlinePlay.playerIdx then
        return false
    end

    if onlineChat.active then
        return false
    end

    return true
end


local function getStoodOnObject(p)
    local stoodOnIndex = p:mem(0x176,FIELD_WORD)

    -- Standing on an NPC
    if stoodOnIndex > 0 then
        if stoodOnIndex > NPC.count() then
            return nil
        end

        return NPC(stoodOnIndex - 1)
    end

    -- Standing on a moving block
    if stoodOnIndex < 0 then
        local npcAddress = NPC_ADDR + stoodOnIndex*NPC_STRUCT_SIZE
        local blockIndex = readmem(npcAddress + 0xF0,FIELD_DFLOAT)

        if blockIndex < 1 or blockIndex > Block.count() then
            return nil
        end

        return Block(blockIndex)
    end

    return nil
end


local keysOrder = {"up","down","left","right","jump","run","altJump","altRun","dropItem","pause"}


onlinePlayPlayers.playerSendProperties = {
    -- Keys
    {
        function(v)
            local data = battlePlayer.getPlayerData(v)

            local canSend = canSendOwnKeys()
            local str = ""

            for _,name in ipairs(keysOrder) do
                local value
                if v.idx ~= onlinePlay.playerIdx then
                    value = data.onlineKeys[name]
                elseif canSend then
                    value = data.rawKeys[name]
                end

                str = str.. onlinePlay.encodeValue("boolean",not not value)
            end

            return str
        end,
        function(v,str,start)
            local data = battlePlayer.getPlayerData(v)

            for _,name in ipairs(keysOrder) do
                data.onlineKeys[name],start = onlinePlay.decodeValue("boolean",str,start)
            end

            return start
        end,
    },

    -- Position
    {"number","width"},
    {"number","height"},

    {
        function(v)
            -- Include the absolute position of the player (may also be used as a backup)
            local output = onlinePlay.encodeValue("sint32",v.x + v.width*0.5).. onlinePlay.encodeValue("sint32",v.y + v.height)

            -- Include the player's position relative to the object that they are standing on, along with the identifier for whatever they're on
            local standingObject = getStoodOnObject(v)

            if type(standingObject) == "NPC" then
                local standingNPCUID = onlinePlayNPC.getUIDFromNPC(standingObject)

                if standingNPCUID ~= nil then
                    local relativeX = (v.x + v.width*0.5) - (standingObject.x + standingObject.width*0.5)
                    local relativeY = (v.y + v.height) - standingObject.y

                    output = output.. onlinePlay.encodeValue("uint8",1)
                    output = output.. onlinePlay.encodeValue(onlinePlayNPC.npcIdentifierType,standingNPCUID)
                    output = output.. onlinePlay.encodeValue("sint16",relativeX)
                    output = output.. onlinePlay.encodeValue("sint16",relativeY)
                else
                    output = output.. onlinePlay.encodeValue("uint8",0)
                end
            elseif type(standingObject) == "Block" then
                local relativeX = (v.x + v.width*0.5) - (standingObject.x + standingObject.width*0.5)
                local relativeY = (v.y + v.height) - standingObject.y

                output = output.. onlinePlay.encodeValue("uint8",2)
                output = output.. onlinePlay.encodeValue("uint16",standingObject.idx)
                output = output.. onlinePlay.encodeValue("sint16",relativeX)
                output = output.. onlinePlay.encodeValue("sint16",relativeY)
            else
                output = output.. onlinePlay.encodeValue("uint8",0)
            end

            return output
        end,
        function(v,str,start)
            local x,y,standingType

            x,start = onlinePlay.decodeValue("sint32",str,start)
            y,start = onlinePlay.decodeValue("sint32",str,start)

            -- Handle a position being relative to another object
            standingType,start = onlinePlay.decodeValue("uint8",str,start)

            if standingType == 1 then -- on an NPC
                local relativeX,relativeY,standingNPCUID

                standingNPCUID,start = onlinePlay.decodeValue(onlinePlayNPC.npcIdentifierType,str,start)
                relativeX,start = onlinePlay.decodeValue("sint16",str,start)
                relativeY,start = onlinePlay.decodeValue("sint16",str,start)

                local standingNPC = onlinePlayNPC.getNPCFromUID(standingNPCUID)

                if standingNPC ~= nil and standingNPC.isValid and standingNPC:mem(0x124,FIELD_BOOL) and standingNPC.heldIndex == 0 then
                    x = standingNPC.x + standingNPC.width*0.5 + relativeX
                    y = standingNPC.y + relativeY
                end
            elseif standingType == 2 then -- on a block
                local relativeX,relativeY,standingBlockIndex

                standingBlockIndex,start = onlinePlay.decodeValue("uint16",str,start)
                relativeX,start = onlinePlay.decodeValue("sint16",str,start)
                relativeY,start = onlinePlay.decodeValue("sint16",str,start)

                if standingBlockIndex >= 1 and standingBlockIndex <= Block.count() then
                    local standingBlock = Block(standingBlockIndex)

                    x = standingBlock.x + standingBlock.width*0.5 + relativeX
                    y = standingBlock.y + relativeY
                end
            end

            v.x = x - v.width*0.5
            v.y = y - v.height

            return start
        end,
    },

    {"sint8","character",1},
    {"sint8","powerup",1},
    {"sint16","reservePowerup",0},
    {"sint8","mount",0},
    {"sint8","mountColor",0},
    {"number","speedX",0},
    {"number","speedY",0},
    {"sint8","direction",-1},

    {"number",0x138,FIELD_FLOAT,0}, -- pushing force
    {"boolean",0x136,FIELD_BOOL,false}, -- pushed flag

    {"sint8",0x16,FIELD_WORD,1}, -- hearts
    {"sint16",0x140,FIELD_WORD,0}, -- invincibility timer
    {"boolean",0x142,FIELD_BOOL,false}, -- invincibility invisiblity (nice)

    {"sint8",0x11C,FIELD_WORD,0}, -- jump force
    {"boolean",0x50,FIELD_BOOL,false}, -- spin jumping
    {"number",0x118,FIELD_FLOAT,0}, -- animation timer

    {"sint8",0x160,FIELD_WORD,0}, -- cooldown timer
    {"sint8",0x162,FIELD_WORD,0}, -- cooldown timer
    {"sint8",0x164,FIELD_WORD,0}, -- tail swipe timer
    {"sint8",0x14,FIELD_WORD,0}, -- link sword timer

    {"number",0x168,FIELD_FLOAT,0}, -- p-speed
    {"boolean",0x16C,FIELD_BOOL,false}, -- has max p-speed
    {"boolean",0x16E,FIELD_BOOL,false}, -- currently flying
    {"number",0x170,FIELD_WORD,0}, -- flight time

    {"number",0x1C,FIELD_WORD,0}, -- peach float timer
    {"number",0x20,FIELD_FLOAT,0}, -- peach float speed
    {"sint8",0x24,FIELD_WORD,0}, -- peach float direction

    {"boolean",0x3C,FIELD_BOOL,false}, -- sliding on a slope
    {"boolean",0x3E,FIELD_BOOL,false}, -- sliding on a slope with enough speed to hurt NPCs/players

    {"uint8","section",0},

    {"sint16","forcedState",0},
    {"sint16","forcedTimer",0},
    {"uint16",0x15E,FIELD_WORD,0}, -- warp index

    {"boolean",0x0C,FIELD_BOOL,false}, -- fairy flag
    {"boolean",0x0E,FIELD_BOOL,false}, -- fairy used
    {"sint16",0x10,FIELD_WORD,0}, -- fairy timer
    {"boolean",0x12,FIELD_BOOL,false}, -- has a key

    {"sint8",0x10C,FIELD_WORD,0}, -- mount "special" value

    {"boolean",0x12E,FIELD_BOOL,false}, -- ducking
    {"sint8",0x26,FIELD_WORD,0}, -- plucking from above

    {"sint8",0xBA,FIELD_WORD,0}, -- yoshi'd player index

    {"boolean","noblockcollision",false},
    {"boolean","nonpcinteraction",false},
    {"boolean","noplayerinteraction",false},
    {"string","collisionGroup",""},

    -- Some general player data
    {
        function(v)
            local playerData = battlePlayer.getPlayerData(v)

            local str = onlinePlay.encodeValue("sint32",playerData.seamlessWrapOffsetX)
            str = str.. onlinePlay.encodeValue("sint32",playerData.seamlessWrapOffsetY)
            str = str.. onlinePlay.encodeValue("sint8",playerData.coins)

            return str
        end,
        function(v,str,start)
            local playerData = battlePlayer.getPlayerData(v)

            playerData.seamlessWrapOffsetX,start = onlinePlay.decodeValue("sint32",str,start)
            playerData.seamlessWrapOffsetY,start = onlinePlay.decodeValue("sint32",str,start)
            playerData.coins,start = onlinePlay.decodeValue("sint8",str,start)

            return start
        end,
    },
    -- Costumes
    {
        function(v)
            return onlinePlay.encodeValue("table",perPlayerCostumes.getAllCostumes(v.idx))
        end,
        function(v,str,start)
            local costumeList

            costumeList,start = onlinePlay.decodeValue("table",str,start)
            perPlayerCostumes.setAllCostumes(v.idx,costumeList)

            return start
        end,
    },
}



function onlinePlayPlayers.getData(p)
    local data = p.data._onlinePlay

    if data == nil then
        data = {}
        p.data._onlinePlay = data

        data.holdingNPC = nil
        data.npcGrabTime = nil

        data.previousJumpForce = p:mem(0x11C,FIELD_WORD)
        data.previousCanJump = p:mem(0x11E,FIELD_BOOL)
        data.previousCanSpinJump = p:mem(0x120,FIELD_BOOL)

        data.warpEnterCommandQueued = false
        data.pipeExitSoundQueued = false

        data.lastUpdateTick = 0
    end

    return data
end


function onlinePlayPlayers.ownsPlayer(p)
    if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE then
        return true
    end

    return (p.idx == onlinePlay.playerIdx)
end

function onlinePlayPlayers.canMakeSound(p)
    if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE then
        return true
    end

    if battleCamera.onlineFollowedPlayerIdx == p.idx or battleCamera.isOnScreen(p.x,p.y,p.width,p.height) then
        return true
    end

    return false
end

function onlinePlayPlayers.sendOwnPlayerUpdate(targetPlayerIdx)
    local p = Player(onlinePlay.playerIdx)

    playerUpdateCommand:send(targetPlayerIdx, p.idx,onlinePlay.encodeObject(p,onlinePlayPlayers.playerSendProperties),lunatime.tick())
end

function onlinePlayPlayers.sendAllPlayerUpdates(targetPlayerIdx)
    for _,p in ipairs(Player.get()) do
        if p.idx ~= targetPlayerIdx then
            playerUpdateCommand:send(targetPlayerIdx, p.idx,onlinePlay.encodeObject(p,onlinePlayPlayers.playerSendProperties))
        end
    end
end


local function canInitiateGrab(p)
    return onlinePlayPlayers.ownsPlayer(p)
    --return false
end

local function playerUsesLenientTopCollision(p,npc)
    local config = NPC.config[npc.id]

    if config.playerblock then
        return false
    end

    return (
        p.mount == MOUNT_BOOT
        or p.mount == MOUNT_YOSHI
        or p.isSpinJumping
        or (p:mem(0x44,FIELD_BOOL) and config.isshell)
        or (p.isTanookiStatue and not config.playerblocktop)
    )
end

local function getNPCHitSide(p,npc)
    local config = NPC.config[npc.id]

    local npcSpeedX = npc.speedX
    local npcSpeedY = npc.speedY

    if Defines.levelFreeze then
        npcSpeedX = 0
        npcSpeedY = 0
    end

    local topLeniency = (playerUsesLenientTopCollision(p,npc) and 16) or 10

    if (p.y + p.height - p.speedY) <= (npc.y - npcSpeedY + topLeniency) then
        if p.speedY > npcSpeedY or config.playerblocktop or Defines.levelFreeze then
            return HITSIDE_TOP
        else
            return HITSIDE_NONE
        end
    elseif (p.x - p.speedX) >= (npc.x + npc.width - npcSpeedX) then
        return HITSIDE_RIGHT
    elseif (p.x + p.width - p.speedX) <= (npc.x - npcSpeedX) then
        return HITSIDE_LEFT
    elseif (p.y - p.speedY) >= (npc.y + npc.height - npcSpeedY) then
        return HITSIDE_BOTTOM
    end

    return HITSIDE_UNKNOWN
end

local function canGrabNPCFromSide(p,npc,hitSide)
    local config = NPC.config[npc.id]

    if not (config.isshell and Defines.levelFreeze) then
        if npc.forcedState == NPCFORCEDSTATE_DROPPED_ITEM then
            if config.isinteractable and not config.grabside then
                return false
            end

            return true
        elseif npc.forcedState == NPCFORCEDSTATE_NONE then
            if not config.grabside then
                return false
            end
        else
            return false
        end
    end

    if npc.id == 22 or npc.id == 49 then -- Billy Gun and Toothy
        return true
    end

    if config.isvegetable and npc:mem(0x130,FIELD_WORD) ~= p.idx then
        return true
    end

    return (hitSide == HITSIDE_RIGHT and p.direction == DIR_LEFT) or (hitSide == HITSIDE_LEFT and p.direction == DIR_RIGHT)
end

local function shouldGrabNPC(p,npc)
    if npc.despawnTimer <= 0 or npc.isGenerator or npc.killFlag > 0 then
        return false
    end

    if npc.forcedState == NPCFORCEDSTATE_YOSHI_TONGUE or npc.forcedState == NPCFORCEDSTATE_YOSHI_MOUTH then
        return false
    end

    if p.standingNPC == npc or p.nonpcinteraction or not Misc.canCollideWith(p,npc) then
        return false
    end

    local config = NPC.config[npc.id]

    local hitSide = getNPCHitSide(p,npc)

    if hitSide == HITSIDE_NONE then
        return false
    end

    -- Some bits of slightly weird battle mode-specific logic
    if npc.heldIndex > 0 and npc.heldIndex ~= p.idx then
        if config.nohurt then
            return false
        else
            hitSide = HITSIDE_UNKNOWN
        end
    end

    if npc.isProjectile and npc:mem(0x132,FIELD_WORD) ~= p.idx and npc:mem(0x130,FIELD_WORD) ~= p.idx then
        if config.isshell and npc.speedX == 0 and p.isSpinJumping and hitSide == HITSIDE_TOP then
            return false
        end

        if p.invincibilityTimer == 0 and (nonHarmfulProjectileNPCIDs[npc.id] or ((config.isshell or npc.id == 45) and npc.speedX == 0)) then
            return false
        end
    end

    -- Top-side collision
    if hitSide == HITSIDE_TOP then
        if config.playerblocktop or (p:mem(0x44,FIELD_BOOL) and config.isshell) then
            return false
        end

        if p:mem(0x58,FIELD_BOOL) and not config.nohurt then -- sliding down a slope
            return false
        end

        if p:mem(0x42,FIELD_WORD) > 0 and npc:mem(0x130,FIELD_WORD) == p.idx then -- unused "NoShellKick"
            return false
        end

        if p.isSpinJumping then
            return false
        end

        if config.isinteractable then
            return false
        end

        -- Grabbing a shell from the top
        if config.isshell and npc.speedX == 0 then
            return true
        end

        return false
    end

    -- grabside NPCs
    if canGrabNPCFromSide(p,npc,hitSide) then
        return true
    end

    -- Shells
    --Misc.dialog("A")
    if (config.isshell or (npc.id == 45 and npc.ai1 == 1)) and npc.speedX == 0 and npc.speedY >= 0 and npc:mem(0x130,FIELD_WORD) ~= p.idx then
        return true
    end

    return false
end

local function handleGrabbingNPCs(p)
    if p.mount ~= MOUNT_NONE or p.character == CHARACTER_LINK or p.forcedState ~= FORCEDSTATE_NONE or p:mem(0x154,FIELD_WORD) ~= 0 or p.isTanookiStatue then
        return
    end

    if p.grabTopTimer > 0 then
        return
    end

    if not p.keys.run then
        return
    end

    for _,npc in NPC.iterateIntersecting(p.x + p.speedX - 32,p.y + p.speedY - 32,p.x + p.speedX + p.width + 32,p.y + p.speedY + p.height + 32) do
        if Colliders.speedCollide(p,npc) and shouldGrabNPC(p,npc) then
            local config = NPC.config[npc.id]

            npc.direction = p.direction

            p:mem(0x154,FIELD_WORD,npc.idx + 1)

            npc.heldIndex = p.idx
            npc:mem(0x12E,FIELD_WORD,Defines.npc_throwfriendlytimer)
            npc:mem(0x130,FIELD_WORD,p.idx)

            -- Forcibly unduck
            if (p.character == CHARACTER_MARIO or p.character == CHARACTER_LUIGI) and p.isDucking then
                if p.speedY ~= 0 then
                    p:mem(0x132,FIELD_BOOL,true)
                end

                p:mem(0x134,FIELD_BOOL,true)
                p.isDucking = false
            end

            -- Play sound
            if not config.isshell or (p.character == CHARACTER_PEACH or p.character == CHARACTER_TOAD) then
                if config.isvegetable then
                    SFX.play(73)
                else
                    SFX.play(23)
                end
            end

            break
        end
    end
end


local function handleHeldNPCs(p)
    local data = onlinePlayPlayers.getData(p)

    if p.idx ~= onlinePlay.playerIdx then
        data.holdingNPC = nil
        return
    end

    local holdingNPC = p.holdingNPC

    if data.holdingNPC == holdingNPC then
        return
    end

    -- Releasing an NPC
    if data.holdingNPC ~= nil and data.holdingNPC.isValid then
        local npcUID = onlinePlayNPC.getUIDFromNPC(data.holdingNPC)

        releaseNPCCommand:send(0, npcUID,onlinePlay.encodeObject(data.holdingNPC,onlinePlayNPC.npcSendProperties))
    end

    -- Grabbing an NPC
    if holdingNPC ~= nil then
        local npcUID = onlinePlayNPC.getUIDFromNPC(holdingNPC)
        if npcUID == nil then
            return
        end

        if onlinePlay.currentMode == onlinePlay.MODE_HOST then
            onlinePlayNPC.assignNPCToOwner(holdingNPC,p.idx)
        end

        data.npcGrabTime = onlinePlay.syncedTime
        grabNPCCommand:send(0, npcUID,data.npcGrabTime,onlinePlay.encodeObject(holdingNPC,onlinePlayNPC.npcSendProperties))
    end

    data.holdingNPC = holdingNPC
end

local function shouldMakeSkiddingSound(p)
    if p.speedY ~= 0 and p:mem(0x176,FIELD_WORD) == 0 and p:mem(0x48,FIELD_WORD) == 0 then -- in the air
        return false
    end

    if p.character == CHARACTER_LINK then
        return false
    end

    if p.forcedState ~= FORCEDSTATE_NONE
    or p:mem(0x12E,FIELD_BOOL) -- ducking
    or p:mem(0x4A,FIELD_BOOL) -- tanooki statue
    or p:mem(0x3C,FIELD_BOOL) -- sliding on a slope
    or p:mem(0x0C,FIELD_BOOL) -- fairy
    or p:mem(0x26,FIELD_WORD) > 0 -- grabbing an NPC from the top
    or p.climbing
    then
        return false
    end


    if p.speedX > 0 and (p.keys.left or (p.direction == DIR_LEFT and p:mem(0x136,FIELD_BOOL))) then
        return true
    elseif p.speedX < 0 and (p.keys.right or (p.direction == DIR_RIGHT and p:mem(0x136,FIELD_BOOL))) then
        return true
    end

    return false
end


local doorBGOEffectMap = {
    [88] = 54,
    [87] = 55,
    [107] = 59,
    [141] = 103,
}

local function spawnDoorEffects(warp)
    local entranceCollider = Colliders.Box(warp.entranceX,warp.entranceY,warp.entranceWidth,warp.entranceHeight)
    local exitCollider = Colliders.Box(warp.exitX,warp.exitY,warp.exitWidth,warp.exitHeight)

    for _,bgo in BGO.iterateByFilterMap(doorBGOEffectMap) do
        local bgoCollider = Colliders.Box(bgo.x,bgo.y,bgo.width,bgo.height)

        if bgoCollider:collide(entranceCollider) or bgoCollider:collide(exitCollider) then
            local effectID = doorBGOEffectMap[bgo.id]

            local doorEffect = Effect.spawn(effectID,bgo.x,bgo.y)

            -- Door effects inherit the size of their respective BGO
            doorEffect.width = bgo.width
            doorEffect.height = bgo.height

            -- Except, like a language, Redigit has an exception to every rule
            if bgo.id == 141 then
                doorEffect.width = 104
                doorEffect.x = doorEffect.x + (bgo.width - doorEffect.width)*0.5
            end
        end
    end
end


local shootyPowerups = table.map{PLAYER_FIREFLOWER,PLAYER_HAMMER,PLAYER_ICE}
local shootyMounts = table.map{MOUNT_NONE,MOUNT_BOOT}

local characterJumpForceBonuses = {
    [CHARACTER_LUIGI] = 3,
}

local function handlePlayer(p)
    local data = onlinePlayPlayers.getData(p)

    if canInitiateGrab(p) then
        handleGrabbingNPCs(p)
    else
        if characterGrabTopTime[p.character] ~= nil then
            p.grabTopTimer = math.min(p.grabTopTimer,characterGrabTopTime[p.character] - 1)
        end
    end

    handleHeldNPCs(p)

    if p.idx == onlinePlay.playerIdx then
        -- When the player jumps, send a command for it
        -- You'd think that detecting this would be simpler... (no, onPlayerJump does not work)
        local expectedJumpForce = Defines.jumpheight + (characterJumpForceBonuses[p.character] or 0)
        local expectedSpinJumpForce = Defines.jumpheight - 6 + (characterJumpForceBonuses[p.character] or 0)

        if data.previousJumpForce == 0 and data.previousCanJump and not p:mem(0x11E,FIELD_BOOL) and p:mem(0x11C,FIELD_WORD) == (expectedJumpForce - 1) then
            jumpedCommand:send(0, false,onlinePlay.encodeObject(p,onlinePlayPlayers.playerSendProperties),lunatime.tick())
        elseif data.previousJumpForce == 0 and data.previousCanSpinJump and not p:mem(0x120,FIELD_BOOL) and p:mem(0x11C,FIELD_WORD) == (expectedSpinJumpForce - 1) then
            jumpedCommand:send(0, true,onlinePlay.encodeObject(p,onlinePlayPlayers.playerSendProperties),lunatime.tick())
        end

        -- Warp command
        if data.warpEnterCommandQueued then
            warpEnterCommand:send(0, data.lastUsedWarpIdx,onlinePlay.encodeObject(p,onlinePlayPlayers.playerSendProperties))
            data.warpEnterCommandQueued = false
        end
    else
        if shootyPowerups[p.powerup] and shootyMounts[p.mount] then
            if p.character == CHARACTER_LINK then
                p:mem(0x162,FIELD_WORD,3) -- disable Link's projectiles
            else
                p:mem(0x160,FIELD_WORD,3) -- disable projectiles
            end
        end

        p:mem(0xBC,FIELD_WORD,3) -- disable mounting

        p:mem(0x11E,FIELD_BOOL,false) -- prevent jumping (so the jump sound doesn't play)
        p:mem(0x120,FIELD_BOOL,false) -- prevent spin jumping and dismounting

        p:mem(0x18,FIELD_BOOL,false) -- disable peach float
        p:mem(0x1A,FIELD_BOOL,false)

        -- Prevent warping
        if p.forcedState == FORCEDSTATE_PIPE then
            if data.pipeExitSoundQueued and p.forcedTimer == 2 then
                if onlinePlayPlayers.canMakeSound(p) then
                    SFX.play(17)
                end

                data.pipeExitSoundQueued = false
            end

            if p.forcedTimer >= 100 then
                p.forcedTimer = math.min(p.forcedTimer,108)
            end
        elseif p.forcedState == FORCEDSTATE_DOOR then
            p.forcedTimer = math.min(p.forcedTimer,27)
        else
            data.pipeExitSoundQueued = false
        end

        --p.warpCooldown = 2  -- disable warping
    end

    data.previousJumpForce = p:mem(0x11C,FIELD_WORD)
    data.previousCanJump = p:mem(0x11E,FIELD_BOOL)
    data.previousCanSpinJump = p:mem(0x120,FIELD_BOOL)

    -- Skidding sounds
    if onlinePlayPlayers.canMakeSound(p) and shouldMakeSkiddingSound(p) then
        SFX.play{sound = 10,delay = 8}
    end
end


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

    for _,p in ipairs(Player.get()) do
        handlePlayer(p)
    end
end


function onlinePlayPlayers.onWarpEnter(eventObj,warp,p)
    if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE then
        return
    end

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

function onlinePlayPlayers.onPostWarpEnter(warp,p)
    if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE then
        return
    end

    local data = onlinePlayPlayers.getData(p)

    if p.idx == onlinePlay.playerIdx then
        data.warpEnterCommandQueued = true
        data.lastUsedWarpIdx = warp.idx
    end
end


function onlinePlayPlayers.initCommands()
    playerUpdateCommand = onlinePlay.createCommand("_player_update",onlinePlay.IMPORTANCE_MINOR)

    function playerUpdateCommand.onReceive(sourcePlayerIdx, playerIdx,propertiesString,updateTick)
        -- Return if this isn't a valid update
        if sourcePlayerIdx ~= onlinePlay.hostPlayerIdx and playerIdx ~= sourcePlayerIdx then
            return
        end

        if playerIdx == onlinePlay.playerIdx or (playerIdx < 1 or playerIdx > Player.count()) then
            return
        end

        -- Don't allow it if it's older than the last update we got
        local p = Player(playerIdx)
        local data = onlinePlayPlayers.getData(p)

        if updateTick ~= nil and updateTick < data.lastUpdateTick then
            return
        end

        -- Set properties
        onlinePlay.decodeObject(p,propertiesString,1,onlinePlayPlayers.playerSendProperties)

        data.lastUpdateTick = updateTick or data.lastUpdateTick
    end

    -- Commands for jumping
    jumpedCommand = onlinePlay.createCommand("_player_jump",onlinePlay.IMPORTANCE_MINOR)

    function jumpedCommand.onReceive(sourcePlayerIdx, isSpinJump,propertiesString,updateTick)
        local p = Player(sourcePlayerIdx)
        local data = onlinePlayPlayers.getData(p)

        -- Set properties
        if updateTick == nil or updateTick >= data.lastUpdateTick then
            onlinePlay.decodeObject(p,propertiesString,1,onlinePlayPlayers.playerSendProperties)
            data.lastUpdateTick = updateTick or data.lastUpdateTick
        end

        -- Make sound
        if onlinePlayPlayers.canMakeSound(p) then
            if isSpinJump then
                SFX.play(33)
            else
                SFX.play(1)
            end
        end
    end


    -- Grabbing/releasing held NPCs
    grabNPCCommand = onlinePlay.createCommand("_player_grabNPC",onlinePlay.IMPORTANCE_MAJOR)
    releaseNPCCommand = onlinePlay.createCommand("_player_releaseNPC",onlinePlay.IMPORTANCE_MAJOR)

    function grabNPCCommand.onReceive(sourcePlayerIdx, npcUID,grabTime,npcProperties)
        local npc = onlinePlayNPC.getNPCFromUID(npcUID)
        if npc == nil then
            return
        end
        
        local oldPlayerIdx = npc:mem(0x12C,FIELD_WORD)

        if oldPlayerIdx > 0 and oldPlayerIdx <= Player.count() then
            local oldPlayer = Player(oldPlayerIdx)
            local oldPlayerData = onlinePlayPlayers.getData(oldPlayer)

            -- This player already grabbed the NPC earlier than the new player
            if oldPlayer.holdingNPC == npc and oldPlayerData.npcGrabTime ~= nil and oldPlayerData.npcGrabTime <= grabTime then
                return
            end

            -- This player was holding the NPC, but has now been succeeded
            oldPlayer:mem(0x154,FIELD_WORD,0) -- reset held NPC
            npc:mem(0x12C,FIELD_WORD,0) -- reset holding player

            oldPlayerData.npcGrabTime = nil
        end

        local newPlayer = Player(sourcePlayerIdx)
        local newPlayerData = onlinePlayPlayers.getData(newPlayer)

        if onlinePlay.currentMode == onlinePlay.MODE_HOST then
            onlinePlayNPC.assignNPCToOwner(npc,newPlayer.idx)
        end
        
        newPlayer:mem(0x154,FIELD_WORD,npc.idx + 1)
        npc:mem(0x12C,FIELD_WORD,newPlayer.idx)

        newPlayerData.npcGrabTime = grabTime

        onlinePlay.decodeObject(npc,npcProperties,1,onlinePlayNPC.npcSendProperties)
    end

    function releaseNPCCommand.onReceive(sourcePlayerIdx, npcUID,npcProperties)
        local npc = onlinePlayNPC.getNPCFromUID(npcUID)
        if npc == nil then
            return
        end
        
        local p = Player(sourcePlayerIdx)

        if p.holdingNPC ~= npc or npc:mem(0x12C,FIELD_WORD) ~= p.idx then
            return
        end

        local data = onlinePlayPlayers.getData(p)

        p:mem(0x154,FIELD_WORD,0) -- reset held NPC
        npc:mem(0x12C,FIELD_WORD,0) -- reset holding player

        data.npcGrabTime = nil

        onlinePlay.decodeObject(npc,npcProperties,1,onlinePlayNPC.npcSendProperties)
    end


    -- Using a warp
    warpEnterCommand = onlinePlay.createCommand("_player_warpEnter",onlinePlay.IMPORTANCE_MAJOR)

    function warpEnterCommand.onReceive(sourcePlayerIdx, warpIdx,propertiesString)
        local p = Player(sourcePlayerIdx)
        local data = onlinePlayPlayers.getData(p)

        -- Set properties
        onlinePlay.decodeObject(p,propertiesString,1,onlinePlayPlayers.playerSendProperties)

        -- Do appropriate effects given the warp type
        local warp = Warp(warpIdx)

        if warp.warpType == 1 then
            -- Pipe
            if onlinePlayPlayers.canMakeSound(p) then
                SFX.play(17)
            end

            data.pipeExitSoundQueued = true
        elseif warp.warpType == 2 then
            -- Door
            if onlinePlayPlayers.canMakeSound(p) then
                SFX.play(46)
            end

            spawnDoorEffects(warp)
        end
    end


    -- Join packet
    playersJoinPacket = onlinePlay.createJoinPacket("_players",1)

    function playersJoinPacket.encodeData(targetPlayerIdx)
        local data = ""

        for _,p in ipairs(Player.get()) do
            if p.idx ~= targetPlayerIdx then
                data = data.. onlinePlay.encodeValue("uint8",p.idx).. onlinePlay.encodeObject(p,onlinePlayPlayers.playerSendProperties)

                local holdingNPC = p.holdingNPC

                if holdingNPC ~= nil and onlinePlayNPC.getUIDFromNPC(holdingNPC) ~= nil then
                    data = data.. onlinePlay.encodeValue(onlinePlayNPC.npcIdentifierType,onlinePlayNPC.getUIDFromNPC(holdingNPC))
                else
                    data = data.. onlinePlay.encodeValue(onlinePlayNPC.npcIdentifierType,-1)
                end
            end
        end
 
        return data
    end

    function playersJoinPacket.onReceive(data)
        local pos = 1

        while (pos <= #data) do
            local playerIdx,holdingNPCUID

            playerIdx,pos = onlinePlay.decodeValue("uint8",data,pos)

            local p = Player(playerIdx)

            pos = onlinePlay.decodeObject(p,data,pos,onlinePlayPlayers.playerSendProperties)

            local playerData = onlinePlayPlayers.getData(p)

            -- Set up held NPC
            holdingNPCUID,pos = onlinePlay.decodeValue(onlinePlayNPC.npcIdentifierType,data,pos)

            local holdingNPC = onlinePlayNPC.getNPCFromUID(holdingNPCUID)

            if holdingNPC ~= nil then
                p:mem(0x154,FIELD_WORD,holdingNPC.idx + 1)
                holdingNPC.heldIndex = p.idx

                playerData.holdingNPC = holdingNPC
            else
                p:mem(0x154,FIELD_WORD,0)

                playerData.holdingNPC = nil
            end
        end
    end
end


function onlinePlayPlayers.onInitAPI()
    onlinePlay = require("scripts/onlinePlay")
    onlinePlayNPC = require("scripts/onlinePlay_npc")
    onlineChat = require("scripts/onlineChat")

    battlePlayer = require("scripts/battlePlayer")
    battleCamera = require("scripts/battleCamera")
    battleMenu = require("scripts/battleMenu")

    registerEvent(onlinePlayPlayers,"onTick")
    registerEvent(onlinePlayPlayers,"onWarpEnter")
    registerEvent(onlinePlayPlayers,"onPostWarpEnter")

    onlinePlayPlayers.initCommands()


    function onlinePlay.onInitialise()
        Audio.sounds[10].muted = true -- skidding sound

        Defines.player_grabSideEnabled = false
        Defines.player_grabShellEnabled = false
    end

    function onlinePlay.onUninitialise()
        Audio.sounds[10].muted = false -- skidding sound

        for _,p in ipairs(Player.get()) do
            p.data._onlinePlay = nil
        end

        Defines.player_grabSideEnabled = true
        Defines.player_grabShellEnabled = true
    end

    function onlinePlay.onDisconnect(playerIdx)
        local p = Player(playerIdx)

        p.data._onlinePlay = nil
    end
end


return onlinePlayPlayers