Build A Boat For Treasure Script – BABFT Schematics - Verified script for Build A Boat For Treasure
Key Required

Build A Boat For Treasure Script – BABFT Schematics

Build A Boat For TreasureAquilesinhoAquilesinho 1.4K views 2.1K executesUpdated Oct 22, 2026
Unrated
00
Lua Script•2654 lines (95655 chars)
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")
local HttpService = game:GetService("HttpService")
local RunService = game:GetService("RunService")
local Player = Players.LocalPlayer
local ROOT = "BABFT_Schematics"
local CHECK_INTERVAL = 0.3
local POSITION_TOLERANCE = 0.35
local ROTATION_TOLERANCE = math.rad(2)
local Schematics = {}
local Holograms = {}
local HologramOffset = Vector3.zero
local MoveHandles = nil
local MoveAnchor = nil
local MoveEnabled = false
local Checking = false
local LastCheck = 0
local function getFolder(playerName)
playerName = playerName or Player.Name
local blocksRoot = Workspace:FindFirstChild("Blocks")
if not blocksRoot then return nil end
return blocksRoot:FindFirstChild(playerName)
end
local function getPPart(model)
if model:IsA("BasePart") then
return model
end
local part = model:FindFirstChild("PPart", true)
if part and part:IsA("BasePart") then
return part
end
return nil
end
local function getBlocks(playerName)
local result = {}
local folder = getFolder(playerName)
if not folder then return result end
for _, model in ipairs(folder:GetChildren()) do
local part = getPPart(model)
if part then
table.insert(result, {
model = model,
part = part
})
end
end
return result
end
-- FILE SYSTEM
local function fileSystemAvailable()
return type(writefile) == "function"
and type(readfile) == "function"
and type(listfiles) == "function"
end
local function ensureDirectory()
if type(isfolder) == "function"
and type(makefolder) == "function" then
if not isfolder(ROOT) then
makefolder(ROOT)
end
return isfolder(ROOT)
end
return false
end
local function safeFileName(name)
name = name:gsub("[^%w_ %-]", "")
name = name:gsub("^%s+", "")
name = name:gsub("%s+$", "")
return name
end
local function filePath(name)
return ROOT .. "/" .. name .. ".json"
end
local function getRotation(cf)
local x, y, z = cf:ToOrientation()
return {x, y, z}
end
local function encodeProperty(value)
local kind = typeof(value)
if kind == "nil" or kind == "boolean" or kind == "number" or kind == "string" then
return {kind = kind, value = value}
elseif kind == "Vector3" then
return {kind = kind, value = {value.X, value.Y, value.Z}}
elseif kind == "Vector2" then
return {kind = kind, value = {value.X, value.Y}}
elseif kind == "Color3" then
return {kind = kind, value = {value.R, value.G, value.B}}
elseif kind == "CFrame" then
return {kind = kind, value = {value:GetComponents()}}
elseif kind == "BrickColor" then
return {kind = kind, value = value.Number}
elseif kind == "EnumItem" then
return {
kind = kind,
enumType = value.EnumType.Name,
value = value.Name
}
elseif kind == "NumberRange" then
return {kind = kind, value = {value.Min, value.Max}}
elseif kind == "UDim" then
return {kind = kind, value = {value.Scale, value.Offset}}
elseif kind == "UDim2" then
return {
kind = kind,
value = {
value.X.Scale, value.X.Offset,
value.Y.Scale, value.Y.Offset
}
}
elseif kind == "Rect" then
return {
kind = kind,
value = {
value.Min.X, value.Min.Y,
value.Max.X, value.Max.Y
}
}
elseif kind == "PhysicalProperties" then
return {
kind = kind,
value = {
value.Density, value.Friction, value.Elasticity,
value.FrictionWeight, value.ElasticityWeight
}
}
end
-- Instance references, Content values and unsupported types are skipped.
return nil
end
local function decodeProperty(data)
if type(data) ~= "table" then
return nil, false
end
local v = data.value
local kind = data.kind
if kind == "nil" then
return nil, true
elseif kind == "boolean" or kind == "number" or kind == "string" then
return v, true
elseif kind == "Vector3" then
return Vector3.new(v[1], v[2], v[3]), true
elseif kind == "Vector2" then
return Vector2.new(v[1], v[2]), true
elseif kind == "Color3" then
return Color3.new(v[1], v[2], v[3]), true
elseif kind == "CFrame" then
return CFrame.new(unpack(v)), true
elseif kind == "BrickColor" then
return BrickColor.new(v), true
elseif kind == "EnumItem" then
local enumType = Enum[data.enumType]
if enumType then
return enumType[v], true
end
elseif kind == "NumberRange" then
return NumberRange.new(v[1], v[2]), true
elseif kind == "UDim" then
return UDim.new(v[1], v[2]), true
elseif kind == "UDim2" then
return UDim2.new(v[1], v[2], v[3], v[4]), true
elseif kind == "Rect" then
return Rect.new(v[1], v[2], v[3], v[4]), true
elseif kind == "PhysicalProperties" then
return PhysicalProperties.new(v[1], v[2], v[3], v[4], v[5]), true
end
return nil, false
end
-- Team anchors. The local schematic coordinates are
-- stored relative to these anchors, allowing the same build to load on any team.
local TEAM_ANCHORS = {
Magenta = {370.9347229003906, -11.899993896484375, 531.893310546875, 0},
Yellow = {-477.9656677246094, -11.899991989135742, 759.8931884765625, 180},
Green = {-477.9656677246094, -11.899991989135742, 401.8931579589844, 180},
Blue = {370.8343200683594, -11.899991989135742, 173.49319458007812, 0},
Red = {370.8343200683594, -11.899991989135742, -184.706787109375, 0},
Black = {-477.9656677246094, -11.899991989135742, 43.893150329589844, 180},
White = {-169.56568908691406, -11.899991989135742, -494.5068664550781, 90},
}
local TEAM_NAME_ALIASES = {
Magenta = "Magenta", Yellow = "Yellow", Green = "Green", Blue = "Blue",
Red = "Red", Black = "Black", White = "White",
}
local function teamNameFromColor(teamColor)
local raw = tostring(teamColor or "")
raw = raw:gsub("TeamColor", ""):gsub("Color3%(%s*", "")
for name in pairs(TEAM_ANCHORS) do
if raw:lower():find(name:lower(), 1, true) then return name end
end
-- Roblox BrickColor names may include spaces (for example, "Bright blue").
local lower = raw:lower()
if lower:find("magenta", 1, true) then return "Magenta" end
if lower:find("yellow", 1, true) or lower:find("yeller", 1, true) then return "Yellow" end
if lower:find("green", 1, true) then return "Green" end
if lower:find("blue", 1, true) then return "Blue" end
if lower:find("red", 1, true) then return "Red" end
if lower:find("black", 1, true) then return "Black" end
if lower:find("white", 1, true) then return "White" end
return nil
end
local TeamsService = game:GetService("Teams")
-- Resolve the current player's team using the game's TeamLeader StringValues first.
-- TeamLeader may contain the player's username; Team/TeamColor are fallbacks.
local function resolveTeamForPlayer(targetPlayer)
if not targetPlayer then return nil end
for _, teamName in ipairs({"blue", "black", "magenta", "green", "red", "white", "yellow"}) do
local team = TeamsService:FindFirstChild(teamName)
local leader = team and team:FindFirstChild("TeamLeader")
if leader and leader:IsA("StringValue") and leader.Value == targetPlayer.Name then
return team
end
end
if targetPlayer.Team and targetPlayer.Team:IsA("Team") then
return targetPlayer.Team
end
local color = targetPlayer.TeamColor
for _, team in ipairs(TeamsService:GetChildren()) do
if team:IsA("Team") and color and team.TeamColor == color then
return team
end
end
return nil
end
local function teamAnchorCFrame(teamOrColor)
local name
if typeof(teamOrColor) == "Instance" and teamOrColor:IsA("Team") then
name = teamNameFromColor(teamOrColor.Name) or teamNameFromColor(teamOrColor.TeamColor)
else
name = teamNameFromColor(teamOrColor)
end
local a = name and TEAM_ANCHORS[name]
if not a then return nil end
return CFrame.new(a[1], a[2], a[3]) * CFrame.Angles(0, math.rad(a[4]), 0)
end
local function currentPlayerTeam()
return resolveTeamForPlayer(Player)
end
local function pivotFromSchematic(modelData)
local savedPivot = CFrame.new(unpack(modelData.pivot))
if modelData.coordinateSpace == "team-relative" then
local team = currentPlayerTeam()
local anchor = team and teamAnchorCFrame(team) or nil
if anchor then return (anchor * savedPivot) + HologramOffset end
warn("[Schematic] Could not resolve your team through TeamLeader/Team; using saved world coordinates.")
end
-- Backwards compatibility with older JSON files that contain world positions.
return savedPivot + HologramOffset
end
local function serializePart(part, pivot)
local relative = pivot:ToObjectSpace(part.CFrame)
local data = {
class = part.ClassName,
name = part.Name,
cf = {relative:GetComponents()},
size = {part.Size.X, part.Size.Y, part.Size.Z},
color = {part.Color.R, part.Color.G, part.Color.B},
transparency = part.Transparency,
reflectance = part.Reflectance,
material = part.Material.Name,
anchored = part.Anchored,
canCollide = part.CanCollide,
shape = nil,
meshId = nil,
textureId = nil,
properties = {}
}
if part:IsA("Part") then
data.shape = part.Shape.Name
end
if part:IsA("MeshPart") then
data.meshId = part.MeshId
data.textureId = part.TextureID
end
-- Capture as many properties as the executor exposes.
if type(getproperties) == "function" then
local ok, properties = pcall(getproperties, part)
if ok and type(properties) == "table" then
for propertyName, value in pairs(properties) do
local encodedOk, encoded = pcall(encodeProperty, value)
if encodedOk and encoded then
data.properties[propertyName] = encoded
end
end
end
end
return data
end
local function getRelativeInstancePath(root, instance)
    local segments = {}
    local current = instance
    while current and current ~= root do
        local parent = current.Parent
        if not parent then return nil end
        local sameNameIndex = 0
        for _, sibling in ipairs(parent:GetChildren()) do
            if sibling.Name == current.Name then
                sameNameIndex += 1
                if sibling == current then break end
            end
        end
        table.insert(segments, 1, {
            name = current.Name,
            class = current.ClassName,
            index = sameNameIndex
        })
        current = parent
    end
    if current ~= root then return nil end
    return segments
end

local function findByRelativeInstancePath(root, segments)
    local current = root
    for _, segment in ipairs(segments or {}) do
        local found, sameNameIndex = nil, 0
        for _, child in ipairs(current:GetChildren()) do
            if child.Name == segment.name then
                sameNameIndex += 1
                if sameNameIndex == (segment.index or 1) and child.ClassName == segment.class then
                    found = child
                    break
                end
            end
        end
        if not found then return nil end
        current = found
    end
    return current
end

-- Use BABFT public definitions to determine which properties
-- exist for each block type. This does not depend on BlockPropertiesServer.
local cachedBlockProperties
local function getBlockPropertyDefinitions(model)
    if not cachedBlockProperties then
        local ok, module = pcall(function()
            return require(game:GetService("ReplicatedStorage").Scripts.BlockProperties)
        end)
        if ok and type(module) == "table" then
            cachedBlockProperties = module
        else
            return nil
        end
    end
    local ok, definitions = pcall(function()
        return cachedBlockProperties[model.Name]
    end)
    if ok and type(definitions) == "table" then return definitions end
    return nil
end

local function serializeBlockProperties(model, definitions)
    local saved = {}
    if type(definitions) ~= "table" then return saved end
    for propertyName, definition in pairs(definitions) do
        if type(definition) == "table" and type(definition.GetState) == "function" then
            local ok, state = pcall(definition.GetState, model)
            if ok then
                local encodedOk, encoded = pcall(encodeProperty, state)
                if encodedOk and encoded then
                    saved[propertyName] = encoded
                end
            end
        end
    end
    return saved
end

-- The auxiliary ValueBase save is limited to properties recognized by the module
-- for this block; it does not dump every internal model value.
local PROPERTY_VALUE_ALIASES = {
    ["Piston length"] = {"ExtendLength"},
    ["Piston speed"] = {"Speed"},
    ["Wheel speed"] = {"MaxSpeed"},
    ["Servo angle"] = {"TargetAngle"},
    ["Fuse time"] = {"FuseTime"},
    ["Flight distance"] = {"FlightDistance"},
    ["Delay time"] = {"WaitDuration"},
    ["Reverse rotation"] = {"ReverseRotation"},
    ["Crosshairs"] = {"ShowCrosshairs"},
    ["Key"] = {"SemitoneOffset"},
    ["Grow head scale"] = {"HeadScale"},
    ["Grow height scale"] = {"HeightScale"},
    ["Grow width scale"] = {"WidthScale"},
    ["Grow depth scale"] = {"DepthScale"},
    ["Shrink head scale"] = {"HeadScale"},
    ["Shrink height scale"] = {"HeightScale"},
    ["Shrink width scale"] = {"WidthScale"},
    ["Shrink depth scale"] = {"DepthScale"},
    ["Aim"] = {"Aim"},
    ["And"] = {"And"}, ["Or"] = {"Or"},
    ["Xor"] = {"Xor"}, ["Not"] = {"Not"},
    ["Additive"] = {"Additive"}, ["Legacy"] = {"Legacy"},
}

local function serializeValueObjects(model, definitions)
    local values, allowedNames = {}, {}
    if type(definitions) ~= "table" then return values end
    for propertyName, definition in pairs(definitions) do
        if type(definition) == "table" and type(definition.GetState) == "function" then
            allowedNames[propertyName] = true
            for _, alias in ipairs(PROPERTY_VALUE_ALIASES[propertyName] or {}) do
                allowedNames[alias] = true
            end
        end
    end
    for _, obj in ipairs(model:GetDescendants()) do
        if obj:IsA("ValueBase") and allowedNames[obj.Name] then
            local path = getRelativeInstancePath(model, obj)
            if path then
                local ok, encoded = pcall(encodeProperty, obj.Value)
                if ok and encoded then
                    table.insert(values, {
                        path = path,
                        name = obj.Name,
                        class = obj.ClassName,
                        value = encoded
                    })
                end
            end
        end
    end
    return values
end

local function restoreValueObjects(model, modelData)
    if not model or not modelData or type(modelData.values) ~= "table" then return 0, 0 end
    local restored, skipped = 0, 0
    for _, saved in ipairs(modelData.values) do
        local target = findByRelativeInstancePath(model, saved.path)
        if target and target:IsA("ValueBase") and target.ClassName == saved.class then
            local decodedOk, value, valid = pcall(function()
                local v, canDecode = decodeProperty(saved.value)
                return v, canDecode
            end)
            if decodedOk and valid then
                local setOk = pcall(function() target.Value = value end)
                if setOk then restored += 1 else skipped += 1 end
            else
                skipped += 1
            end
        else
            -- The destination block does not expose this value/property: skip it.
            skipped += 1
        end
    end
    return restored, skipped
end


-- Capture BindTool structure: B1/B2/B3 tags and Beam edges between blocks.
-- IDs are stable JSON indices, not Instance references.
local function serializeBindTags(model)
    local tags = {}
    for _, tagName in ipairs({"B1", "B2", "B3", "SeatInput"}) do
        local ok, hasTag = pcall(function() return model:HasTag(tagName) end)
        if ok and hasTag then tags[#tags + 1] = tagName end
    end
    return tags
end

local function getBindingActionMap(model)
    local result = {}
    for _, child in ipairs(model:GetChildren()) do
        if child.Name:sub(1, 4) == "Bind" and child.Name ~= "BindingSB" then
            local action = child:FindFirstChild("ActionName")
            if action and action:IsA("ValueBase") and type(action.Value) == "string" then
                local keyCode
                for _, value in ipairs(child:GetDescendants()) do
                    if value:IsA("ValueBase") and
                        (value.Name == "KeyCode" or value.Name == "KeyCodeValue" or value.Name == "Key") then
                        if type(value.Value) == "number" then
                            keyCode = value.Value
                            break
                        end
                    end
                end
                result[action.Value] = {
                    bindName = child.Name,
                    keyCode = keyCode
                }
            end
        end
    end
    return result
end

local function serializeBindConnections(models)
    local idByModel = {}
    for index, entry in ipairs(models) do
        idByModel[entry.model] = index
    end
    local connections, seen = {}, {}
    for _, entry in ipairs(models) do
        local sourceModel = entry.model
        for _, object in ipairs(sourceModel:GetDescendants()) do
            if object:IsA("Beam") and object.Attachment0 and object.Attachment1 then
                local function ownerFromAttachment(attachment)
                    local parent = attachment.Parent
                    local candidate = parent and parent.Parent
                    if candidate and candidate:IsA("Model") then return candidate end
                    return candidate and candidate:FindFirstAncestorOfClass("Model") or nil
                end
                local a = ownerFromAttachment(object.Attachment0)
                local b = ownerFromAttachment(object.Attachment1)
                local aid, bid = idByModel[a], idByModel[b]
                if aid and bid and aid ~= bid then
                    local left, right = math.min(aid, bid), math.max(aid, bid)
                    local key = tostring(left) .. ":" .. tostring(right)
                    if not seen[key] then
                        seen[key] = true
                        local actionSource, actionSourceId = a, aid
                        if not a:HasTag("B1") and b:HasTag("B1") then
                            actionSource, actionSourceId = b, bid
                        end
                        local actionNames = {}
                        for actionName in pairs(getBindingActionMap(actionSource) or {}) do
                            actionNames[#actionNames + 1] = actionName
                        end
                        table.sort(actionNames)
                        connections[#connections + 1] = {
                            a = aid,
                            b = bid,
                            actionSource = actionSourceId,
                            aTags = serializeBindTags(a),
                            bTags = serializeBindTags(b),
                            actions = actionNames
                        }
                    end
                end
            end
        end
    end
    return connections
end

local function serializeModel(model, sourceAnchor)
local worldPivot = model:GetPivot()
local pivot = sourceAnchor and sourceAnchor:ToObjectSpace(worldPivot) or worldPivot
local parts = {}
for _, obj in ipairs(model:GetDescendants()) do
if obj:IsA("BasePart") then
table.insert(parts, serializePart(obj, worldPivot))
end
end
local mainPart = getPPart(model)
local mainRelative = nil
if mainPart then
mainRelative = {
worldPivot:ToObjectSpace(mainPart.CFrame):GetComponents()
}
end
local propertyDefinitions = getBlockPropertyDefinitions(model)
return {
name = model.Name,
pivot = {pivot:GetComponents()},
mainPart = mainRelative,
parts = parts,
bindTags = serializeBindTags(model),
bindActions = getBindingActionMap(model),
-- States are read with the same GetState method used by BABFT property UI.
blockProperties = serializeBlockProperties(model, propertyDefinitions),
values = serializeValueObjects(model, propertyDefinitions)
}
end
local function saveSchematic(name, targetPlayerName)
if not fileSystemAvailable() then
warn("This executor does not provide all required file functions.")
return false
end
if not ensureDirectory() then
warn("Could not create the schematics folder.")
return false
end
name = safeFileName(name)
if name == "" then
warn("Enter a valid name.")
return false
end
targetPlayerName = targetPlayerName or Player.Name
local targetFolder = getFolder(targetPlayerName)
if not targetFolder then
warn("Could not find a block folder for:", targetPlayerName)
return false
end
local models = getBlocks(targetPlayerName)
if #models == 0 then
warn("No blocks found for:", targetPlayerName)
return false
end
local Players = game:GetService("Players")
local sourcePlayer = Players:FindFirstChild(targetPlayerName)
local sourceTeam = resolveTeamForPlayer(sourcePlayer)
local sourceAnchor = sourceTeam and teamAnchorCFrame(sourceTeam) or nil
if not sourceAnchor then
warn("Could not identify the team/origin for", targetPlayerName, "; using legacy world coordinates.")
end
local data = {
version = 2,
coordinateSpace = sourceAnchor and "team-relative" or "world",
sourceTeam = sourceTeam and (teamNameFromColor(sourceTeam.Name) or teamNameFromColor(sourceTeam.TeamColor)) or nil,
name = name,
sourcePlayer = targetPlayerName,
blocks = {}
}
for index, entry in ipairs(models) do
local blockData = serializeModel(entry.model, sourceAnchor)
blockData.id = index
table.insert(data.blocks, blockData)
end
data.bindConnections = serializeBindConnections(models)
-- Each hologram item carries the connection table so it survives loading,
-- which otherwise keeps only each block's individual data.
for _, blockData in ipairs(data.blocks) do
    blockData.bindConnections = data.bindConnections
end
local ok, encoded = pcall(function()
return HttpService:JSONEncode(data)
end)
if not ok then
warn("JSON encoding failed:", encoded)
return false
end
local saved, err = pcall(function()
writefile(filePath(name), encoded)
end)
if not saved then
warn("Save failed:", err)
return false
end
print("Schematic saved:", name, "Blocks:", #data.blocks)
return true
end
local function listSchematics()
local result = {}
if not fileSystemAvailable() then
return result
end
if not ensureDirectory() then
return result
end
local ok, files = pcall(function()
return listfiles(ROOT)
end)
if not ok then
return result
end
for _, path in ipairs(files) do
if path:lower():match("%.json$") then
local readOk, content = pcall(function()
return readfile(path)
end)
if readOk then
local decodeOk, data = pcall(function()
return HttpService:JSONDecode(content)
end)
if decodeOk
and type(data) == "table"
and type(data.blocks) == "table" then
table.insert(result, {
name = data.name or path,
path = path,
data = data
})
end
end
end
end
table.sort(result, function(a, b)
return a.name:lower() < b.name:lower()
end)
return result
end
-- HOLOGRAM RECONSTRUCTION
local function makeCFrame(values)
return CFrame.new(
values[1], values[2], values[3],
values[4], values[5], values[6],
values[7], values[8], values[9],
values[10], values[11], values[12]
)
end
local function updateMoveAnchor()
    if not MoveAnchor or not MoveAnchor.Parent or #Holograms == 0 then return end

    local minV, maxV
    for _, item in ipairs(Holograms) do
        for _, entry in ipairs(item.parts or {}) do
            local part = entry.part
            if part and part.Parent then
                -- Compute the world-axis-aligned bounds, including each block's
                -- actual dimensions and rotation (not just its center position).
                local half = part.Size * 0.5
                local cf = part.CFrame
                for sx = -1, 1, 2 do
                    for sy = -1, 1, 2 do
                        for sz = -1, 1, 2 do
                            local corner = cf:PointToWorldSpace(Vector3.new(
                                half.X * sx, half.Y * sy, half.Z * sz
                            ))
                            minV = minV and Vector3.new(
                                math.min(minV.X, corner.X),
                                math.min(minV.Y, corner.Y),
                                math.min(minV.Z, corner.Z)
                            ) or corner
                            maxV = maxV and Vector3.new(
                                math.max(maxV.X, corner.X),
                                math.max(maxV.Y, corner.Y),
                                math.max(maxV.Z, corner.Z)
                            ) or corner
                        end
                    end
                end
            end
        end
    end

    if minV and maxV then
        local size = maxV - minV
        -- Handles are drawn around the Adornee's faces, so make it span
        -- the entire schematic instead of using a 1x1x1 anchor.
        MoveAnchor.Size = Vector3.new(
            math.max(size.X, 0.2),
            math.max(size.Y, 0.2),
            math.max(size.Z, 0.2)
        )
        MoveAnchor.CFrame = CFrame.new((minV + maxV) / 2)
    end
end

local function setMoveMode(enabled)
    MoveEnabled = enabled and #Holograms > 0
    if MoveAnchor then MoveAnchor:Destroy(); MoveAnchor = nil end
    if MoveEnabled then
        local folder = Workspace:FindFirstChild("Hologram") or Workspace
        MoveAnchor = Instance.new("Part")
        MoveAnchor.Name = "SchematicMoveAnchor"
        MoveAnchor.Size = Vector3.new(1,1,1)
        MoveAnchor.Transparency = 1
        MoveAnchor.Anchored = true
        MoveAnchor.CanCollide = false
        MoveAnchor.CanTouch = false
        MoveAnchor.CanQuery = false
        MoveAnchor.CFrame = CFrame.new()
        MoveAnchor.Parent = folder
        updateMoveAnchor()
        if MoveHandles then
            MoveHandles.Adornee = MoveAnchor
            MoveHandles.Visible = true
        end
    elseif MoveHandles then
        MoveHandles.Adornee = nil
        MoveHandles.Visible = false
    end
end

local function applyHologramMove(delta)
    if delta.Magnitude < 1e-5 then return end
    HologramOffset += delta
    for _, item in ipairs(Holograms) do
        item.pivot = item.pivot + delta
        for _, entry in ipairs(item.parts or {}) do
            if entry.part and entry.part.Parent then
                entry.part.CFrame = entry.part.CFrame + delta
            end
        end
    end
    updateMoveAnchor()
end

local function removeHolograms()
Checking = false
MoveEnabled = false
HologramOffset = Vector3.zero
if MoveHandles then MoveHandles.Adornee = nil; MoveHandles.Visible = false end
if MoveAnchor then MoveAnchor:Destroy(); MoveAnchor = nil end
for _, item in ipairs(Holograms) do
if item.model and item.model.Parent then
item.model:Destroy()
end
end
table.clear(Holograms)
local folder = Workspace:FindFirstChild("Hologram")
if folder then
for _, obj in ipairs(folder:GetChildren()) do
if obj:GetAttribute("SchematicGhost") then
obj:Destroy()
end
end
end
end
local function getHologramFolder()
local folder = Workspace:FindFirstChild("Hologram")
if not folder then
folder = Instance.new("Folder")
folder.Name = "Hologram"
folder.Parent = Workspace
end
return folder
end
local function reconstructPart(data, parent, pivot)
local class = data.class
local part
if class == "MeshPart"
and type(data.meshId) == "string"
and data.meshId ~= "" then
part = Instance.new("MeshPart")
pcall(function()
part.MeshId = data.meshId
part.TextureID = data.textureId or ""
end)
else
part = Instance.new("Part")
if data.shape then
pcall(function()
part.Shape = Enum.PartType[data.shape]
end)
end
end
part.Name = data.name or "PPart"
-- Restore captured properties individually. Any unsupported/read-only
-- property is skipped without stopping the rest of the reconstruction.
local properties = data.properties
if type(properties) == "table" then
for propertyName, encoded in pairs(properties) do
-- Position/orientation are applied from the relative schematic
-- transform below; visual/ghost settings are forced afterward.
local protected = {
Parent = true,
Name = true,
CFrame = true,
Position = true,
Orientation = true,
Rotation = true,
PivotOffset = true,
AssemblyRootPart = true,
AssemblyCenterOfMass = true,
AssemblyLinearVelocity = true,
AssemblyAngularVelocity = true,
ReceiveAge = true,
NetworkOwnershipRule = true,
UniqueId = true,
SourceAssetId = true,
MeshContent = true,
TextureContent = true
}
if not protected[propertyName] then
local decodedOk, value, valid = pcall(function()
local v, canDecode = decodeProperty(encoded)
return v, canDecode
end)
if decodedOk and valid then
pcall(function()
part[propertyName] = value
end)
end
end
end
end
-- Fallback for older schematic files and properties that could not be
-- serialized by getproperties().
pcall(function()
part.Size = Vector3.new(data.size[1], data.size[2], data.size[3])
end)
pcall(function()
part.Color = Color3.new(data.color[1], data.color[2], data.color[3])
end)
pcall(function()
part.Reflectance = data.reflectance or 0
end)
pcall(function()
part.Material = Enum.Material[data.material]
end)
part.Anchored = true
part.CanCollide = false
part.CanTouch = false
-- Keep schematic ghosts out of the game's building raycasts. Selection is
-- performed in screen space below, so CanQuery can safely stay false.
part.CanQuery = false
part.CastShadow = false
-- Keep holograms semi-transparent and place each part relative to pivot.
pcall(function()
part.Transparency = math.max(data.transparency or 0, 0.5)
end)
part.CFrame = pivot * makeCFrame(data.cf)
part:SetAttribute("SchematicGhost", true)
part.Parent = parent
return part
end
local function createErrorLabel(adornee, modelName)
if not adornee then
return nil
end
local billboard = Instance.new("BillboardGui")
billboard.Name = "SchematicErrorLabel"
billboard.Adornee = adornee
billboard.Size = UDim2.fromOffset(260, 70)
billboard.StudsOffset = Vector3.new(0, 2.8, 0)
billboard.AlwaysOnTop = true
billboard.Enabled = false
local label = Instance.new("TextLabel")
label.Name = "Message"
label.Size = UDim2.fromScale(1, 1)
label.BackgroundColor3 = Color3.fromRGB(25, 22, 10)
label.BackgroundTransparency = 0.15
label.BorderSizePixel = 0
label.TextColor3 = Color3.fromRGB(255, 220, 40)
label.TextStrokeTransparency = 0.35
label.TextWrapped = true
label.TextXAlignment = Enum.TextXAlignment.Left
label.TextYAlignment = Enum.TextYAlignment.Center
label.Font = Enum.Font.GothamSemibold
label.TextSize = 12
label.Text = ""
label.Parent = billboard
local corner = Instance.new("UICorner")
corner.CornerRadius = UDim.new(0, 5)
corner.Parent = label
billboard.Parent = adornee
return billboard, label
end
local ShowMissingBlockMessages = false -- Compatibilidade
local ShowAllHologramMessages = false
local ShowHolograms = true
local function applyHologramVisibility()
for _, item in ipairs(Holograms) do
if item.model and item.model.Parent then
for _, descendant in ipairs(item.model:GetDescendants()) do
if descendant:IsA("BasePart") then
descendant.LocalTransparencyModifier = ShowHolograms and 0 or 1
end
end
end
if item.errorBillboard and item.errorBillboard.Parent then
item.errorBillboard.Enabled = ShowAllHologramMessages and ShowHolograms
and item.errorMessage ~= nil and item.errorMessage ~= ""
end
end
end
local function setErrorMessage(item, message, enabled)
item.errorMessage = message or ""
local shouldShow = ShowAllHologramMessages and ShowHolograms
and enabled == true and message ~= nil and message ~= ""
if item.errorBillboard and item.errorBillboard.Parent then
item.errorBillboard.Enabled = shouldShow
end
if item.errorLabel and item.errorLabel.Parent then
item.errorLabel.Text = message or ""
end
end
local function createHologram(modelData, folder)
local ghost = Instance.new("Model")
ghost.Name = modelData.name or "SchematicGhost"
ghost:SetAttribute("SchematicGhost", true)
local pivot = pivotFromSchematic(modelData)
local parts = {}
for _, partData in ipairs(modelData.parts) do
local ok, part = pcall(function()
return reconstructPart(partData, ghost, pivot)
end)
if ok and part then
table.insert(parts, {
part = part,
data = partData
})
end
end
local mainPart = nil
if modelData.mainPart then
local expectedCF = pivot * makeCFrame(modelData.mainPart)
local bestDistance = math.huge
for _, entry in ipairs(parts) do
local distance =
(entry.part.Position - expectedCF.Position).Magnitude
if distance < bestDistance then
bestDistance = distance
mainPart = entry.part
end
end
end
if not mainPart and #parts > 0 then
mainPart = parts[1].part
end
ghost.PrimaryPart = mainPart
local errorBillboard, errorLabel = createErrorLabel(mainPart, ghost.Name)
ghost.Parent = folder
table.insert(Holograms, {
model = ghost,
pivot = pivot,
parts = parts,
data = modelData,
status = "missing",
errorBillboard = errorBillboard,
errorLabel = errorLabel,
errorMessage = "MISSING: no block found at this position"
})
return ghost
end
local function getSchematicInventoryReport(blocks)
local dataFolder = Player:FindFirstChild("Data")
if not dataFolder then
return false, {"Could not read Player.Data"}
end

-- Count only block types actually present in the loaded schematic.
-- If Player.Data has no matching ValueBase, do not invent a zero-stock entry
-- for that name: it is not a readable inventory item and should not be listed.
local required = {}
for _, item in ipairs(blocks or {}) do
    local modelData = item and item.data
    local name = modelData and modelData.name
    if type(name) == "string" and name ~= "" then
        required[name] = (required[name] or 0) + 1
    end
end

local missing = {}
for name, needed in pairs(required) do
    local value = dataFolder:FindFirstChild(name)
    if value and value:IsA("ValueBase") then
        local available = tonumber(value.Value)
        if available and available < needed then
            table.insert(missing, {
                name = name,
                needed = needed,
                available = available,
                short = needed - available
            })
        end
    end
end

table.sort(missing, function(a, b) return a.name < b.name end)
return #missing == 0, missing
end
local function showInventoryReport(blocks)
local enough, missing = getSchematicInventoryReport(blocks)
if enough then
local total = #blocks
print(string.format("[Schematic] Inventory OK: materials available for all %d blocks.", total))
return true, string.format("Materials OK | %d blocks", total)
end
warn("[Schematic] INSUFFICIENT MATERIALS — unavailable blocks will be skipped:")
for _, item in ipairs(missing) do
warn(string.format("  %s: need %d, have %d, missing %d", item.name, item.needed, item.available, item.short))
end
local summary = {}
for _, item in ipairs(missing) do
table.insert(summary, string.format("%s: missing %d (needs %d, has %d)", item.name, item.short, item.needed, item.available))
end
return false, "Missing materials: " .. table.concat(summary, " | ")
end
local function loadSchematic(entry)
removeHolograms()
local folder = getHologramFolder()
for _, modelData in ipairs(entry.data.blocks) do
modelData.coordinateSpace = entry.data.coordinateSpace or modelData.coordinateSpace
createHologram(modelData, folder)
end
Checking = true
showInventoryReport(Holograms)
print(
"Schematic loaded:",
entry.name,
"Blocks:",
#Holograms
)
end
-- BLOCK COMPARISON
local function angleDifference(a, b)
return math.abs(math.atan2(
math.sin(a - b),
math.cos(a - b)
))
end
local function rotationsMatch(a, b)
local ax, ay, az = a:ToOrientation()
local bx, by, bz = b:ToOrientation()
return angleDifference(ax, bx) <= ROTATION_TOLERANCE
and angleDifference(ay, by) <= ROTATION_TOLERANCE
and angleDifference(az, bz) <= ROTATION_TOLERANCE
end
-- Spatial index to avoid comparing every hologram with every block.
-- A tolerância de posição continua sendo exatamente POSITION_TOLERANCE.
local SPATIAL_CELL_SIZE = 2
local function spatialCell(position)
return math.floor(position.X / SPATIAL_CELL_SIZE),
math.floor(position.Y / SPATIAL_CELL_SIZE),
math.floor(position.Z / SPATIAL_CELL_SIZE)
end
local function buildBlockSpatialIndex(blocks)
local index = {}
for _, entry in ipairs(blocks) do
local x, y, z = spatialCell(entry.part.Position)
local key = x .. ":" .. y .. ":" .. z
local cell = index[key]
if not cell then
cell = {}
index[key] = cell
end
table.insert(cell, entry)
end
return index
end
local function findBlockAt(position, spatialIndex)
local cx, cy, cz = spatialCell(position)
local best, bestDistance = nil, POSITION_TOLERANCE
local radius = math.ceil(POSITION_TOLERANCE / SPATIAL_CELL_SIZE) + 1
for x = cx - radius, cx + radius do
for y = cy - radius, cy + radius do
for z = cz - radius, cz + radius do
local cell = spatialIndex[x .. ":" .. y .. ":" .. z]
if cell then
for _, entry in ipairs(cell) do
local distance = (entry.part.Position - position).Magnitude
if distance <= bestDistance then
best = entry
bestDistance = distance
end
end
end
end
end
end
return best
end
local function setStatus(item, status)
item.status = status
if status == "correct" then
setErrorMessage(item, "", false)
elseif status == "missing" then
setErrorMessage(item, "MISSING: place " .. tostring(item.data.name or "este bloco"), true)
elseif status == "wrong_type" then
setErrorMessage(item, item.errorMessage ~= "" and item.errorMessage or "WRONG BLOCK TYPE", true)
elseif status == "wrong_properties" then
setErrorMessage(item, item.errorMessage ~= "" and item.errorMessage or "PROPERTY MISMATCH", true)
end
local color
if status == "correct" then
color = Color3.fromRGB(0, 255, 120)
elseif status == "wrong_type" then
color = Color3.fromRGB(255, 35, 35)
elseif status == "wrong_properties" then
color = Color3.fromRGB(255, 210, 0)
else
color = Color3.fromRGB(0, 220, 255)
end
for _, entry in ipairs(item.parts) do
local part = entry.part
if part and part.Parent then
part.Color = color
if status == "correct" then
part.Transparency = 1
else
part.Transparency = 0.35
end
end
end
end
local function compareBlocks(force)
-- Full comparison searches for each hologram among ALL real blocks.
-- With thousands of parts, this costs O(holograms * blocks) every 0.3 seconds.
-- If floating messages are disabled, there is no reason to run this scanner.
if not force and (not Checking or not ShowAllHologramMessages) then
return
end
local blocks = getBlocks()
local spatialIndex = buildBlockSpatialIndex(blocks)
for _, item in ipairs(Holograms) do
local expected = item.data
local pivot = item.pivot
local mainPosition = pivot.Position
if expected.mainPart then
mainPosition =
(pivot * makeCFrame(expected.mainPart)).Position
end
local found = findBlockAt(mainPosition, spatialIndex)
if not found then
item.errorMessage = "MISSING: place " .. tostring(expected.name or "este bloco")
setStatus(item, "missing")
elseif found.model.Name ~= expected.name then
item.errorMessage = "TIPO: esperado " .. tostring(expected.name)
.. " | found " .. tostring(found.model.Name)
setStatus(item, "wrong_type")
else
local actual = found.part
local messages = {}
-- Size is intentionally ignored: it should not trigger a yellow warning.
local expectedRotation = pivot
if expected.mainPart then
expectedRotation = pivot * makeCFrame(expected.mainPart)
end
if not rotationsMatch(actual.CFrame, expectedRotation) then
local ex, ey, ez = expectedRotation:ToOrientation()
local ax, ay, az = actual.CFrame:ToOrientation()
table.insert(messages, string.format(
"Rotation: expected %.0f, %.0f, %.0f° | actual %.0f, %.0f, %.0f°",
math.deg(ex), math.deg(ey), math.deg(ez),
math.deg(ax), math.deg(ay), math.deg(az)
))
end
if #messages == 0 then
item.errorMessage = ""
setStatus(item, "correct")
else
item.errorMessage = table.concat(messages, "\n")
setStatus(item, "wrong_properties")
end
end
end
end
-- BUILD: uses the game's BuildingTool remote and server-side validation.
local refreshStatus
local BuildRunning = false
local function findScaleTool(character, backpack)
return (backpack and backpack:FindFirstChild("ScalingTool"))
or (character and character:FindFirstChild("ScalingTool"))
end
local function getSavedPrimarySize(modelData)
if type(modelData.parts) ~= "table" then return nil end
for _, partData in ipairs(modelData.parts) do
if partData.name == "PPart" and type(partData.size) == "table" then
local x, y, z = tonumber(partData.size[1]), tonumber(partData.size[2]), tonumber(partData.size[3])
if x and y and z and x > 0 and y > 0 and z > 0 then
return Vector3.new(x, y, z)
end
end
end
return nil
end
local function capturePlacedModel(blocksFolder, before, blockName, desiredCF, claimedModels, timeout)
if not blocksFolder then return nil end
local deadline = os.clock() + (timeout or 2)
repeat
    local best, bestDistance = nil, math.huge
    for _, model in ipairs(blocksFolder:GetChildren()) do
        if not before[model] and not claimedModels[model]
            and model:IsA("Model") and model.Name == blockName then
            local part = getPPart(model)
            if part then
                local distance = (part.Position - desiredCF.Position).Magnitude
                if distance < bestDistance then
                    best, bestDistance = model, distance
                end
            end
        end
    end
    if best then
        claimedModels[best] = true
        return best
    end
    task.wait(0.03)
until os.clock() >= deadline
return nil
end
-- Retrieve the saved primary-part color from the schematic.
local function getSavedPrimaryColor(modelData)
if type(modelData.parts) ~= "table" then return nil end
for _, partData in ipairs(modelData.parts) do
if partData.name == "PPart" and type(partData.color) == "table" then
local r, g, b = tonumber(partData.color[1]), tonumber(partData.color[2]), tonumber(partData.color[3])
if r and g and b then
return Color3.new(
math.clamp(r, 0, 1),
math.clamp(g, 0, 1),
math.clamp(b, 0, 1)
)
end
end
end
return nil
end
local function buildSchematic()
if BuildRunning then
refreshStatus("Build is already running")
return
end
local loaded = Holograms
if #loaded == 0 then
refreshStatus("Load a schematic first")
return
end
-- Partial-build mode: do not cancel the entire schematic when inventory is short.
-- The per-block placement loop skips unavailable block types and continues
-- building everything for which the server accepts placement.
local enoughMaterials, inventoryMessage = showInventoryReport(loaded)
if not enoughMaterials then
    warn("[Schematic] Partial build enabled: available blocks will be placed; unavailable blocks will be skipped.")
    refreshStatus("Partial build | " .. inventoryMessage)
end
local character = Player.Character or Player.CharacterAdded:Wait()
local humanoid = character:FindFirstChildOfClass("Humanoid")
local backpack = Player:FindFirstChildOfClass("Backpack")
local tool = (backpack and backpack:FindFirstChild("BuildingTool"))
or character:FindFirstChild("BuildingTool")
if not humanoid or not tool then
refreshStatus("BuildingTool not found")
return
end
local scaleTool = findScaleTool(character, backpack)
local scaleRemote = scaleTool and scaleTool:FindFirstChild("RF")
if scaleRemote and not scaleRemote:IsA("RemoteFunction") then scaleRemote = nil end
local paintTool = (backpack and backpack:FindFirstChild("PaintingTool"))
or (character and character:FindFirstChild("PaintingTool"))
local paintRemote = paintTool and paintTool:FindFirstChild("RF")
if paintRemote and not paintRemote:IsA("RemoteFunction") then paintRemote = nil end
local remote = tool:FindFirstChild("RF")
if not remote or not remote:IsA("RemoteFunction") then
refreshStatus("BuildingTool.RF not found")
return
end
local replicated = game:GetService("ReplicatedStorage")
local templates = replicated:FindFirstChild("BuildingParts")
local dataFolder = Player:FindFirstChild("Data")
local guiPlayer = Player:FindFirstChildOfClass("PlayerGui")
local buildGui = guiPlayer and guiPlayer:FindFirstChild("BuildGui")
local inventory = buildGui and buildGui:FindFirstChild("InventoryFrame")
local more = inventory and inventory:FindFirstChild("MoreFrame")
local anchorObj = more and more:FindFirstChild("AnchorBlock")
local anchorValue = anchorObj and anchorObj:FindFirstChild("Anchor")
local rotationObj = more and (more:FindFirstChild("CombineRotation") or more:FindFirstChild("CombineRotations"))
local rotationValue = rotationObj and (rotationObj:FindFirstChild("CombineRotation") or rotationObj:FindFirstChild("Value"))
local bindObj = more and more:FindFirstChild("MakeBinds")
local bindValue = bindObj and bindObj:FindFirstChild("MakeBinds")
-- Force the requested BuildingTool defaults for this build.
if anchorValue and anchorValue:IsA("BoolValue") then anchorValue.Value = true end
if rotationValue and rotationValue:IsA("BoolValue") then rotationValue.Value = false end
if bindValue and bindValue:IsA("BoolValue") then bindValue.Value = false end
if not templates or not dataFolder then
refreshStatus("BuildingParts or Player.Data not found")
return
end
local resolvedTeam = currentPlayerTeam()
local teamColor = resolvedTeam and resolvedTeam.TeamColor or Player.TeamColor
local zone = Workspace:FindFirstChild(tostring(teamColor) .. "Zone")
if not zone or not zone:IsA("BasePart") then
refreshStatus("Team zone not found")
return
end
-- Use the plot zone as a stable reference for every placement.
-- This avoids deriving the transform from an arbitrary existing block or camera state.
local target = zone
local allBlocksRoot = Workspace:FindFirstChild("Blocks")
local playerBlocks = allBlocksRoot and allBlocksRoot:FindFirstChild(Player.Name)
local blocksFolder = playerBlocks
BuildRunning = true
refreshStatus("Equipping BuildingTool...")
pcall(function() humanoid:EquipTool(tool) end)
task.spawn(function()
local placedCount, failedCount = 0, 0
local templateCache = {}
local countValueCache = {}
local secondPassQueue = {}
local claimedModels = {}
local placedModelsByIndex = {}
local paintQueue = {}
local scaleQueue = {}
local PAINT_BATCH_SIZE = 30
local SCALE_BATCH_SIZE = 30
-- Colocação em lotes de 50; ferramentas secundárias em lotes de 30.
local PLACEMENTS_PER_BATCH = 30
local BATCH_DELAY = 0.03
local function flushPaintQueue()
if not paintRemote or #paintQueue == 0 then
 table.clear(paintQueue)
 return
end
local queue = paintQueue
paintQueue = {}
for startIndex = 1, #queue, PAINT_BATCH_SIZE do
 local batch = {}
 local endIndex = math.min(startIndex + PAINT_BATCH_SIZE - 1, #queue)
 for i = startIndex, endIndex do
  batch[#batch + 1] = queue[i]
 end
 local okPaint, paintErr = pcall(function()
  paintRemote:InvokeServer(batch)
 end)
 if not okPaint then
  warn("[Schematic Build] Paint batch failed:", paintErr)
 end
end
end

local function flushScaleQueue()
if not scaleRemote or #scaleQueue == 0 then
 table.clear(scaleQueue)
 return
end
local queue = scaleQueue
scaleQueue = {}
for batchStart = 1, #queue, SCALE_BATCH_SIZE do
 local batchEnd = math.min(batchStart + SCALE_BATCH_SIZE - 1, #queue)
 local total = batchEnd - batchStart + 1
 local finished = 0
 for i = batchStart, batchEnd do
  local entry = queue[i]
  task.spawn(function()
   local okScale, scaleErr = pcall(function()
    scaleRemote:InvokeServer(entry.model, entry.size, entry.cframe)
   end)
   if not okScale then
    warn("[Schematic Build] ScalingTool failed for", entry.blockName, scaleErr)
   end
   finished += 1
  end)
 end
 while finished < total and BuildRunning do
  task.wait()
 end
end
end

local function processItem(index, item)
if not BuildRunning then return end
local modelData = item.data
local blockName = modelData.name
local template = templateCache[blockName]
if template == nil then
    template = templates:FindFirstChild(blockName) or false
    templateCache[blockName] = template
end
local countValue = countValueCache[blockName]
if countValue == nil then
    countValue = dataFolder:FindFirstChild(blockName) or false
    countValueCache[blockName] = countValue
end
if template == false then template = nil end
if countValue == false then countValue = nil end
if not template or not template:IsA("Model") then
failedCount += 1
warn("[Schematic Build] Model not found in BuildingParts:", blockName)
elseif not template.PrimaryPart then
failedCount += 1
warn("[Schematic Build] Model has no PrimaryPart:", blockName)
elseif not countValue or not countValue:IsA("ValueBase") or countValue.Value <= 0 then
failedCount += 1
warn("[Schematic Build] No available units:", blockName)
else
local desiredCF
local okCF, resultCF = pcall(function()
local pivot = pivotFromSchematic(modelData)
if modelData.mainPart then
return pivot * makeCFrame(modelData.mainPart)
end
return pivot
end)
if okCF then desiredCF = resultCF end
if not desiredCF then
failedCount += 1
warn("[Schematic Build] Invalid CFrame:", blockName)
else
local relativeCF = zone.CFrame:ToObjectSpace(desiredCF)
local anchor = true
-- Keep Create Keybinds disabled during placement. BindTool connections are
-- handled in the separate restoration pass after all blocks have been placed.
local makeBinds = false
local args = {
blockName,
countValue.Value,
target,
relativeCF,
anchor,
desiredCF,
false, -- Combine Rotation: OFF
makeBinds
}
-- Snapshot before the call. Each placement searches for a new model
-- with the expected name near the requested position; it does not use the camera or
-- a ChildAdded listener that could capture the same part across 30 calls.
local before = {}
if blocksFolder then
    for _, existing in ipairs(blocksFolder:GetChildren()) do before[existing] = true end
end
local okInvoke, result = pcall(function()
    return remote:InvokeServer(unpack(args))
end)
local newlyPlacedModel = nil
if okInvoke and result ~= false then
    newlyPlacedModel = (typeof(result) == "Instance" and
        (result:IsA("Model") and result or result:FindFirstAncestorOfClass("Model"))) or nil
    if not newlyPlacedModel or newlyPlacedModel.Parent ~= blocksFolder then
        newlyPlacedModel = capturePlacedModel(blocksFolder, before, blockName, desiredCF, claimedModels, 2)
    elseif newlyPlacedModel then
        claimedModels[newlyPlacedModel] = true
    end
end
if okInvoke and result ~= false then
placedCount += 1
if newlyPlacedModel and newlyPlacedModel.Parent == blocksFolder then
    placedModelsByIndex[index] = newlyPlacedModel
end
local propertyModel = newlyPlacedModel
if propertyModel and propertyModel.Parent == blocksFolder then
    local restored, skipped = restoreValueObjects(propertyModel, modelData)
    if skipped > 0 then
        warn("[Schematic Properties]", blockName, "values restored:", restored, "skipped:", skipped)
    end
end
-- ScalingTool: usa o modelo retornado ou capturado por nome + posição;
-- if the part cannot be identified, the tool is not applied to a random model.
local savedSize = getSavedPrimarySize(modelData)
if scaleRemote and savedSize and blocksFolder then
local placedModel = newlyPlacedModel
if placedModel and placedModel.Parent == blocksFolder then
local placedPrimary = getPPart(placedModel)
if placedPrimary then
 table.insert(scaleQueue, {model = placedModel, size = savedSize, cframe = desiredCF, blockName = blockName})
end
end
end
-- PaintingTool usa lotes de {modelo, Color3}; aplicar a cor
-- after placement to avoid one call per individual part.
local savedColor = getSavedPrimaryColor(modelData)
local placedModel = newlyPlacedModel
if paintRemote and savedColor and blocksFolder
and placedModel and placedModel.Parent == blocksFolder then
table.insert(paintQueue, {placedModel, savedColor})
-- Painting is sent after the placement batch finishes.
elseif savedColor then
warn("[Schematic Paint] Could not identify the model to paint:", blockName)
end
else
table.insert(secondPassQueue, {
index = index,
args = args,
blockName = blockName,
modelData = modelData,
desiredCF = desiredCF,
blocksFolder = blocksFolder
})
failedCount += 1
warn("[Schematic Build] Block request failed; it will be retried at the end:", blockName, result)
end
end
end
end

-- Processa a schematic em lotes de 30 chamadas concorrentes.
for batchStart = 1, #loaded, PLACEMENTS_PER_BATCH do
    if not BuildRunning then break end
    claimedModels = {}
    local batchEnd = math.min(batchStart + PLACEMENTS_PER_BATCH - 1, #loaded)
    local pendingCalls = batchEnd - batchStart + 1
    local finishedCalls = 0
    for index = batchStart, batchEnd do
        local item = loaded[index]
        task.spawn(function()
            local ok, err = pcall(processItem, index, item)
            if not ok then
                failedCount += 1
                warn("[Schematic Build] Error on block", index, err)
            end
            finishedCalls += 1
        end)
    end
    while finishedCalls < pendingCalls and BuildRunning do
        task.wait()
    end
    flushScaleQueue()
    flushPaintQueue()
    refreshStatus(string.format("Build %d/%d | placed: %d | failures: %d", batchEnd, #loaded, placedCount, failedCount))
    if batchEnd < #loaded then task.wait(BATCH_DELAY) end
end
-- Segunda passagem somente após terminar a primeira.
if BuildRunning and #secondPassQueue > 0 then
refreshStatus(string.format("First pass complete | retrying %d blocks...", #secondPassQueue))
task.wait(1)
local remainingFailures = 0
for retryIndex, pending in ipairs(secondPassQueue) do
if not BuildRunning then break end
local blocksFolder = pending.blocksFolder
local before = {}
if blocksFolder then
for _, existing in ipairs(blocksFolder:GetChildren()) do
before[existing] = true
end
end
local okRetry, retryResult = pcall(function()
return remote:InvokeServer(unpack(pending.args))
end)
local placedModel = (typeof(retryResult) == "Instance" and
(retryResult:IsA("Model") and retryResult or retryResult:FindFirstAncestorOfClass("Model"))) or nil
if not placedModel and blocksFolder then
for _, existing in ipairs(blocksFolder:GetChildren()) do
if not before[existing] and existing:IsA("Model") and getPPart(existing) then
placedModel = existing
break
end
end
end
if okRetry and retryResult ~= false then
placedCount += 1
if placedModel and placedModel.Parent == blocksFolder and pending.index then
    placedModelsByIndex[pending.index] = placedModel
end
if placedModel and placedModel.Parent == blocksFolder then
    local restored, skipped = restoreValueObjects(placedModel, pending.modelData)
    if skipped > 0 then
        warn("[Schematic Properties]", pending.blockName, "values restored:", restored, "skipped:", skipped)
    end
end
local savedSize = getSavedPrimarySize(pending.modelData)
if scaleRemote and savedSize and placedModel and placedModel.Parent == blocksFolder then
 table.insert(scaleQueue, {model = placedModel, size = savedSize, cframe = pending.desiredCF, blockName = pending.blockName})
end
local savedColor = getSavedPrimaryColor(pending.modelData)
if paintRemote and savedColor and placedModel and placedModel.Parent == blocksFolder then
table.insert(paintQueue, {placedModel, savedColor})
if #paintQueue >= PAINT_BATCH_SIZE then flushPaintQueue() end
end
else
remainingFailures += 1
warn("[Schematic Build] Second pass failed:", pending.blockName, retryResult)
end
refreshStatus(string.format("Second pass %d/%d | placed: %d | remaining failures: %d",
retryIndex, #secondPassQueue, placedCount, remainingFailures))
end
flushScaleQueue()
flushPaintQueue()
failedCount = remainingFailures
end
-- Restore connections after all blocks exist.
-- The official BindTool exposes RF:InvokeServer(actionMap, groupA, keyMap, connected, seatInput).
local function restoreBindConnections()
    local savedConnections = loaded[1] and loaded[1].data and loaded[1].data.bindConnections
    if type(savedConnections) ~= "table" or #savedConnections == 0 then
        warn("[Schematic BindTool] This schematic has no saved connections (or is from an older version).")
        return
    end
    local bindTool = (backpack and backpack:FindFirstChild("BindTool"))
        or (character and character:FindFirstChild("BindTool"))
    local bindRemote = bindTool and bindTool:FindFirstChild("RF")
    if not bindRemote or not bindRemote:IsA("RemoteFunction") then
        warn("[Schematic BindTool] BindTool.RF not found; connections were not restored.")
        return
    end
    local restored, skipped = 0, 0
    for _, edge in ipairs(savedConnections) do
        local modelA = placedModelsByIndex[edge.a]
        local modelB = placedModelsByIndex[edge.b]
        if modelA and modelB and modelA.Parent == blocksFolder and modelB.Parent == blocksFolder then
            local function tag(model, tagName)
                local ok, result = pcall(function() return model:HasTag(tagName) end)
                return ok and result
            end
            local groupA, groupB
            if tag(modelA, "B1") and (tag(modelB, "B2") or tag(modelB, "B3")) then
                groupA, groupB = {modelA}, {modelB}
            elseif tag(modelB, "B1") and (tag(modelA, "B2") or tag(modelA, "B3")) then
                groupA, groupB = {modelB}, {modelA}
            end
            if groupA and groupB then
                local actionMap, keyMap = {}, {}
                for _, source in ipairs(groupA) do
                    for _, child in ipairs(source:GetChildren()) do
                        if child.Name:sub(1, 4) == "Bind" and child.Name ~= "BindingSB" then
                            local action = child:FindFirstChild("ActionName")
                            if action and action:IsA("ValueBase") and type(action.Value) == "string" then
                                actionMap[action.Value] = actionMap[action.Value] or {}
                                table.insert(actionMap[action.Value], child)
                            end
                        end
                    end
                end
                for _, actionName in ipairs(edge.actions or {}) do
                    local savedSourceData
                    for _, hologramItem in ipairs(loaded) do
                        if hologramItem.data and hologramItem.data.id == (edge.actionSource or edge.a) then
                            savedSourceData = hologramItem.data
                            break
                        end
                    end
                    local actions = savedSourceData and savedSourceData.bindActions
                    if actions and actions[actionName] and actions[actionName].keyCode then
                        keyMap[actionName] = actions[actionName].keyCode
                    end
                end
                local seatInput = tag(groupA[1], "SeatInput") or tag(groupB[1], "SeatInput")
                local okBind, bindErr = pcall(function()
                    bindRemote:InvokeServer(actionMap, groupB, keyMap, false, seatInput)
                end)
                if okBind then
                    restored += 1
                else
                    skipped += 1
                    warn("[Schematic BindTool] Failed to restore connection:", bindErr)
                end
            else
                skipped += 1
            end
        else
            skipped += 1
        end
    end
    print("[Schematic BindTool] Ligações solicitadas:", restored, "| ignoradas:", skipped)
end
restoreBindConnections()
-- Verificação final obrigatória, mesmo sem erros detectados durante a colocação.
refreshStatus("Build submitted | waiting for blocks to appear before verification...")
task.wait(1.5)
compareBlocks(true)
local correct, missing, wrong = 0, 0, 0
for _, item in ipairs(Holograms) do
if item.status == "correct" then
correct += 1
elseif item.status == "missing" then
missing += 1
else
wrong += 1
end
end
BuildRunning = false
refreshStatus(string.format("Final check | Correct: %d | Missing: %d | Different: %d", correct, missing, wrong))
end)
end
-- UI
local gui = Instance.new("ScreenGui")
gui.Name = "BABFTSchematicUI"
gui.ResetOnSpawn = false
local parentOk = pcall(function()
gui.Parent = game:GetService("CoreGui")
end)
if not parentOk or not gui.Parent then
gui.Parent = Player:WaitForChild("PlayerGui")
end
local main = Instance.new("Frame")
main.Name = "Main"
main.Size = UDim2.fromOffset(310, 500)
main.Position = UDim2.new(0, 30, 0.3, 0)
main.BackgroundColor3 = Color3.fromRGB(25, 28, 38)
main.BorderSizePixel = 0
main.Active = true
main.Draggable = true
main.Parent = gui
local corner = Instance.new("UICorner")
corner.CornerRadius = UDim.new(0, 10)
corner.Parent = main
local title = Instance.new("TextLabel")
title.Size = UDim2.new(1, 0, 0, 38)
title.BackgroundColor3 = Color3.fromRGB(40, 45, 62)
title.BorderSizePixel = 0
title.Text = "BABFT SCHEMATICS"
title.TextColor3 = Color3.new(1, 1, 1)
title.Font = Enum.Font.GothamBold
title.TextSize = 16
title.Parent = main
local function makeButton(parent, text, y, callback)
local button = Instance.new("TextButton")
button.Size = UDim2.new(1, -20, 0, 34)
button.Position = UDim2.new(0, 10, 0, y)
button.BackgroundColor3 = Color3.fromRGB(50, 100, 175)
button.BorderSizePixel = 0
button.Text = text
button.TextColor3 = Color3.new(1, 1, 1)
button.Font = Enum.Font.GothamSemibold
button.TextSize = 13
button.Parent = parent
local c = Instance.new("UICorner")
c.CornerRadius = UDim.new(0, 6)
c.Parent = button
button.MouseButton1Click:Connect(callback)
return button
end

-- Roblox Handles: use the same axis-drag interaction as the Move tool.
MoveHandles = Instance.new("Handles")
MoveHandles.Name = "SchematicMoveHandles"
MoveHandles.Style = Enum.HandlesStyle.Movement
MoveHandles.Faces = Faces.new(Enum.NormalId.Right, Enum.NormalId.Left, Enum.NormalId.Top, Enum.NormalId.Bottom, Enum.NormalId.Front, Enum.NormalId.Back)
MoveHandles.Visible = false
MoveHandles.Parent = Player:WaitForChild("PlayerGui")
local lastDragDistance = 0
MoveHandles.MouseButton1Down:Connect(function()
    lastDragDistance = 0
end)
MoveHandles.MouseButton1Up:Connect(function()
    lastDragDistance = 0
end)
MoveHandles.MouseDrag:Connect(function(face, distance)
    if not MoveEnabled or #Holograms == 0 then return end
    local deltaDistance = distance - lastDragDistance
    lastDragDistance = distance
    local localNormal = Vector3.FromNormalId(face)
    local delta = MoveAnchor and MoveAnchor.CFrame:VectorToWorldSpace(localNormal) * deltaDistance or localNormal * deltaDistance
    applyHologramMove(delta)
end)

-- Select holograms in screen space instead of raycasting them. This lets ghost
-- parts keep CanQuery=false, so BABFT's own BuildingTool cannot snap/place blocks
-- onto the schematic merely because the user clicked it.
local Mouse = Player:GetMouse()
local UserInputService = game:GetService("UserInputService")
local GuiService = game:GetService("GuiService")
local camera = Workspace.CurrentCamera

local function pointInsideSchematicScreenBounds(mouseX, mouseY)
    camera = Workspace.CurrentCamera
    if not camera then return false end

    for _, item in ipairs(Holograms) do
        for _, entry in ipairs(item.parts or {}) do
            local part = entry.part
            if part and part.Parent and part:IsA("BasePart") then
                local cf = part.CFrame
                local half = part.Size * 0.5
                local minX, minY = math.huge, math.huge
                local maxX, maxY = -math.huge, -math.huge
                local anyOnScreen = false

                for sx = -1, 1, 2 do
                    for sy = -1, 1, 2 do
                        for sz = -1, 1, 2 do
                            local corner = cf:PointToWorldSpace(Vector3.new(
                                half.X * sx, half.Y * sy, half.Z * sz
                            ))
                            local screen, visible = camera:WorldToScreenPoint(corner)
                            if screen.Z > 0 then
                                anyOnScreen = true
                                minX = math.min(minX, screen.X)
                                minY = math.min(minY, screen.Y)
                                maxX = math.max(maxX, screen.X)
                                maxY = math.max(maxY, screen.Y)
                            end
                        end
                    end
                end

                if anyOnScreen and mouseX >= minX - 3 and mouseX <= maxX + 3
                    and mouseY >= minY - 3 and mouseY <= maxY + 3 then
                    return true
                end
            end
        end
    end
    return false
end

UserInputService.InputBegan:Connect(function(input, gameProcessed)
    if input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end

    -- Ignore clicks on this script's own control panel.
    local mousePos = UserInputService:GetMouseLocation()
    local guiObjects = GuiService:GetGuiObjectsAtPosition(mousePos.X, mousePos.Y)
    for _, object in ipairs(guiObjects) do
        if object == gui or object:IsDescendantOf(gui) then
            return
        end
    end

    -- Mouse.X/Y match WorldToScreenPoint's screen coordinates.
    local clickedSchematic = pointInsideSchematicScreenBounds(Mouse.X, Mouse.Y)
    if clickedSchematic then
        if not MoveEnabled then
            setMoveMode(true)
        else
            updateMoveAnchor()
        end
        refreshStatus("Drag the arrows to move the hologram; click outside to hide them.")
    elseif MoveEnabled then
        setMoveMode(false)
        refreshStatus("Hologram movement hidden")
    end
end)

local function makeTextBox(parent, placeholder, y)
local box = Instance.new("TextBox")
box.Size = UDim2.new(1, -20, 0, 32)
box.Position = UDim2.new(0, 10, 0, y)
box.BackgroundColor3 = Color3.fromRGB(40, 44, 57)
box.BorderSizePixel = 0
box.PlaceholderText = placeholder
box.PlaceholderColor3 = Color3.fromRGB(170, 175, 190)
box.Text = ""
box.TextColor3 = Color3.new(1, 1, 1)
box.Font = Enum.Font.Gotham
box.TextSize = 13
box.ClearTextOnFocus = false
box.Parent = parent
local c = Instance.new("UICorner")
c.CornerRadius = UDim.new(0, 6)
c.Parent = box
return box
end
local nameBox = makeTextBox(main, "Schematic name...", 48)
local status = Instance.new("TextLabel")
status.Size = UDim2.new(1, -20, 0, 18)
status.Position = UDim2.new(0, 10, 1, -20)
status.BackgroundTransparency = 1
status.Text = "Ready"
status.TextColor3 = Color3.fromRGB(200, 205, 220)
status.Font = Enum.Font.Gotham
status.TextSize = 11
status.Parent = main
refreshStatus = function(text)
status.Text = text
end
local selectedSavePlayer = Player
local savePlayerButton
savePlayerButton = makeButton(main, "Save player: " .. Player.Name, 87, function()
local playerList = Players:GetPlayers()
table.sort(playerList, function(a, b) return a.Name:lower() < b.Name:lower() end)
if #playerList == 0 then return end
local currentIndex = 0
for i, candidate in ipairs(playerList) do
if candidate == selectedSavePlayer then
currentIndex = i
break
end
end
currentIndex = (currentIndex % #playerList) + 1
selectedSavePlayer = playerList[currentIndex]
savePlayerButton.Text = "Save player: " .. selectedSavePlayer.Name
refreshStatus("Source: " .. selectedSavePlayer.Name)
end)
makeButton(main, "Save Build", 127, function()
local name = nameBox.Text
if name == "" then
refreshStatus("Enter a name first!")
return
end
if saveSchematic(name, selectedSavePlayer.Name) then
refreshStatus("Saved: " .. safeFileName(name) .. " (" .. selectedSavePlayer.Name .. ")")
else
refreshStatus("Could not save schematic")
end
end)
makeButton(main, "Load Schematic", 167, function()
local old = main:FindFirstChild("SchematicList")
if old then
old:Destroy()
end
local list = Instance.new("Frame")
list.Name = "SchematicList"
list.Size = UDim2.fromOffset(280, 240)
list.Position = UDim2.new(1, 10, 0, 35)
list.BackgroundColor3 = Color3.fromRGB(28, 31, 43)
list.BorderSizePixel = 0
list.Parent = main
local c = Instance.new("UICorner")
c.CornerRadius = UDim.new(0, 8)
c.Parent = list
local heading = Instance.new("TextLabel")
heading.Size = UDim2.new(1, -40, 0, 30)
heading.Position = UDim2.new(0, 10, 0, 0)
heading.BackgroundTransparency = 1
heading.Text = "Available Schematics"
heading.TextColor3 = Color3.new(1, 1, 1)
heading.Font = Enum.Font.GothamBold
heading.TextSize = 13
heading.Parent = list
local close = Instance.new("TextButton")
close.Size = UDim2.fromOffset(28, 28)
close.Position = UDim2.new(1, -32, 0, 2)
close.BackgroundTransparency = 1
close.Text = "X"
close.TextColor3 = Color3.new(1, 1, 1)
close.Font = Enum.Font.GothamBold
close.TextSize = 14
close.Parent = list
close.MouseButton1Click:Connect(function()
list:Destroy()
end)
local scroll = Instance.new("ScrollingFrame")
scroll.Size = UDim2.new(1, -16, 1, -40)
scroll.Position = UDim2.new(0, 8, 0, 35)
scroll.BackgroundTransparency = 1
scroll.BorderSizePixel = 0
scroll.ScrollBarThickness = 5
scroll.CanvasSize = UDim2.new(0, 0, 0, 0)
scroll.AutomaticCanvasSize = Enum.AutomaticSize.Y
scroll.Parent = list
local layout = Instance.new("UIListLayout")
layout.Padding = UDim.new(0, 5)
layout.Parent = scroll
local found = listSchematics()
if #found == 0 then
local empty = Instance.new("TextLabel")
empty.Size = UDim2.new(1, -8, 0, 50)
empty.BackgroundTransparency = 1
empty.Text = "No schematics found."
empty.TextColor3 = Color3.fromRGB(190, 195, 210)
empty.Font = Enum.Font.Gotham
empty.TextSize = 12
empty.Parent = scroll
end
for _, entry in ipairs(found) do
local row = Instance.new("Frame")
row.Size = UDim2.new(1, -8, 0, 36)
row.BackgroundTransparency = 1
row.Parent = scroll
local button = Instance.new("TextButton")
button.Size = UDim2.new(1, -40, 1, 0)
button.Position = UDim2.new(0, 0, 0, 0)
button.BackgroundColor3 = Color3.fromRGB(48, 80, 125)
button.BorderSizePixel = 0
button.Text = entry.name .. " (" .. #entry.data.blocks .. " blocks)"
button.TextColor3 = Color3.new(1, 1, 1)
button.Font = Enum.Font.Gotham
button.TextSize = 12
button.TextTruncate = Enum.TextTruncate.AtEnd
button.Parent = row
local bc = Instance.new("UICorner")
bc.CornerRadius = UDim.new(0, 5)
bc.Parent = button
button.MouseButton1Click:Connect(function()
loadSchematic(entry)
local enoughMaterials, inventoryMessage = showInventoryReport(Holograms)
refreshStatus(enoughMaterials and ("Loaded: " .. entry.name .. " | " .. inventoryMessage) or inventoryMessage)
list:Destroy()
end)
local deleteButton = Instance.new("TextButton")
deleteButton.Size = UDim2.new(0, 34, 1, 0)
deleteButton.Position = UDim2.new(1, -34, 0, 0)
deleteButton.BackgroundColor3 = Color3.fromRGB(170, 55, 55)
deleteButton.BorderSizePixel = 0
deleteButton.Text = "X"
deleteButton.TextColor3 = Color3.new(1, 1, 1)
deleteButton.Font = Enum.Font.GothamBold
deleteButton.TextSize = 14
deleteButton.Parent = row
local dc = Instance.new("UICorner")
dc.CornerRadius = UDim.new(0, 5)
dc.Parent = deleteButton
deleteButton.MouseButton1Click:Connect(function()
if type(delfile) ~= "function" then
refreshStatus("This executor does not support file deletion")
return
end
local ok, err = pcall(function()
delfile(entry.path)
end)
if ok then
row:Destroy()
refreshStatus("Schematic deleted: " .. entry.name)
else
refreshStatus("Delete failed: " .. tostring(err))
end
end)
end
end)

-- IMAGE IMPORT FILE BROWSER
-- Browse folders recursively by entering them; Back returns to the previous folder.
local IMAGE_DIRS = {
    [[C:\Users\marco\AppData\Local\Xeno\workspace\Images]],
    [[workspace\Images]],
    [[Images]],
    [[C:\Users\marco\AppData\Local\Xeno\workspace]]
}

local function imageExtension(path)
    local lower = tostring(path):lower()
    return lower:match("%.png$") or lower:match("%.jpe?g$") or lower:match("%.bmp$")
end

local function normalizePath(path)
    path = tostring(path or ""):gsub("/", "\\")
    while path:find("\\\\", 1, true) and not path:match("^\\\\") do
        path = path:gsub("\\\\", "\\")
    end
    return path
end

local function joinPath(parent, child)
    parent, child = normalizePath(parent), normalizePath(child)
    if child:match("^%a:\\") or child:match("^\\\\") then return child end
    if child:lower() == parent:lower() or child:sub(1, #parent + 1):lower() == (parent .. "\\"):lower() then
        return child
    end
    -- Some executors return paths relative to their workspace, e.g. workspace\\Images\\x.png.
    local childLower, parentLower = child:lower(), parent:lower()
    local marker = parentLower:match("workspace\\(.+)$")
    if marker and childLower:sub(1, #marker + 1) == (marker .. "\\") then
        return normalizePath(parent:match("^(.*workspace)") .. "\\" .. child)
    end
    return parent:gsub("\\+$", "") .. "\\" .. child:gsub("^\\+", "")
end

local function pathIsFolder(path)
    if type(isfolder) == "function" then
        local ok, result = pcall(isfolder, path)
        if ok and result == true then return true end
    end
    return false
end

local function parentPath(path)
    path = normalizePath(path):gsub("\\+$", "")
    local parent = path:match("^(.*)\\[^\\]+$")
    if parent == "" then return nil end
    if parent and parent:match("^%a:$") then parent = parent .. "\\" end
    return parent
end

local function samePath(a, b)
    a = normalizePath(a):gsub("\\+$", "")
    b = normalizePath(b):gsub("\\+$", "")
    return a:lower() == b:lower()
end

local function listDirectory(path)
    if type(listfiles) ~= "function" then
        return nil, "Xeno listfiles() is unavailable"
    end

    path = normalizePath(path):gsub("\\+$", "")
    local attempts, attemptSeen = {}, {}
    local function addAttempt(value)
        value = normalizePath(value)
        if value ~= "" and not attemptSeen[value:lower()] then
            attemptSeen[value:lower()] = true
            attempts[#attempts + 1] = value
        end
    end

    addAttempt(path)
    local afterWorkspace = path:match("[Ww]orkspace\\(.+)$")
    if afterWorkspace then
        addAttempt("workspace\\" .. afterWorkspace)
        addAttempt(afterWorkspace)
    end
    -- Xeno may expose the same workspace through a relative Images path.
    addAttempt("Images")
    addAttempt("workspace\\Images")

    local entries, usedPath
    for _, candidate in ipairs(attempts) do
        local ok, result = pcall(listfiles, candidate)
        if ok and type(result) == "table" then
            -- Prefer the first non-empty result, but keep empty result as fallback.
            if #result > 0 then
                entries, usedPath = result, candidate
                break
            elseif entries == nil then
                entries, usedPath = result, candidate
            end
        end
    end
    if type(entries) ~= "table" then
        return nil, "Cannot open folder: " .. tostring(path)
    end

    local function workspaceRelative(value)
        value = normalizePath(value)
        local rel = value:match("^[^:]*[Ww]orkspace\\(.+)$")
        if rel then return rel end
        rel = value:match("^workspace\\(.+)$")
        if rel then return rel end
        return nil
    end

    local currentRel = workspaceRelative(path)
    local currentBase = path:match("([^\\]+)$") or path
    local normalizedEntries, seen = {}, {}

    for _, entry in ipairs(entries) do
        if type(entry) == "string" then
            local raw = normalizePath(entry)
            local fullPath

            if raw:match("^%a:\\") or raw:match("^\\\\") then
                fullPath = raw
            else
                -- If listfiles returns a workspace-relative path, map it back to
                -- the current absolute path only when it belongs to this directory.
                local rel = workspaceRelative(raw)
                if rel then
                    if currentRel then
                        local currentLower, relLower = currentRel:lower(), rel:lower()
                        if relLower:sub(1, #currentLower + 1) == currentLower .. "\\" then
                            fullPath = path .. "\\" .. rel:sub(#currentRel + 2)
                        end
                    elseif currentBase:lower() == "images" and rel:lower():sub(1, 7) == "images\\" then
                        fullPath = path .. "\\" .. rel:sub(8)
                    end
                end
                if not fullPath then
                    local rawLower, pathLower = raw:lower(), path:lower()
                    local pathTail = path:match("([^\\]+\\[^\\]+)$")
                    if rawLower:sub(1, #pathLower + 1) == pathLower .. "\\" then
                        fullPath = raw
                    elseif pathTail and rawLower:sub(1, #pathTail + 1) == pathTail:lower() .. "\\" then
                        fullPath = path .. "\\" .. raw:sub(#pathTail + 2)
                    elseif not raw:find("\\", 1, true) then
                        fullPath = path .. "\\" .. raw
                    end
                end
            end

            if fullPath then
                fullPath = normalizePath(fullPath)
                local parent = parentPath(fullPath)
                if parent and samePath(parent, path) and not seen[fullPath:lower()] then
                    seen[fullPath:lower()] = true
                    normalizedEntries[#normalizedEntries + 1] = fullPath
                end
            end
        end
    end

    -- Some executor builds do not accept absolute paths in isfolder(). Try the
    -- equivalent workspace-relative path too, so folders remain recognizable.
    local function isDirectory(entryPath)
        if pathIsFolder(entryPath) then return true end
        if type(isfolder) == "function" then
            local rel = workspaceRelative(entryPath)
            if rel then
                local candidates = {"workspace\\" .. rel, rel}
                for _, candidate in ipairs(candidates) do
                    local ok, result = pcall(isfolder, candidate)
                    if ok and result == true then return true end
                end
            end
        end
        -- Folder names usually have no extension; use this only as fallback.
        return not entryPath:match("%.[^\\]+$")
    end

    table.sort(normalizedEntries, function(a, b)
        local af, bf = isDirectory(a), isDirectory(b)
        if af ~= bf then return af end
        return a:lower() < b:lower()
    end)
    return normalizedEntries
end

local function openImageImport()
    local old = main:FindFirstChild("ImageImportWindow")
    if old then old:Destroy() end

    local popup = Instance.new("Frame")
    popup.Name = "ImageImportWindow"
    popup.Size = UDim2.fromOffset(390, 455)
    popup.Position = UDim2.new(1, 10, 0, 35)
    popup.BackgroundColor3 = Color3.fromRGB(28, 31, 43)
    popup.BorderSizePixel = 0
    popup.Parent = main
    Instance.new("UICorner", popup).CornerRadius = UDim.new(0, 8)

    local title = Instance.new("TextLabel")
    title.Size = UDim2.new(1, -44, 0, 30)
    title.Position = UDim2.fromOffset(10, 3)
    title.BackgroundTransparency = 1
    title.Text = "Import Image"
    title.TextColor3 = Color3.new(1, 1, 1)
    title.Font = Enum.Font.GothamBold
    title.TextSize = 14
    title.TextXAlignment = Enum.TextXAlignment.Left
    title.Parent = popup

    local close = Instance.new("TextButton")
    close.Size = UDim2.fromOffset(28, 28)
    close.Position = UDim2.new(1, -32, 0, 3)
    close.BackgroundTransparency = 1
    close.Text = "X"
    close.TextColor3 = Color3.new(1, 1, 1)
    close.Font = Enum.Font.GothamBold
    close.TextSize = 14
    close.Parent = popup
    close.MouseButton1Click:Connect(function() popup:Destroy() end)

    local folderLabel = Instance.new("TextLabel")
    folderLabel.Size = UDim2.new(1, -20, 0, 44)
    folderLabel.Position = UDim2.fromOffset(10, 30)
    folderLabel.BackgroundTransparency = 1
    folderLabel.Text = "PATH: (select a starting folder)"
    folderLabel.TextColor3 = Color3.fromRGB(190, 200, 220)
    folderLabel.Font = Enum.Font.Code
    folderLabel.TextSize = 10
    folderLabel.TextWrapped = true
    folderLabel.TextTruncate = Enum.TextTruncate.None
    folderLabel.TextXAlignment = Enum.TextXAlignment.Left
    folderLabel.TextYAlignment = Enum.TextYAlignment.Center
    folderLabel.Parent = popup

    local search = Instance.new("TextBox")
    search.Size = UDim2.new(1, -20, 0, 28)
    search.Position = UDim2.fromOffset(10, 77)
    search.BackgroundColor3 = Color3.fromRGB(20, 23, 32)
    search.BorderSizePixel = 0
    search.PlaceholderText = "Search image or folder..."
    search.Text = ""
    search.TextColor3 = Color3.new(1, 1, 1)
    search.PlaceholderColor3 = Color3.fromRGB(150, 155, 170)
    search.Font = Enum.Font.Gotham
    search.TextSize = 11
    search.ClearTextOnFocus = false
    search.Parent = popup
    Instance.new("UICorner", search).CornerRadius = UDim.new(0, 5)

    local back = Instance.new("TextButton")
    back.Size = UDim2.fromOffset(85, 26)
    back.Position = UDim2.fromOffset(10, 110)
    back.BackgroundColor3 = Color3.fromRGB(65, 70, 88)
    back.BorderSizePixel = 0
    back.Text = "< Back"
    back.TextColor3 = Color3.new(1, 1, 1)
    back.Font = Enum.Font.Gotham
    back.TextSize = 11
    back.Parent = popup
    Instance.new("UICorner", back).CornerRadius = UDim.new(0, 5)

    local refresh = Instance.new("TextButton")
    refresh.Size = UDim2.fromOffset(85, 26)
    refresh.Position = UDim2.fromOffset(101, 110)
    refresh.BackgroundColor3 = Color3.fromRGB(48, 80, 125)
    refresh.BorderSizePixel = 0
    refresh.Text = "Refresh"
    refresh.TextColor3 = Color3.new(1, 1, 1)
    refresh.Font = Enum.Font.Gotham
    refresh.TextSize = 11
    refresh.Parent = popup
    Instance.new("UICorner", refresh).CornerRadius = UDim.new(0, 5)

    local scroll = Instance.new("ScrollingFrame")
    scroll.Size = UDim2.new(1, -20, 0, 215)
    scroll.Position = UDim2.fromOffset(10, 141)
    scroll.BackgroundColor3 = Color3.fromRGB(20, 23, 32)
    scroll.BorderSizePixel = 0
    scroll.ScrollBarThickness = 5
    scroll.CanvasSize = UDim2.new(0, 0, 0, 0)
    scroll.AutomaticCanvasSize = Enum.AutomaticSize.Y
    scroll.Parent = popup
    Instance.new("UICorner", scroll).CornerRadius = UDim.new(0, 5)
    local layout = Instance.new("UIListLayout")
    layout.Padding = UDim.new(0, 4)
    layout.Parent = scroll

    local scaleBox = Instance.new("TextBox")
    scaleBox.Size = UDim2.fromOffset(88, 25)
    scaleBox.Position = UDim2.fromOffset(10, 360)
    scaleBox.BackgroundColor3 = Color3.fromRGB(40, 44, 57)
    scaleBox.BorderSizePixel = 0
    scaleBox.Text = "10"
    scaleBox.PlaceholderText = "Studs"
    scaleBox.TextColor3 = Color3.new(1, 1, 1)
    scaleBox.Font = Enum.Font.Gotham
    scaleBox.TextSize = 11
    scaleBox.Parent = popup
    Instance.new("UICorner", scaleBox).CornerRadius = UDim.new(0, 4)

    local heightBox = Instance.new("TextBox")
    heightBox.Size = UDim2.fromOffset(72, 25)
    heightBox.Position = UDim2.fromOffset(104, 360)
    heightBox.BackgroundColor3 = Color3.fromRGB(40, 44, 57)
    heightBox.BorderSizePixel = 0
    heightBox.Text = "0.2"
    heightBox.PlaceholderText = "Size Y"
    heightBox.TextColor3 = Color3.new(1, 1, 1)
    heightBox.Font = Enum.Font.Gotham
    heightBox.TextSize = 11
    heightBox.Parent = popup
    Instance.new("UICorner", heightBox).CornerRadius = UDim.new(0, 4)

    local materialBox = Instance.new("TextBox")
    materialBox.Size = UDim2.fromOffset(174, 25)
    materialBox.Position = UDim2.fromOffset(184, 360)
    materialBox.BackgroundColor3 = Color3.fromRGB(40, 44, 57)
    materialBox.BorderSizePixel = 0
    materialBox.Text = "PlasticBlock"
    materialBox.PlaceholderText = "BABFT block name"
    materialBox.TextColor3 = Color3.new(1, 1, 1)
    materialBox.Font = Enum.Font.Gotham
    materialBox.TextSize = 10
    materialBox.Parent = popup
    Instance.new("UICorner", materialBox).CornerRadius = UDim.new(0, 4)

    local status = Instance.new("TextLabel")
    status.Size = UDim2.new(1, -20, 0, 34)
    status.Position = UDim2.fromOffset(10, 386)
    status.BackgroundTransparency = 1
    status.Text = "Choose an image file."
    status.TextColor3 = Color3.fromRGB(220, 220, 230)
    status.Font = Enum.Font.Gotham
    status.TextSize = 10
    status.TextWrapped = true
    status.TextXAlignment = Enum.TextXAlignment.Left
    status.Parent = popup

    local import = Instance.new("TextButton")
    import.Size = UDim2.new(1, -20, 0, 28)
    import.Position = UDim2.fromOffset(10, 411)
    import.BackgroundColor3 = Color3.fromRGB(35, 130, 85)
    import.BorderSizePixel = 0
    import.Text = "Import selected image"
    import.TextColor3 = Color3.new(1, 1, 1)
    import.Font = Enum.Font.GothamBold
    import.TextSize = 11
    import.Parent = popup
    Instance.new("UICorner", import).CornerRadius = UDim.new(0, 5)

    local currentPath, selectedPath
    local history = {}

    local function clearRows()
        for _, child in ipairs(scroll:GetChildren()) do
            if child ~= layout then child:Destroy() end
        end
    end

    local render
    local function enterFolder(path)
        history[#history + 1] = currentPath
        currentPath = path
        selectedPath = nil
        render()
    end

    render = function()
        clearRows()
        folderLabel.Text = "PATH: " .. normalizePath(currentPath or "(none)")
        back.Active = #history > 0
        back.AutoButtonColor = #history > 0
        local entries, err = listDirectory(currentPath)
        if not entries then
            status.Text = err or "Could not read this folder."
            return
        end
        local query = search.Text:lower()
        local shown = 0
        for _, path in ipairs(entries) do
            if type(path) == "string" then
                path = normalizePath(path)
                local name = path:gsub("\\", "/"):match("([^/]+)$") or path
                local folder = pathIsFolder(path) or (not path:match("%.[^\\]+$"))
                local isImage = imageExtension(path)
                if (folder or isImage) and (query == "" or name:lower():find(query, 1, true)) then
                    shown += 1
                    local item = Instance.new("TextButton")
                    item.Size = UDim2.new(1, -8, 0, 27)
                    item.BackgroundColor3 = (path == selectedPath) and Color3.fromRGB(65, 100, 145)
                        or (folder and Color3.fromRGB(28, 75, 43) or Color3.fromRGB(42, 47, 62))
                    item.BorderSizePixel = 0
                    item.Text = (folder and "[DIR]  " or "[IMG]  ") .. name
                    item.TextColor3 = folder and Color3.fromRGB(100, 255, 130) or Color3.fromRGB(240, 240, 245)
                    item.Font = Enum.Font.Gotham
                    item.TextSize = 11
                    item.TextXAlignment = Enum.TextXAlignment.Left
                    item.Parent = scroll
                    Instance.new("UICorner", item).CornerRadius = UDim.new(0, 4)
                    item.MouseButton1Click:Connect(function()
                        if folder then
                            enterFolder(path)
                        else
                            selectedPath = path
                            status.Text = "Selected: " .. path
                            render()
                        end
                    end)
                end
            end
        end
        if shown == 0 then
            local empty = Instance.new("TextLabel")
            empty.Size = UDim2.new(1, -8, 0, 34)
            empty.BackgroundTransparency = 1
            empty.Text = query ~= "" and "No matching image/folder." or "No image files or subfolders."
            empty.TextColor3 = Color3.fromRGB(180, 190, 210)
            empty.Font = Enum.Font.Gotham
            empty.TextSize = 11
            empty.Parent = scroll
        end
        status.Text = selectedPath and ("Selected: " .. normalizePath(selectedPath))
            or (tostring(shown) .. " item(s) shown | folders are green.")
    end

    back.MouseButton1Click:Connect(function()
        if #history > 0 then
            currentPath = table.remove(history)
            selectedPath = nil
            render()
        end
    end)
    refresh.MouseButton1Click:Connect(render)
    search:GetPropertyChangedSignal("Text"):Connect(render)

    local function startAt(index)
        local path = IMAGE_DIRS[index]
        if path then
            local entries = listDirectory(path)
            if entries ~= nil then
                currentPath = path
                render()
                return true
            end
        end
        return false
    end

    if not startAt(1) and not startAt(2) then
        currentPath = IMAGE_DIRS[1]
        status.Text = "Couldn't open Images. Check the Xeno workspace folder."
        render()
    end

    -- Decode uncompressed 24/32-bit BMP directly. PNG/JPEG can be supplied by an
    -- executor-provided decodeImagePixels(path) function returning {width,height,pixels}.
    local function decodeBMP(path)
        local ok, bytes = pcall(readfile, path)
        if not ok or type(bytes) ~= "string" or #bytes < 54 then return nil, "Could not read BMP" end
        local function u16(i) local a,b=string.byte(bytes,i,i+1); if not b then return 0 end; return a+b*256 end
        local function u32(i) local a,b,c,d=string.byte(bytes,i,i+3); if not d then return 0 end; return a+b*256+c*65536+d*16777216 end
        if string.sub(bytes,1,2) ~= "BM" then return nil, "Not a BMP file" end
        local offset, dib = u32(11), u32(15)
        local width, height, planes, bpp, compression = u32(19), u32(23), u16(27), u16(29), u32(31)
        if dib < 40 or width < 1 or height < 1 or width > 512 or height > 512 then return nil, "BMP size/header unsupported (max 512x512)" end
        if planes ~= 1 or (bpp ~= 24 and bpp ~= 32) or compression ~= 0 then return nil, "BMP must be uncompressed 24/32-bit" end
        local stride = math.floor((width*bpp+31)/32)*4
        local pixels = {}
        for y=1,height do
            pixels[y] = {}
            local srcY = (height > 0) and (height-y) or (y-1)
            for x=1,width do
                local pos = offset + srcY*stride + (x-1)*(bpp/8) + 1
                local b,g,r,a = string.byte(bytes,pos,pos+3)
                if not r then return nil, "BMP pixel data is truncated" end
                pixels[y][x] = {r,g,b,a or 255}
            end
        end
        return {width=width,height=height,pixels=pixels}
    end

    local function decodeSelectedImage(path)
        local ext = path:lower():match("%.([%w]+)$")
        if ext == "bmp" then return decodeBMP(path) end
        local env = (type(getgenv) == "function" and getgenv()) or _G
        local decoder = rawget(env, "decodeImagePixels")
        if type(decoder) == "function" then
            local ok, result = pcall(decoder, path)
            if ok and type(result) == "table" and tonumber(result.width) and tonumber(result.height) and type(result.pixels) == "table" then
                return result
            end
            return nil, "decodeImagePixels(path) failed or returned an invalid pixel table"
        end
        return nil, "This executor has no PNG/JPG pixel decoder. Convert the image to uncompressed 24/32-bit BMP to import it."
    end

    local function importPixels(image)
        local width, height = math.floor(tonumber(image.width) or 0), math.floor(tonumber(image.height) or 0)
        if width < 1 or height < 1 or width*height > 20000 then return false, "Invalid image or over 20,000 pixels; resize it first." end
        local studs = tonumber(scaleBox.Text)
        local sizeY = tonumber(heightBox.Text)
        local blockName = materialBox.Text:gsub("^%s+", ""):gsub("%s+$", "")
        if not studs or studs <= 0 or not sizeY or sizeY <= 0 or blockName == "" then
            return false, "Enter positive Scale/Size Y and a BABFT block name."
        end
        local templates = game:GetService("ReplicatedStorage"):FindFirstChild("BuildingParts")
        if not templates or not templates:FindFirstChild(blockName) then return false, "BABFT block not found: " .. blockName end
        local zone = Workspace:FindFirstChild(tostring((currentPlayerTeam() and currentPlayerTeam().TeamColor) or Player.TeamColor) .. "Zone")
        if not zone or not zone:IsA("BasePart") then return false, "Could not find your team zone." end
        -- Scale means studs per 100 source pixels: 200x100 at scale 10 becomes 20x10 studs.
        local pixelSize = studs / 100
        local px, pz = pixelSize, pixelSize
        if px < 0.05 or sizeY < 0.05 then return false, "Block dimensions cannot be below 0.05 studs." end
        local totalWidth, totalDepth = width * px, height * pz
        local blocks = {}
        local id = 0
        -- Center image in front of the plot zone; one schematic block per non-transparent pixel.
        local origin = zone.CFrame * CFrame.new(-totalWidth/2, 2 + sizeY/2, -totalDepth/2)
        for y=1,height do
            local row = image.pixels[y]
            if type(row) == "table" then
                for x=1,width do
                    local rgba = row[x]
                    if type(rgba) == "table" then
                        local r,g,b,a = tonumber(rgba[1]),tonumber(rgba[2]),tonumber(rgba[3]),tonumber(rgba[4]) or 255
                        if r and g and b and a > 8 then
                            id += 1
                            local cf = CFrame.new((x-0.5)*px, 0, (y-0.5)*pz)
                            local partData = {class="Part",name="PPart",cf={cf:GetComponents()},size={px,sizeY,pz},color={math.clamp(r/255,0,1),math.clamp(g/255,0,1),math.clamp(b/255,0,1)},transparency=0,reflectance=0,material="SmoothPlastic",anchored=true,canCollide=false,properties={}}
                            blocks[#blocks+1] = {id=id,name=blockName,pivot={origin:GetComponents()},mainPart={cf:GetComponents()},parts={partData},coordinateSpace="world"}
                        end
                    end
                end
            end
        end
        if #blocks == 0 then return false, "No visible pixels found in image." end
        removeHolograms()
        local folder = getHologramFolder()
        for _, data in ipairs(blocks) do createHologram(data, folder) end
        Checking = true
        refreshStatus("Image loaded: " .. width .. "x" .. height .. " | " .. #blocks .. " pixels")
        print("[Import Image] Loaded", #blocks, "pixel blocks from", width .. "x" .. height)
        return true, #blocks
    end

    import.MouseButton1Click:Connect(function()
        if not selectedPath then status.Text = "Select an image first."; return end
        status.Text = "Reading image..."
        local image, err = decodeSelectedImage(selectedPath)
        if not image then
            status.Text = err or "Could not decode image."
            warn("[Import Image] " .. tostring(err))
            return
        end
        local ok, result = importPixels(image)
        if not ok then status.Text = tostring(result); warn("[Import Image] " .. tostring(result)); return end
        status.Text = "Imported " .. tostring(result) .. " pixel blocks. Click BUILD SCHEMATIC to place them."
    end)
end

local messagesButton
messagesButton = makeButton(main, "Floating Messages: OFF", 327, function()
ShowAllHologramMessages = not ShowAllHologramMessages
messagesButton.Text = "Floating Messages: " .. (ShowAllHologramMessages and "ON" or "OFF")
applyHologramVisibility()
if ShowAllHologramMessages then compareBlocks() end
end)
local hologramsButton
hologramsButton = makeButton(main, "Holograms: ON", 367, function()
ShowHolograms = not ShowHolograms
hologramsButton.Text = "Holograms: " .. (ShowHolograms and "ON" or "OFF")
applyHologramVisibility()
end)
-- Movement handles now appear automatically when the player clicks a schematic,
-- and disappear when they click anywhere outside the hologram.
makeButton(main, "BUILD SCHEMATIC", 287, function()
buildSchematic()
end)
makeButton(main, "Remove Holograms", 207, function()
removeHolograms()
refreshStatus("Holograms removed")
end)

makeButton(main, "Import Image...", 407, function()
openImageImport()
end)
makeButton(main, "Verify Now", 247, function()
compareBlocks(true)
local correct = 0
local missing = 0
local wrong = 0
for _, item in ipairs(Holograms) do
if item.status == "correct" then
correct = correct + 1
elseif item.status == "missing" then
missing = missing + 1
else
wrong = wrong + 1
end
end
refreshStatus(
"Correct: " .. correct
.. " | Missing: " .. missing
.. " | Errors: " .. wrong
)
end)
RunService.Heartbeat:Connect(function()
if Checking and os.clock() - LastCheck >= CHECK_INTERVAL then
LastCheck = os.clock()
compareBlocks()
end
end)
print("BABFT Schematics System initialized!")

How to Execute Build A Boat For Treasure Script – BABFT Schematics

Step 1: Copy Code: Click the Copy Code button in the viewer above to grab the clean Lua loadstring.

Step 2: Launch Executor: Open a compatible, unpatched executor. If you need one, check our Roblox Executors Directory.

Step 3: Join Experience: Launch Roblox and join Build A Boat For Treasure.

Step 4: Attach & Run: Attach/inject your executor into the Roblox process, paste the code into the script tab, and press Execute.

Created By

Aquilesinho
Aquilesinho
Script Creator
View Author Profile

Roblox Experience

Build A Boat For Treasure
Build A Boat For Treasure
By Chillz Studios
Active Players:13.5K
Play on Roblox

Features & Information

Execute Build A Boat For Treasure Script – BABFT Schematics. Key system required, created by Aquilesinho, updated Oct 22, 2026. Tested and working on supported executors.
#BABFT#Build A Boat For Treasure#Schematics#Building#Build Tools

Script Changelog (1)

@AquilesinhoOct 22, 2026

Initial release of BABFT Schematics

Community Comments (5)

AquilesinhoAquilesinhoOct 25, 2026

Guys, I made a HUGE MISTAKE WHEN PUBLISHING! I put my Windows username in the code 😭 I published the corrected version, now it works!

MocjEscanorMocjEscanormodOct 25, 2026

Hello! Please remove the game name in the title. Thankyou!

Replying to @MocjEscanor
AquilesinhoAquilesinhoOct 25, 2026

@MocjEscanor Ok 👍

k035946k035946Oct 23, 2026

it crashes the entire game

Replying to @k035946
AquilesinhoAquilesinhoOct 24, 2026

@k035946 Thanks for reporting this! The script hasn't crashed during my own tests, but I'll investigate whether large schematics or performance limitations could be causing this. Do you remember what you were doing when the game crashed?

Frequently Asked Questions

How do I execute Build A Boat For Treasure Script – BABFT Schematics in Roblox?

To run Build A Boat For Treasure Script – BABFT Schematics, copy the Lua loadstring from the code viewer above, launch Roblox and join Build A Boat For Treasure, attach a working executor (such as Potassium, Solara, or Delta from our Executors directory), paste the code into your executor console, and click Execute.

Is Build A Boat For Treasure Script – BABFT Schematics keyless or does it require an access key?

Build A Boat For Treasure Script – BABFT Schematics requires an access key. You can retrieve the official key directly using the Get Script Key link provided above.

Is Build A Boat For Treasure Script – BABFT Schematics currently safe and working?

This script is active and operational. Always test scripts using a secondary (alt) Roblox account to safeguard your main profile against anti-cheat bans.