-- Initial speed of a box.
local SPEED = 7
-- Acceleration of a box.
local ACCEL = 0.1
-- Elevator interface/database version.
local VERSION = 8
-- Maximum time a box can go without players nearby.
local PTIMEOUT = 120

-- Localize for performance.
local vector_distance = vector.distance
local vector_round = vector.round
local math_floor = math.floor
local math_random = math.random
local math_min = math.min
local math_max = math.max

-- Detect optional mods.
local technic_path = minetest.get_modpath("technic")
local chains_path = minetest.get_modpath("chains")
local homedecor_path = minetest.get_modpath("homedecor")
local armor_path = minetest.get_modpath("3d_armor")

-- Central "network" table.
local elevator = {
    motors = {},
}

local str = minetest.get_mod_storage and minetest.get_mod_storage()

local elevator_file = minetest.get_worldpath() .. "/elevator"

local function load_elevator()
    if str and ((str.contains and str:contains("data")) or (str:get_string("data") and str:get_string("data") ~= "")) then
        elevator = minetest.deserialize(str:get_string("data"))
        return
    end
    local file = io.open(elevator_file)
    if file then
        elevator = minetest.deserialize(file:read("*all")) or {}
        file:close()
    end
end

local function save_elevator()
    if str then
        str:set_string("data", minetest.serialize(elevator))
        return
    end
    local f = io.open(elevator_file, "w")
    f:write(minetest.serialize(elevator))
    f:close()
end

load_elevator()

-- Elevator boxes in action.
local boxes = {}
-- Player formspecs.
local formspecs = {}
-- Player near box timeout.
local lastboxes = {}
-- Players riding boxes.
local riding = {}
-- Globalstep timer.
local time = 0

-- Helper function to read unloaded nodes.
local function get_node(pos)
    local node = minetest.get_node_or_nil(pos)
    if node then return node end
    local _,_ = VoxelManip():read_from_map(pos, pos)
    return minetest.get_node_or_nil(pos)
end

-- Use homedecor's placeholder if possible.
local placeholder = homedecor_path and "homedecor:expansion_placeholder" or "elevator:placeholder"
if homedecor_path then
    minetest.register_alias("elevator:placeholder", "homedecor:expansion_placeholder")
else
    -- Placeholder node, in the style of homedecor.
    minetest.register_node(placeholder, {
        description = "Expansion Placeholder",
        selection_box = {
            type = "fixed",
            fixed = {0, 0, 0, 0, 0, 0},
        },
        groups = {
            not_in_creative_inventory=1
        },
        drawtype = "airlike",
        paramtype = "light",
        sunlight_propagates = true,

        walkable = false,
        buildable_to = false,
        is_ground_content = false,

        on_dig = function(pos, node, player)
            minetest.remove_node(pos)
            minetest.set_node(pos, {name=placeholder})
        end
    })
end

local VISUAL_INCREASE = 1.75

-- Cause <sender> to ride <motorhash> beginning at <pos> and targetting <target>.
local function create_box(motorhash, pos, target, sender)
    -- First create the box.
    local obj = minetest.add_entity(pos, "elevator:box")
    obj:setpos(pos)
    -- Attach the player.
    sender:setpos(pos)
    sender:set_attach(obj, "", {x=0, y=9, z=0}, {x=0, y=0, z=0})

    pova.set_modifier(sender, "eye_offset", {{x=0, y=-9, z=0},{x=0, y=-9, z=0}}, "elevator")
    pova.set_modifier(sender, "properties", {visual_size = {x=VISUAL_INCREASE, y=VISUAL_INCREASE}}, "elevator")

    if armor_path then
        armor:update_player_visuals(sender)
    end
    -- Set the box properties.
    obj:get_luaentity().motor = motorhash
    obj:get_luaentity().uid = math_floor(math_random() * 1000000)
    obj:get_luaentity().attached = sender:get_player_name()
    obj:get_luaentity().start = pos
    obj:get_luaentity().target = target
    obj:get_luaentity().halfway = {x=pos.x, y=(pos.y+target.y)/2, z=pos.z}
    obj:get_luaentity().vmult = (target.y < pos.y) and -1 or 1
    -- Set the speed.
    obj:set_velocity({x=0, y=SPEED*obj:get_luaentity().vmult, z=0})
    obj:set_acceleration({x=0, y=ACCEL*obj:get_luaentity().vmult, z=0})
    -- Set the tables.
    boxes[motorhash] = obj
    riding[sender:get_player_name()] = {
        motor = motorhash,
        pos = pos,
        target = target,
        box = obj,
    }
    return obj
end

-- Try to teleport player away from any closed (on) elevator node.
local function teleport_player_from_elevator(player)
    local function solid(pos)
        if not minetest.registered_nodes[minetest.get_node(pos).name] then
            return true
        end
        return minetest.registered_nodes[minetest.get_node(pos).name].walkable
    end
    local pos = vector_round(player:get_pos())
    local node = minetest.get_node(pos)
    -- elevator_off is like a shaft, so the player would already be falling.
    if node.name == "elevator:elevator_on" then
        local front = vector.subtract(pos, minetest.facedir_to_dir(node.param2))
        local front_above = vector.add(front, {x=0, y=1, z=0})
        local front_below = vector.subtract(front, {x=0, y=1, z=0})
        -- If the front isn't solid, it's ok to teleport the player.
        if not solid(front) and not solid(front_above) then
            player:setpos(front)
        end
    end
end

minetest.register_globalstep(function(dtime)
    -- Don't want to run this too often.
    time = time + dtime
    if time < 0.5 then
        return
    end
    time = 0
    -- Only count riders who are still logged in.
    local newriding = {}
    for _,p in ipairs(minetest.get_connected_players()) do
        local pos = p:get_pos()
        local name = p:get_player_name()
        newriding[name] = riding[name]
        -- If the player is indeed riding, update their position.
        if newriding[name] then
            newriding[name].pos = pos
        end
    end
    riding = newriding
    for name,r in pairs(riding) do
        -- If the box is no longer loaded or existent, create another.
        local ok = r.box and r.box.get_pos and r.box:get_pos() and r.box:get_luaentity() and r.box:get_luaentity().attached == name
        if not ok then
            minetest.log("action", "[elevator] "..minetest.pos_to_string(r.pos).." created due to lost rider.")
            minetest.after(0, create_box, r.motor, r.pos, r.target, minetest.get_player_by_name(name))
        end
    end
    -- Ensure boxes are deleted after <PTIMEOUT> seconds if there are no players nearby.
    for motor,obj in pairs(boxes) do
        if type(obj) ~= "table" then
            return
        end
        lastboxes[motor] = lastboxes[motor] and math_min(lastboxes[motor], PTIMEOUT) or PTIMEOUT
        lastboxes[motor] = math_max(lastboxes[motor] - 1, 0)
        local pos = obj:get_pos()
        if pos then
            for _,object in ipairs(minetest.get_objects_inside_radius(pos, 5)) do
                if object.is_player and object:is_player() then
                    lastboxes[motor] = PTIMEOUT
                    break
                end
            end
            if lastboxes[motor] < 1 then
                minetest.log("action", "[elevator] "..minetest.pos_to_string(pos).." broke due to lack of players.")
                boxes[motor] = false
            end
        else
            minetest.log("action", "[elevator] "..minetest.pos_to_string(pos).." broke due to lack of position during player check.")
            boxes[motor] = false
        end
    end
end)

minetest.register_on_leaveplayer(function(player)
    -- We don't want players potentially logging into open elevators.
    teleport_player_from_elevator(player)
end)

local function phash(pos)
    return minetest.pos_to_string(pos)
end

local function punhash(pos)
    return minetest.string_to_pos(pos)
end

-- Starting from <pos>, locate a motor hash.
local function locate_motor(pos)
    local p = vector.new(pos)
    while true do
        local node = get_node(p)
        if node.name == "elevator:elevator_on" or node.name == "elevator:elevator_off" then
            p.y = p.y + 2
        elseif node.name == "elevator:shaft" then
            p.y = p.y + 1
        elseif node.name == "elevator:motor" then
            return phash(p)
        else
            return nil
        end
    end
end

local function build_motor(hash)
    local need_saving = false
    local motor = elevator.motors[hash]
    -- Just ignore motors that don't exist.
    if not motor then
        return
    end
    local p = punhash(hash)
    local node = get_node(p)
    -- And ignore motors that aren't motors.
    if node.name ~= "elevator:motor" then
        return
    end
    p.y = p.y - 1
    motor.elevators = {}
    motor.pnames = {}
    motor.labels = {}
    -- Run down through the shaft, storing information about elevators.
    while true do
        local node = get_node(p)
        if node.name == "elevator:shaft" then
            p.y = p.y - 1
        else
            p.y = p.y - 1
            local node = get_node(p)
            if node.name == "elevator:elevator_on" or node.name == "elevator:elevator_off" then
                table.insert(motor.elevators, phash(p))
                table.insert(motor.pnames, tostring(p.y))
                table.insert(motor.labels, "")
                p.y = p.y - 1
                need_saving = true
            else
                break
            end
        end
    end
    -- Set the elevators fully.
    for i,m in ipairs(motor.elevators) do
        local pos = punhash(m)
        local meta = minetest.get_meta(pos)
        meta:set_int("version", VERSION)
        if meta:get_string("motor") ~= hash then
            build_motor(meta:get_string("motor"))
        end
        motor.labels[i] = meta:get_string("label")
        meta:set_string("motor", hash)
        if motor.labels[i] ~= meta:get_string("infotext") then
            meta:set_string("infotext", motor.labels[i])
        end
    end
    if need_saving then
        save_elevator()
    end
end

local function unbuild(pos, add)
    local need_saving = false
    local p = table.copy(pos)
    p.y = p.y - 1
    -- Loop down through the network, set any elevators below this to the off position.
    while true do
        local node = get_node(p)
        if node.name == "elevator:shaft" then
            p.y = p.y - 1
        else
            p.y = p.y - 1
            local node = get_node(p)
            if node.name == "elevator:elevator_on" or node.name == "elevator:elevator_off" then
                local meta = minetest.get_meta(p)
                meta:set_string("motor", "")
                p.y = p.y - 1
            else
                break
            end
        end
    end
    -- After a short delay, build the motor and handle box removal.
    minetest.after(0.01, function(p2, add)
        if not p2 or not add then
            return
        end
        p2.y = p2.y + add
        local motorhash = locate_motor(p2)
        build_motor(motorhash)
        -- If there's a box below this point, break it.
        if boxes[motorhash] and boxes[motorhash]:get_pos() and p2.y >= boxes[motorhash]:get_pos().y then
            boxes[motorhash] = nil
        end
        -- If the box does not exist, just clear it.
        if boxes[motorhash] and not boxes[motorhash]:get_pos() then
            boxes[motorhash] = nil
        end
    end, table.copy(pos), add)
end

minetest.register_node("elevator:motor", {
    description = "Elevator Motor",
    tiles = {
        "default_steel_block.png",
        "default_steel_block.png",
        "elevator_motor.png",
        "elevator_motor.png",
        "elevator_motor.png",
        "elevator_motor.png",
    },
    groups = {cracky=1},
    sounds = default.node_sound_stone_defaults(),
    after_place_node = function(pos, placer, itemstack)
        -- Set up the motor table.
        elevator.motors[phash(pos)] = {
            elevators = {},
            pnames = {},
            labels = {},
        }
        save_elevator()
        build_motor(phash(pos))
    end,
    on_destruct = function(pos)
        -- Destroy everything related to this motor.
        boxes[phash(pos)] = nil
        elevator.motors[phash(pos)] = nil
        save_elevator()
    end,
})

for _,mode in ipairs({"on", "off"}) do
    local nodename = "elevator:elevator_"..mode
    local on = (mode == "on")
    local box
    local cbox
    if on then
        -- Active elevators have a ceiling and floor.
        box = {

            { 0.48, -0.5,-0.5,  0.5,  1.5, 0.5},
            {-0.5 , -0.5, 0.48, 0.48, 1.5, 0.5},
            {-0.5,  -0.5,-0.5 ,-0.48, 1.5, 0.5},

            { -0.5,-0.5,-0.5,0.5,-0.48, 0.5},
            { -0.5, 1.45,-0.5,0.5, 1.5, 0.5},
        }
        cbox = table.copy(box)
        -- But you can enter them from the top.
        cbox[5] = nil
    else
        -- Inactive elevators are almost like shafts.
        box = {

            { 0.48, -0.5,-0.5,  0.5,  1.5, 0.5},
            {-0.5 , -0.5, 0.48, 0.48, 1.5, 0.5},
            {-0.5,  -0.5,-0.5 ,-0.48, 1.5, 0.5},
            {-0.5 , -0.5, -0.48, 0.5, 1.5, -0.5},
        }
        cbox = box
    end
    minetest.register_node(nodename, {
        description = "Elevator",
        drawtype = "nodebox",
        sunlight_propagates = false,
        paramtype = "light",
        paramtype2 = "facedir",
        on_rotate = screwdriver.disallow,

        selection_box = {
                type = "fixed",
                fixed = box,
        },

        collision_box = {
                type = "fixed",
                fixed = cbox,
        },

        node_box = {
                type = "fixed",
                fixed = box,
        },

        tiles = on and {
                "default_steel_block.png",
                "default_steel_block.png",
                "elevator_box.png",
                "elevator_box.png",
                "elevator_box.png",
                "elevator_box.png",
        } or {
                "elevator_box.png",
                "elevator_box.png",
                "elevator_box.png",
                "elevator_box.png",
                "elevator_box.png",
                "elevator_box.png",
        },
        groups = {cracky=1, choppy=1, snappy=1},
        drop = "elevator:elevator_off",

        -- Emit a bit of light when active.
        light_source = (on and 4 or nil),

        after_place_node  = function(pos, placer, itemstack)
            local meta = minetest.get_meta(pos)
            meta:set_int("version", VERSION)

            -- Add a placeholder to avoid nodes being placed in the top.
            local p = vector.add(pos, {x=0, y=1, z=0})
            local p2 = minetest.dir_to_facedir(placer:get_look_dir())
            minetest.set_node(p, {name=placeholder, paramtype2="facedir", param2=p2})

            -- Try to build a motor above.
            local motor = locate_motor(pos)
            if motor then
                build_motor(motor)
            end
        end,

        after_dig_node = function(pos, node, meta, digger)
            unbuild(pos, 2)
        end,

        on_place = function(itemstack, placer, pointed_thing)
            local pos  = pointed_thing.above
            local node = minetest.get_node(vector.add(pos, {x=0, y=1, z=0}))
            if (node ~= nil and node.name ~= "air" and node.name ~= placeholder) then
                return
            end
            return minetest.item_place(itemstack, placer, pointed_thing)
        end,

        on_rightclick = function(pos, node, sender)
            if not sender or not sender:is_player() then
                return
            end
            local formspec
            local meta = minetest.get_meta(pos)
            formspecs[sender:get_player_name()] = {pos}
            if on then
                if vector_distance(sender:get_pos(), pos) > 1 or minetest.get_node(sender:get_pos()).name ~= nodename then
                    minetest.chat_send_player(sender:get_player_name(), "# Server: You are not inside the booth.")
                    return
                end
                -- Build the formspec from the motor table.
                local tpnames = {}
                local tpnames_l = {}
                local motorhash = meta:get_string("motor")
                local motor = elevator.motors[motorhash]
                for ji,jv in ipairs(motor.pnames) do
                    if tonumber(jv) ~= pos.y then
                        table.insert(tpnames, jv)
												local jy = jv -- Is a string.
												local rp = rc.pos_to_realmpos({x=pos.x, y=tonumber(jv), z=pos.z})
												if rp then
													jy = tostring(rp.y)
												end
												local ename = (motor.labels[ji] and motor.labels[ji] ~= "") and (jy .. " - " .. minetest.formspec_escape(motor.labels[ji])) or jy
												if tonumber(jv) > pos.y then
													ename = ename .. " - Up"
												else
													ename = ename .. " - Down"
												end
                        table.insert(tpnames_l, ename)
                    end
                end
                formspecs[sender:get_player_name()] = {pos, tpnames}
                if #tpnames > 0 then
                    if not minetest.test_protection(pos, sender:get_player_name()) then
                        formspec = "size[4,6]" .. default.gui_bg .. default.gui_bg_img .. default.gui_slots
                        .."label[0,0;Click once to travel.]"
                        .."textlist[-0.1,0.5;4,4;target;"..table.concat(tpnames_l, ",").."]"
                        .."field[0.25,5.25;4,0;label;;"..minetest.formspec_escape(meta:get_string("label")).."]"
                        .."button_exit[-0.05,5.5;4,1;setlabel;Set label]"
                    else
                        formspec = "size[4,4.4]" .. default.gui_bg .. default.gui_bg_img .. default.gui_slots
                        .."label[0,0;Click once to travel.]"
                        .."textlist[-0.1,0.5;4,4;target;"..table.concat(tpnames_l, ",").."]"
                    end
                else
                    if not minetest.test_protection(pos, sender:get_player_name()) then
                        formspec = "size[4,2]" .. default.gui_bg .. default.gui_bg_img .. default.gui_slots
                        .."label[0,0;No targets available.]"
                        .."field[0.25,1.25;4,0;label;;"..minetest.formspec_escape(meta:get_string("label")).."]"
                        .."button_exit[-0.05,1.5;4,1;setlabel;Set label]"
                    else
                        formspec = "size[4,0.4]" .. default.gui_bg .. default.gui_bg_img .. default.gui_slots
                        .."label[0,0;No targets available.]"
                    end
                end
                minetest.show_formspec(sender:get_player_name(), "elevator:elevator", formspec)
            elseif not elevator.motors[meta:get_string("motor")] then
                if not minetest.test_protection(pos, sender:get_player_name()) then
                    formspec = "size[4,2]" .. default.gui_bg .. default.gui_bg_img .. default.gui_slots
                    .."label[0,0;This elevator is inactive.]"
                    .."field[0.25,1.25;4,0;label;;"..minetest.formspec_escape(meta:get_string("label")).."]"
                    .."button_exit[-0.05,1.5;4,1;setlabel;Set label]"
                else
                    formspec = "size[4,0.4]" .. default.gui_bg .. default.gui_bg_img .. default.gui_slots
                    .."label[0,0;This elevator is inactive.]"
                end
                minetest.show_formspec(sender:get_player_name(), "elevator:elevator", formspec)
            elseif boxes[meta:get_string("motor")] then
                if not minetest.test_protection(pos, sender:get_player_name()) then
                    formspec = "size[4,2]" .. default.gui_bg .. default.gui_bg_img .. default.gui_slots
                    .."label[0,0;This elevator is in use.]"
                    .."field[0.25,1.25;4,0;label;;"..minetest.formspec_escape(meta:get_string("label")).."]"
                    .."button_exit[-0.05,1.5;4,1;setlabel;Set label]"
                else
                    formspec = "size[4,0.4]" .. default.gui_bg .. default.gui_bg_img .. default.gui_slots
                    .."label[0,0;This elevator is in use.]"
                end
                minetest.show_formspec(sender:get_player_name(), "elevator:elevator", formspec)
            end
        end,

        on_destruct = function(pos)
            local p = vector.add(pos, {x=0, y=1, z=0})
            if get_node(p).name == placeholder then
                minetest.remove_node(p)
            end
        end,
    })
end

minetest.register_on_player_receive_fields(function(sender, formname, fields)
    if formname ~= "elevator:elevator" then
        return
    end
    local pos = formspecs[sender:get_player_name()] and formspecs[sender:get_player_name()][1] or nil
    if not pos then
        return true
    end
    local meta = minetest.get_meta(pos)
    if fields.setlabel then
        if minetest.test_protection(pos, sender:get_player_name()) then
            return true
        end
        meta:set_string("label", fields.label)
        meta:set_string("infotext", fields.label)
        -- Rebuild the elevator shaft so the other elevators can read this label.
        local motorhash = meta:get_string("motor")
        build_motor(elevator.motors[motorhash] and motorhash or locate_motor(pos))
        return true
    end
    -- Double check if it's ok to go.
    if vector_distance(sender:get_pos(), pos) > 1 then
        return true
    end
    if fields.target then
        local closeformspec = ""
        -- HACK: With player information extensions enabled, we can check if closing formspecs are now allowed. This is specifically used on Survival in Ethereal.
        local pi = minetest.get_player_information(sender:get_player_name())
        if (not (pi.major == 0 and pi.minor == 4 and pi.patch == 15)) and (pi.protocol_version or 29) < 29 then
            closeformspec = "size[4,2] label[0,0;You are now using the elevator.\nUpgrade Minetest to avoid this dialog.] button_exit[0,1;4,1;close;Close]"
        end
        -- End hacky HACK.
        minetest.after(0.2, minetest.show_formspec, sender:get_player_name(), "elevator:elevator", closeformspec)
        -- Ensure we're connected to a motor.
        local motorhash = meta:get_string("motor")
        local motor = elevator.motors[motorhash]
        if not motor then
            motorhash = locate_motor(pos)
            motor = elevator.motors[motorhash]
            if motor then
                meta:set_string("motor", "")
                build_motor(motorhash)
                minetest.chat_send_player(sender:get_player_name(), "# Server: Recalibrated to a new motor, please try again.")
                return true
            end
        end
        if not motor then
            minetest.chat_send_player(sender:get_player_name(), "# Server: This elevator is not attached to a motor.")
            return true
        end
        if not formspecs[sender:get_player_name()][2] or not formspecs[sender:get_player_name()][2][minetest.explode_textlist_event(fields.target).index] then
            return true
        end
        -- Locate our target elevator.
        local target = nil
        local selected_target = formspecs[sender:get_player_name()][2][minetest.explode_textlist_event(fields.target).index]
        for i,v in ipairs(motor.pnames) do
            if v == selected_target then
                target = punhash(motor.elevators[i])
            end
        end
        -- Found the elevator? Then go!
        if target then
            -- Final check.
            if boxes[motorhash] then
                minetest.chat_send_player(sender:get_player_name(), "# Server: This elevator is in use.")
                return true
            end
            local obj = create_box(motorhash, pos, target, sender)
            -- Teleport anyone standing within an on elevator out, or they'd fall through the off elevators.
            for _,p in ipairs(motor.elevators) do
                local p = punhash(p)
                for _,object in ipairs(minetest.get_objects_inside_radius(p, 0.6)) do
                    if object.is_player and object:is_player() then
                        if object:get_player_name() ~= obj:get_luaentity().attached then
                            teleport_player_from_elevator(object)
                        end
                    end
                end
            end
        else
            minetest.chat_send_player(sender:get_player_name(), "# Server: This target is invalid.")
            return true
        end
        return true
    end
    return true
end)

-- Compatability with an older version.
minetest.register_alias("elevator:elevator", "elevator:elevator_off")

-- Ensure an elevator is up to the latest version.
local function upgrade_elevator(pos, meta)
    if meta:get_int("version") ~= VERSION then
        minetest.log("action", "[elevator] Updating elevator with old version at "..minetest.pos_to_string(pos))
        minetest.after(0, function(pos) build_motor(locate_motor(pos)) end, pos)
        meta:set_int("version", VERSION)
        meta:set_string("formspec", "")
        meta:set_string("infotext", meta:get_string("label"))
    end
end

-- Convert off to on when applicable.
local offabm = function(pos, node)
    local meta = minetest.get_meta(pos)
    upgrade_elevator(pos, meta)
    if not boxes[meta:get_string("motor")] and elevator.motors[meta:get_string("motor")] then
        node.name = "elevator:elevator_on"
        minetest.swap_node(pos, node)
    end
end

minetest.register_abm({
    nodenames = {"elevator:elevator_off"},
    interval = 1,
    chance = 1,
    action = offabm,
    label = "Elevator (Off)",
})

-- Convert on to off when applicable.
minetest.register_abm({
    nodenames = {"elevator:elevator_on"},
    interval = 1,
    chance = 1,
    action = function(pos, node)
        local meta = minetest.get_meta(pos)
        upgrade_elevator(pos, meta)
        if boxes[meta:get_string("motor")] or not elevator.motors[meta:get_string("motor")] then
            node.name = "elevator:elevator_off"
            minetest.swap_node(pos, node)
        end
    end,
    label = "Elevator (On)",
})

minetest.register_node("elevator:shaft", {
    description = "Elevator Shaft",
    tiles = { "elevator_shaft.png" },
    drawtype = "nodebox",
    paramtype = "light",
    on_rotate = screwdriver.disallow,
    sunlight_propagates = true,
    groups = {cracky=2, oddly_breakable_by_hand=1},
    sounds = default.node_sound_stone_defaults(),
    node_box = {
        type = "fixed",
        fixed = {
            {-8/16,-8/16,-8/16,-7/16,8/16,8/16},
            {7/16,-8/16,-8/16,8/16,8/16,8/16},
            {-7/16,-8/16,-8/16,7/16,8/16,-7/16},
            {-7/16,-8/16,8/16,7/16,8/16,7/16},
        },
    },
    collisionbox = {
        type = "fixed",
        fixed = {
            {-8/16,-8/16,-8/16,-7/16,8/16,8/16},
            {7/16,-8/16,-8/16,8/16,8/16,8/16},
            {-7/16,-8/16,-8/16,7/16,8/16,-7/16},
            {-7/16,-8/16,8/16,7/16,8/16,7/16},
        },
    },
    after_place_node = function(pos)
        -- We might have connected a motor above to an elevator below.
        build_motor(locate_motor(pos))
    end,
    on_destruct = function(pos)
        -- Remove boxes and deactivate elevators below us.
        unbuild(pos, 1)
    end,
})

local box = {
    { 0.48, -0.5,-0.5,  0.5,  1.5, 0.5},
    {-0.5 , -0.5, 0.48, 0.48, 1.5, 0.5},
    {-0.5,  -0.5,-0.5 ,-0.48, 1.5, 0.5},
    {-0.5 , -0.5, -0.48, 0.5, 1.5, -0.5},
    { -0.5,-0.5,-0.5,0.5,-0.48, 0.5},
    { -0.5, 1.45,-0.5,0.5, 1.5, 0.5},
}

-- Elevator box node. Not intended to be placeable.
minetest.register_node("elevator:elevator_box", {
    description = "Elevator",
    drawtype = "nodebox",
    paramtype = 'light',
    paramtype2 = "facedir",
    wield_scale = {x=0.6, y=0.6, z=0.6},

    selection_box = {
            type = "fixed",
            fixed = { -0.5, -0.5, -0.5, 0.5, 1.5, 0.5 }
    },

    collision_box = {
            type = "fixed",
            fixed = box,
    },

    node_box = {
            type = "fixed",
            fixed = box,
    },

    tiles = {
            "default_steel_block.png",
            "default_steel_block.png",
            "elevator_box.png",
            "elevator_box.png",
            "elevator_box.png",
            "elevator_box.png",
    },
    groups = {not_in_creative_inventory = 1},

    light_source = 4,
})

-- Remove the player from self, and teleport them to pos if specified.
local function detach(self, pos)
    local player = minetest.get_player_by_name(self.attached)
    local attached = player:get_attach()
    if not attached or attached:get_luaentity().uid ~= self.uid then
        return
    end
    player:set_detach()

    pova.remove_modifier(player, "eye_offset", "elevator")
    pova.remove_modifier(player, "properties", "elevator")

    if armor_path then
        armor:update_player_visuals(player)
    end
    if pos then
        player:setpos(pos)
	minetest.after(0.1, function(pl, p)
		pl:setpos(p)
	end, player, pos)
    end
    riding[self.attached] = nil
end

local box_entity = {
    physical = false,
    collisionbox = {0,0,0,0,0,0},
    visual = "wielditem",
    visual_size = {x=1, y=1},
    textures = {"elevator:elevator_box"},

    attached = "",
    motor = false,
    target = false,

    start = false,
    lastpos = false,
    halfway = false,
    vmult = 0,

    on_activate = function(self, staticdata)
        -- Don't want the box being destroyed by anything except the elevator system.
        self.object:set_armor_groups({immortal=1})
    end,

    on_step = function(self, dtime)
        local pos = self.object:get_pos()
        -- First, check if this box needs removed.
        -- If the motor has a box and it isn't this box.
        if boxes[self.motor] and boxes[self.motor] ~= self.object then
            minetest.log("action", "[elevator] "..minetest.pos_to_string(pos).." broke due to duplication.")
            self.object:remove()
            return
        end
        -- If our attached player can't be found.
        if not minetest.get_player_by_name(self.attached) then
            minetest.log("action", "[elevator] "..minetest.pos_to_string(pos).." broke due to lack of attachee logged in.")
            self.object:remove()
            boxes[self.motor] = nil
            return
        end
        -- If our attached player is no longer with us.
        if not minetest.get_player_by_name(self.attached):get_attach() or minetest.get_player_by_name(self.attached):get_attach():get_luaentity().uid ~= self.uid then
            minetest.log("action", "[elevator] "..minetest.pos_to_string(pos).." broke due to lack of attachee.")
            self.object:remove()
            boxes[self.motor] = nil
            return
        end
        -- If our motor's box is nil, we should self-destruct.
        if not boxes[self.motor] then
            minetest.log("action", "[elevator] "..minetest.pos_to_string(pos).." broke due to nil entry in boxes.")
            detach(self)
            self.object:remove()
            boxes[self.motor] = nil
            return
        end

        minetest.get_player_by_name(self.attached):setpos(pos)
        -- Ensure lastpos is set to something.
        self.lastpos = self.lastpos or pos

        -- Loop through all travelled nodes.
        for y=self.lastpos.y,pos.y,((self.lastpos.y > pos.y) and -0.3 or 0.3) do
            local p = vector_round({x=pos.x, y=y, z=pos.z})
            local node = get_node(p)
            if node.name == "elevator:shaft" then
                -- Nothing, just continue on our way.
            elseif node.name == "elevator:elevator_on" or node.name == "elevator:elevator_off" then
                -- If this is our target, detach the player here, destroy this box, and update the target elevator without waiting for the abm.
                if vector_distance(p, self.target) < 1 then
                    minetest.log("action", "[elevator] "..minetest.pos_to_string(p).." broke due to arrival.")
                    detach(self, vector.add(self.target, {x=0, y=-0.4, z=0}))
                    self.object:remove()
                    boxes[self.motor] = nil
                    offabm(self.target, node)
                    return
                end
            else
                -- Check if we're in the top part of an elevator, if so it's fine.
                local below = vector.add(p, {x=0,y=-1,z=0})
                local belownode = get_node(below)
                if belownode.name ~= "elevator:elevator_on" and belownode.name ~= "elevator:elevator_off" then
                    -- If we aren't, then break the box.
                    minetest.log("action", "[elevator] "..minetest.pos_to_string(p).." broke on "..node.name)
                    boxes[self.motor] = nil
                    detach(self, p)
                    self.object:remove()
                    return
                end
            end
        end
        self.lastpos = pos
    end,
}

minetest.register_entity("elevator:box", box_entity)



-- Register recipes!
minetest.register_craft({
		output = "elevator:elevator",
		recipe = {
				{"cast_iron:ingot", "chains:iron_chain", "cast_iron:ingot"},
				{"cast_iron:ingot", "", "cast_iron:ingot"},
				{"stainless_steel:ingot", "default:glass", "stainless_steel:ingot"},
		},
})

minetest.register_craft({
		output = "elevator:shaft",
		recipe = {
				{"titanium:crystal", "", "titanium:crystal"},
				{"xpanes:chainlink_flat", "default:obsidian_glass", "xpanes:chainlink_flat"},
				{"cast_iron:ingot", "", "cast_iron:ingot"},
		},
})

minetest.register_craft({
		output = "elevator:motor",
		recipe = {
				{"default:diamond", "techcrafts:control_logic_unit", "default:diamond"},
				{"stainless_steel:block", "techcrafts:electric_motor", "stainless_steel:block"},
				{"chains:iron_chain", "default:diamond", "chains:iron_chain"}
		},
})

