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!")