Files
aya/client/common/content/scripts/Libraries/RbxStamper.lua
2025-12-17 16:47:48 +00:00

2208 lines
94 KiB
Lua

local t = {}
function waitForChild(instance, name)
while not instance:FindFirstChild(name) do
instance.ChildAdded:wait()
end
end
-- Do a line/plane intersection. The line starts at the camera. The plane is at y == 0, normal(0, 1, 0)
--
-- vectorPos - End point of the line.
--
-- Return:
-- cellPos - The terrain cell intersection point if there is one, vectorPos if there isn't.
-- hit - Whether there was a plane intersection. Value is true if there was, false if not.
function PlaneIntersection(vectorPos)
local hit = false
local currCamera = game:GetService("Workspace").CurrentCamera
local startPos = Vector3.new(currCamera.CoordinateFrame.p.X, currCamera.CoordinateFrame.p.Y, currCamera.CoordinateFrame.p.Z)
local endPos = Vector3.new(vectorPos.X, vectorPos.Y, vectorPos.Z)
local normal = Vector3.new(0, 1, 0)
local p3 = Vector3.new(0, 0, 0)
local startEndDot = normal:Dot(endPos - startPos)
local cellPos = vectorPos
if startEndDot ~= 0 then
local t = normal:Dot(p3 - startPos) / startEndDot
if(t >=0 and t <=1) then
local intersection = ((endPos - startPos) * t) + startPos
cellPos = game:GetService("Workspace").Terrain:WorldToCell(intersection)
hit = true
end
end
return cellPos, hit
end
-- Purpose:
-- Checks for terrain touched by the mouse hit.
-- Will do a plane intersection if no terrain is touched.
--
-- mouse - Mouse to check the .hit for.
--
-- Return:
-- cellPos - Cell position hit. Nil if none.
function GetTerrainForMouse(mouse)
-- There was no target, so all it could be is a plane intersection.
-- Check for a plane intersection. If there isn't one then nothing will get hit.
local cell = game:GetService("Workspace").Terrain:WorldToCellPreferSolid(Vector3.new(mouse.hit.x, mouse.hit.y, mouse.hit.z))
local planeLoc = nil
-- If nothing was hit, do the plane intersection.
if 0 == game:GetService("Workspace").Terrain:GetCell(cell.X, cell.Y, cell.Z).Value then
cell = nil
planeLoc, hit = PlaneIntersection(Vector3.new(mouse.hit.x, mouse.hit.y, mouse.hit.z))
if hit then
cell = planeLoc
end
end
return cell
end
-- setup helper functions
local insertBoundingBoxOverlapVector = Vector3.new(.3, .3, .3) -- we can still stamp if our character extrudes into the target stamping space by .3 or fewer units
-- rotates a model by yAngle radians about the global y-axis
local function rotatePartAndChildren(part, rotCF, offsetFromOrigin)
-- rotate this thing, if it's a part
if part:IsA("BasePart") then
part.CFrame = (rotCF * (part.CFrame - offsetFromOrigin)) + offsetFromOrigin
end
-- recursively do the same to all children
local partChildren = part:GetChildren()
for c = 1, #partChildren do rotatePartAndChildren(partChildren[c], rotCF, offsetFromOrigin) end
end
local function modelRotate(model, yAngle)
local rotCF = CFrame.Angles(0, yAngle, 0)
local offsetFromOrigin = model:GetModelCFrame().p
rotatePartAndChildren(model, rotCF, offsetFromOrigin)
end
local function collectParts(object, baseParts, scripts, decals)
if object:IsA("BasePart") then
baseParts[#baseParts+1] = object
elseif object:IsA("Script") then
scripts[#scripts+1] = object
elseif object:IsA("Decal") then
decals[#decals+1] = object
end
for index,child in pairs(object:GetChildren()) do
collectParts(child, baseParts, scripts, decals)
end
end
local function clusterPartsInRegion(startVector, endVector)
local cluster = game:GetService("Workspace"):FindFirstChild("Terrain")
local startCell = cluster:WorldToCell(startVector)
local endCell = cluster:WorldToCell(endVector)
local startX = startCell.X
local startY = startCell.Y
local startZ = startCell.Z
local endX = endCell.X
local endY = endCell.Y
local endZ = endCell.Z
if startX < cluster.MaxExtents.Min.X then startX = cluster.MaxExtents.Min.X end
if startY < cluster.MaxExtents.Min.Y then startY = cluster.MaxExtents.Min.Y end
if startZ < cluster.MaxExtents.Min.Z then startZ = cluster.MaxExtents.Min.Z end
if endX > cluster.MaxExtents.Max.X then endX = cluster.MaxExtents.Max.X end
if endY > cluster.MaxExtents.Max.Y then endY = cluster.MaxExtents.Max.Y end
if endZ > cluster.MaxExtents.Max.Z then endZ = cluster.MaxExtents.Max.Z end
for x = startX, endX do
for y = startY, endY do
for z = startZ, endZ do
if (cluster:GetCell(x, y, z).Value) > 0 then return true end
end
end
end
return false
end
local function findSeatsInModel(parent, seatTable)
if not parent then return end
if parent.className == "Seat" or parent.className == "VehicleSeat" then
table.insert(seatTable, parent)
end
local myChildren = parent:GetChildren()
for j = 1, #myChildren do
findSeatsInModel(myChildren[j], seatTable)
end
end
local function setSeatEnabledStatus(model, isEnabled)
local seatList = {}
findSeatsInModel(model, seatList)
if isEnabled then
-- remove any welds called "SeatWeld" in seats
for i = 1, #seatList do
local nextSeat = seatList[i]:FindFirstChild("SeatWeld")
while nextSeat do nextSeat:Remove() nextSeat = seatList[i]:FindFirstChild("SeatWeld") end
end
else
-- put a weld called "SeatWeld" in every seat
-- this tricks it into thinking there's already someone sitting there, and it won't make you sit XD
for i = 1, #seatList do
local fakeWeld = Instance.new("Weld")
fakeWeld.Name = "SeatWeld"
fakeWeld.Parent = seatList[i]
end
end
end
local function autoAlignToFace(parts)
local aatf = parts:FindFirstChild("AutoAlignToFace")
if aatf then return aatf.Value else return false end
end
local function getClosestAlignedWorldDirection(aVector3InWorld)
local xDir = Vector3.new(1,0,0)
local yDir = Vector3.new(0,1,0)
local zDir = Vector3.new(0,0,1)
local xDot = aVector3InWorld.x * xDir.x + aVector3InWorld.y * xDir.y + aVector3InWorld.z * xDir.z
local yDot = aVector3InWorld.x * yDir.x + aVector3InWorld.y * yDir.y + aVector3InWorld.z * yDir.z
local zDot = aVector3InWorld.x * zDir.x + aVector3InWorld.y * zDir.y + aVector3InWorld.z * zDir.z
if math.abs(xDot) > math.abs(yDot) and math.abs(xDot) > math.abs(zDot) then
if xDot > 0 then
return 0
else
return 3
end
elseif math.abs(yDot) > math.abs(xDot) and math.abs(yDot) > math.abs(zDot) then
if yDot > 0 then
return 1
else
return 4
end
else
if zDot > 0 then
return 2
else
return 5
end
end
end
local function positionPartsAtCFrame3(aCFrame, currentParts)
local insertCFrame = nil
if not currentParts then return currentParts end
if currentParts and (currentParts:IsA("Model") or currentParts:IsA("Tool")) then
insertCFrame = currentParts:GetModelCFrame()
currentParts:TranslateBy(aCFrame.p - insertCFrame.p)
else
currentParts.CFrame = aCFrame
end
return currentParts
end
local function calcRayHitTime(rayStart, raySlope, intersectionPlane)
if math.abs(raySlope) < .01 then return 0 end -- 0 slope --> we just say intersection time is 0, and sidestep this dimension
return (intersectionPlane - rayStart) / raySlope
end
local function modelTargetSurface(partOrModel, rayStart, rayEnd)
if not partOrModel then
return 0
end
local modelCFrame = nil
local modelSize = nil
if partOrModel:IsA("Model") then
modelCFrame = partOrModel:GetModelCFrame()
modelSize = partOrModel:GetModelSize()
else
modelCFrame = partOrModel.CFrame
modelSize = partOrModel.Size
end
local mouseRayStart = modelCFrame:pointToObjectSpace(rayStart)
local mouseRayEnd = modelCFrame:pointToObjectSpace(rayEnd)
local mouseSlope = mouseRayEnd - mouseRayStart
local xPositive = 1
local yPositive = 1
local zPositive = 1
if mouseSlope.X > 0 then xPositive = -1 end
if mouseSlope.Y > 0 then yPositive = -1 end
if mouseSlope.Z > 0 then zPositive = -1 end
-- find which surface the transformed mouse ray hits (using modelSize):
local xHitTime = calcRayHitTime(mouseRayStart.X, mouseSlope.X, modelSize.X/2 * xPositive)
local yHitTime = calcRayHitTime(mouseRayStart.Y, mouseSlope.Y, modelSize.Y/2 * yPositive)
local zHitTime = calcRayHitTime(mouseRayStart.Z, mouseSlope.Z, modelSize.Z/2 * zPositive)
local hitFace = 0
--if xHitTime >= 0 and yHitTime >= 0 and zHitTime >= 0 then
if xHitTime > yHitTime then
if xHitTime > zHitTime then
-- xFace is hit
hitFace = 1*xPositive
else
-- zFace is hit
hitFace = 3*zPositive
end
else
if yHitTime > zHitTime then
-- yFace is hit
hitFace = 2*yPositive
else
-- zFace is hit
hitFace = 3*zPositive
end
end
return hitFace
end
local function getBoundingBox2(partOrModel)
-- for models, the bounding box is defined as the minimum and maximum individual part bounding boxes
-- relative to the first part's coordinate frame.
local minVec = Vector3.new(math.huge, math.huge, math.huge)
local maxVec = Vector3.new(-math.huge, -math.huge, -math.huge)
if partOrModel:IsA("Terrain") then
minVec = Vector3.new(-2, -2, -2)
maxVec = Vector3.new(2, 2, 2)
elseif partOrModel:IsA("BasePart") then
minVec = -0.5 * partOrModel.Size
maxVec = -minVec
else
maxVec = partOrModel:GetModelSize()*0.5
minVec = -maxVec
end
-- Adjust bounding box to reflect what the model or part author wants in terms of justification
local justifyValue = partOrModel:FindFirstChild("Justification")
if justifyValue ~= nil then
-- find the multiple of 4 that contains the model
justify = justifyValue.Value
two = Vector3.new(2, 2, 2)
actualBox = maxVec - minVec - Vector3.new(0.01, 0.01, 0.01)
containingGridBox = Vector3.new(4 * math.ceil(actualBox.x/4), 4 * math.ceil(actualBox.y/4), 4 * math.ceil(actualBox.z/4))
adjustment = containingGridBox - actualBox
minVec = minVec - 0.5 * adjustment * justify
maxVec = maxVec + 0.5 * adjustment * (two - justify)
end
return minVec, maxVec
end
local function getBoundingBoxInWorldCoordinates(partOrModel)
local minVec = Vector3.new(math.huge, math.huge, math.huge)
local maxVec = Vector3.new(-math.huge, -math.huge, -math.huge)
if partOrModel:IsA("BasePart") and not partOrModel:IsA("Terrain") then
vec1 = partOrModel.CFrame:pointToWorldSpace(-0.5 * partOrModel.Size)
vec2 = partOrModel.CFrame:pointToWorldSpace(0.5 * partOrModel.Size)
minVec = Vector3.new(math.min(vec1.X, vec2.X), math.min(vec1.Y, vec2.Y), math.min(vec1.Z, vec2.Z))
maxVec = Vector3.new(math.max(vec1.X, vec2.X), math.max(vec1.Y, vec2.Y), math.max(vec1.Z, vec2.Z))
elseif partOrModel:IsA("Terrain") then
-- we shouldn't have to deal with this case
--minVec = Vector3.new(-2, -2, -2)
--maxVec = Vector3.new(2, 2, 2)
else
vec1 = partOrModel:GetModelCFrame():pointToWorldSpace(-0.5 * partOrModel:GetModelSize())
vec2 = partOrModel:GetModelCFrame():pointToWorldSpace(0.5 * partOrModel:GetModelSize())
minVec = Vector3.new(math.min(vec1.X, vec2.X), math.min(vec1.Y, vec2.Y), math.min(vec1.Z, vec2.Z))
maxVec = Vector3.new(math.max(vec1.X, vec2.X), math.max(vec1.Y, vec2.Y), math.max(vec1.Z, vec2.Z))
end
return minVec, maxVec
end
local function getTargetPartBoundingBox(targetPart)
if targetPart.Parent:FindFirstChild("RobloxModel") ~= nil then
return getBoundingBox2(targetPart.Parent)
else
return getBoundingBox2(targetPart)
end
end
local function getMouseTargetCFrame(targetPart)
if targetPart.Parent:FindFirstChild("RobloxModel") ~= nil then
if targetPart.Parent:IsA("Tool") then return targetPart.Parent.Handle.CFrame
else return targetPart.Parent:GetModelCFrame() end
else
return targetPart.CFrame
end
end
local function isBlocker(part) -- returns whether or not we want to cancel the stamp because we're blocked by this part
if not part then return false end
if not part.Parent then return false end
if part:FindFirstChild("Humanoid") then return false end
if part:FindFirstChild("RobloxStamper") or part:FindFirstChild("RobloxModel") then return true end
if part:IsA("Part") and not part.CanCollide then return false end
if part == game:GetService("Lighting") then return false end
return isBlocker(part.Parent)
end
-- helper function to determine if a character can be pushed upwards by a certain amount
-- character is 5 studs tall, we'll check a 1.5 x 1.5 x 4.5 box around char, with center .5 studs below torsocenter
local function spaceAboveCharacter(charTorso, newTorsoY, stampData)
local partsAboveChar = game:GetService("Workspace"):FindPartsInRegion3(
Region3.new(Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z) - Vector3.new(.75, 2.75, .75),
Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z) + Vector3.new(.75, 1.75, .75)),
charTorso.Parent,
100)
for j = 1, #partsAboveChar do
if partsAboveChar[j].CanCollide and not partsAboveChar[j]:IsDescendantOf(stampData.CurrentParts) then return false end
end
if clusterPartsInRegion(Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z) - Vector3.new(.75, 2.75, .75),
Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z) + Vector3.new(.75, 1.75, .75)) then
return false
end
return true
end
local function findConfigAtMouseTarget(Mouse, stampData)
-- *Critical Assumption* :
-- This function assumes the target CF axes are orthogonal with the target bounding box faces
-- And, it assumes the insert CF axes are orthongonal with the insert bounding box faces
-- Therefore, insertion will not work with angled faces on wedges or other "non-block" parts, nor
-- will it work for parts in a model that are not orthogonally aligned with the model's CF.
if not Mouse then return nil end -- This can happen sometimes, return if so
if not stampData then error("findConfigAtMouseTarget: stampData is nil") return nil end
if not stampData["CurrentParts"] then return nil end
local grid = 4.0
local admissibleConfig = false
local targetConfig = CFrame.new(0,0,0)
local minBB, maxBB = getBoundingBox2(stampData.CurrentParts)
local diagBB = maxBB - minBB
local insertCFrame
if stampData.CurrentParts:IsA("Model") or stampData.CurrentParts:IsA("Tool") then
insertCFrame = stampData.CurrentParts:GetModelCFrame()
else
insertCFrame = stampData.CurrentParts.CFrame
end
if Mouse then
if stampData.CurrentParts:IsA("Tool") then
Mouse.TargetFilter = stampData.CurrentParts.Handle
else
Mouse.TargetFilter = stampData.CurrentParts
end
end
local hitPlane = false
local targetPart = nil
local success = pcall(function() targetPart = Mouse.Target end)
if not success then-- or targetPart == nil then
return admissibleConfig, targetConfig
end
local mouseHitInWorld = Vector3.new(0, 0, 0)
if Mouse then
mouseHitInWorld = Vector3.new(Mouse.Hit.x, Mouse.Hit.y, Mouse.Hit.z)
end
local cellPos = nil
-- Nothing was hit, so check for the default plane.
if nil == targetPart then
cellPos = GetTerrainForMouse(Mouse)
if nil == cellPos then
hitPlane = false
return admissibleConfig, targetConfig
else
targetPart = game:GetService("Workspace").Terrain
hitPlane = true
-- Take into account error that will occur.
cellPos = Vector3.new(cellPos.X - 1, cellPos.Y, cellPos.Z)
mouseHitInWorld = game:GetService("Workspace").Terrain:CellCenterToWorld(cellPos.x, cellPos.y, cellPos.z)
end
end
-- test mouse hit location
local minBBTarget, maxBBTarget = getTargetPartBoundingBox(targetPart)
local diagBBTarget = maxBBTarget - minBBTarget
local targetCFrame = getMouseTargetCFrame(targetPart)
if targetPart:IsA("Terrain") then
if not cluster then cluster = game:GetService("Workspace"):FindFirstChild("Terrain") end
local cellID = cluster:WorldToCellPreferSolid(mouseHitInWorld)
if hitPlane then
cellID = cellPos
end
targetCFrame = CFrame.new(game:GetService("Workspace").Terrain:CellCenterToWorld(cellID.x, cellID.y, cellID.z))
end
local mouseHitInTarget = targetCFrame:pointToObjectSpace(mouseHitInWorld)
local targetVectorInWorld = Vector3.new(0,0,0)
if Mouse then
-- DON'T WANT THIS IN TERMS OF THE MODEL CFRAME! (.TargetSurface is in terms of the part CFrame, so this would break, right? [HotThoth])
-- (ideally, we would want to make the Mouse.TargetSurface a model-targetsurface instead, but for testing will be using the converse)
--targetVectorInWorld = targetCFrame:vectorToWorldSpace(Vector3.FromNormalId(Mouse.TargetSurface))
targetVectorInWorld = targetPart.CFrame:vectorToWorldSpace(Vector3.FromNormalId(Mouse.TargetSurface)) -- better, but model cframe would be best
--[[if targetPart.Parent:IsA("Model") then
local hitFace = modelTargetSurface(targetPart.Parent, Mouse.Hit.p, game.Workspace.CurrentCamera.CoordinateFrame.p) -- best, if you get it right
local WORLD_AXES = {Vector3.new(1, 0, 0), Vector3.new(0, 1, 0), Vector3.new(0, 0, 1)}
if hitFace > 0 then
targetVectorInWorld = targetCFrame:vectorToWorldSpace(WORLD_AXES[hitFace])
elseif hitFace < 0 then
targetVectorInWorld = targetCFrame:vectorToWorldSpace(-WORLD_AXES[-hitFace])
end
end]]
end
local targetRefPointInTarget
local clampToSurface
if getClosestAlignedWorldDirection(targetVectorInWorld) == 0 then
targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(1, -1, 1))
insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(-1, -1, 1))
clampToSurface = Vector3.new(0,1,1)
elseif getClosestAlignedWorldDirection(targetVectorInWorld) == 3 then
targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(-1, -1, -1))
insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(1, -1, -1))
clampToSurface = Vector3.new(0,1,1)
elseif getClosestAlignedWorldDirection(targetVectorInWorld) == 1 then
targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(-1, 1, 1))
insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(-1, -1, 1))
clampToSurface = Vector3.new(1,0,1)
elseif getClosestAlignedWorldDirection(targetVectorInWorld) == 4 then
targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(-1, -1, 1))
insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(-1, 1, 1))
clampToSurface = Vector3.new(1,0,1)
elseif getClosestAlignedWorldDirection(targetVectorInWorld) == 2 then
targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(-1, -1, 1))
insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(-1, -1, -1))
clampToSurface = Vector3.new(1,1,0)
else
targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(1, -1, -1))
insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(1, -1, 1))
clampToSurface = Vector3.new(1,1,0)
end
targetRefPointInTarget = targetRefPointInTarget * (0.5 * diagBBTarget) + 0.5 * (maxBBTarget + minBBTarget)
insertRefPointInInsert = insertRefPointInInsert * (0.5 * diagBB) + 0.5 * (maxBB + minBB)
-- To Do: For cases that are not aligned to the world grid, account for the minimal rotation
-- needed to bring the Insert part(s) into alignment with the Target Part
-- Apply the rotation here
local delta = mouseHitInTarget - targetRefPointInTarget
local deltaClamped = Vector3.new(grid * math.modf(delta.x/grid), grid * math.modf(delta.y/grid), grid * math.modf(delta.z/grid))
deltaClamped = deltaClamped * clampToSurface
local targetTouchInTarget = deltaClamped + targetRefPointInTarget
local TargetTouchRelToWorld = targetCFrame:pointToWorldSpace(targetTouchInTarget)
local InsertTouchInWorld = insertCFrame:vectorToWorldSpace(insertRefPointInInsert)
local posInsertOriginInWorld = TargetTouchRelToWorld - InsertTouchInWorld
local x, y, z, R00, R01, R02, R10, R11, R12, R20, R21, R22 = insertCFrame:components()
targetConfig = CFrame.new(posInsertOriginInWorld.x, posInsertOriginInWorld.y, posInsertOriginInWorld.z, R00, R01, R02, R10, R11, R12, R20, R21, R22)
admissibleConfig = true
return admissibleConfig, targetConfig, getClosestAlignedWorldDirection(targetVectorInWorld)
end
local function truncateToCircleEighth(bigValue, littleValue)
local big = math.abs(bigValue)
local little = math.abs(littleValue)
local hypotenuse = math.sqrt(big*big + little*little)
local frac = little / hypotenuse
local bigSign = 1
local littleSign = 1
if bigValue < 0 then bigSign = -1 end
if littleValue < 0 then littleSign = -1 end
if frac > .382683432 then
-- between 22.5 and 45 degrees, so truncate to 45-degree tilt
return .707106781 * hypotenuse * bigSign, .707106781 * hypotenuse * littleSign
else
-- between 0 and 22.5 degrees, so truncate to 0-degree tilt
return hypotenuse * bigSign, 0
end
end
local function saveTheWelds(object, manualWeldTable, manualWeldParentTable)
if object:IsA("ManualWeld") or object:IsA("Rotate") then
table.insert(manualWeldTable, object)
table.insert(manualWeldParentTable, object.Parent)
else
local children = object:GetChildren()
for i = 1, #children do
saveTheWelds(children[i], manualWeldTable, manualWeldParentTable)
end
end
end
local function restoreTheWelds(manualWeldTable, manualWeldParentTable)
for i = 1, #manualWeldTable do
manualWeldTable[i].Parent = manualWeldParentTable[i]
end
end
t.CanEditRegion = function(partOrModel, EditRegion) -- todo: use model and stamper metadata
if not EditRegion then return true, false end
local minBB, maxBB = getBoundingBoxInWorldCoordinates(partOrModel)
if minBB.X < EditRegion.CFrame.p.X - EditRegion.Size.X/2 or
minBB.Y < EditRegion.CFrame.p.Y - EditRegion.Size.Y/2 or
minBB.Z < EditRegion.CFrame.p.Z - EditRegion.Size.Z/2 then
return false, false
end
if maxBB.X > EditRegion.CFrame.p.X + EditRegion.Size.X/2 or
maxBB.Y > EditRegion.CFrame.p.Y + EditRegion.Size.Y/2 or
maxBB.Z > EditRegion.CFrame.p.Z + EditRegion.Size.Z/2 then
return false, false
end
return true, false
end
t.GetStampModel = function(assetId, terrainShape, useAssetVersionId)
if assetId == 0 then
return nil, "No Asset"
end
if assetId < 0 then
return nil, "Negative Asset"
end
local function UnlockInstances(object)
if object:IsA("BasePart") then
object.Locked = false
end
for index,child in pairs(object:GetChildren()) do
UnlockInstances(child)
end
end
local function getClosestColorToTerrainMaterial(terrainValue)
if terrainValue == 1 then
return BrickColor.new("Bright green")
elseif terrainValue == 2 then
return BrickColor.new("Bright yellow")
elseif terrainValue == 3 then
return BrickColor.new("Bright red")
elseif terrainValue == 4 then
return BrickColor.new("Sand red")
elseif terrainValue == 5 then
return BrickColor.new("Black")
elseif terrainValue == 6 then
return BrickColor.new("Dark stone grey")
elseif terrainValue == 7 then
return BrickColor.new("Sand blue")
elseif terrainValue == 8 then
return BrickColor.new("Deep orange")
elseif terrainValue == 9 then
return BrickColor.new("Dark orange")
elseif terrainValue == 10 then
return BrickColor.new("Reddish brown")
elseif terrainValue == 11 then
return BrickColor.new("Light orange")
elseif terrainValue == 12 then
return BrickColor.new("Light stone grey")
elseif terrainValue == 13 then
return BrickColor.new("Sand green")
elseif terrainValue == 14 then
return BrickColor.new("Medium stone grey")
elseif terrainValue == 15 then
return BrickColor.new("Really red")
elseif terrainValue == 16 then
return BrickColor.new("Really blue")
elseif terrainValue == 17 then
return BrickColor.new("Bright blue")
else
return BrickColor.new("Bright green")
end
end
local function setupFakeTerrainPart(cellMat, cellType, cellOrient)
local newTerrainPiece = nil
if (cellType == 1 or cellType == 4) then newTerrainPiece = Instance.new("WedgePart") newTerrainPiece.formFactor = "Custom"
elseif (cellType == 2) then newTerrainPiece = Instance.new("CornerWedgePart")
else newTerrainPiece = Instance.new("Part") newTerrainPiece.formFactor = "Custom" end
newTerrainPiece.Name = "MegaClusterCube"
newTerrainPiece.Size = Vector3.new(4, 4, 4)
newTerrainPiece.BottomSurface = "Smooth"
newTerrainPiece.TopSurface = "Smooth"
-- can add decals or textures here if feeling particularly adventurous... for now, can make a table of look-up colors
newTerrainPiece.BrickColor = getClosestColorToTerrainMaterial(cellMat)
local sideways = 0
local flipped = math.pi
if cellType == 4 then sideways = -math.pi/2 end
if cellType == 2 or cellType == 3 then flipped = 0 end
newTerrainPiece.CFrame = CFrame.Angles(0, math.pi/2*cellOrient + flipped, sideways)
if cellType == 3 then
local inverseCornerWedgeMesh = Instance.new("SpecialMesh")
inverseCornerWedgeMesh.MeshType = "FileMesh"
inverseCornerWedgeMesh.MeshId = "https://assetdelivery.roblox.com/v1/asset?id=66832495"
inverseCornerWedgeMesh.Scale = Vector3.new(2, 2, 2)
inverseCornerWedgeMesh.Parent = newTerrainPiece
end
local materialTag = Instance.new("Vector3Value")
materialTag.Value = Vector3.new(cellMat, cellType, cellOrient)
materialTag.Name = "ClusterMaterial"
materialTag.Parent = newTerrainPiece
return newTerrainPiece
end
-- This call will cause a "wait" until the data comes back
-- below we wait a max of 8 seconds before deciding to bail out on loading
local root
local loader
loading = true
if useAssetVersionId then
loader = coroutine.create(function()
root = game:GetService("InsertService"):LoadAssetVersion(assetId)
loading = false
end)
coroutine.resume(loader)
else
loader = coroutine.create(function()
root = game:GetService("InsertService"):LoadAsset(assetId)
loading = false
end)
coroutine.resume(loader)
end
local lastGameTime = 0
local totalTime = 0
local maxWait = 8
while loading and totalTime < maxWait do
lastGameTime = tick()
wait(1)
totalTime = totalTime + tick() - lastGameTime
end
loading = false
if totalTime >= maxWait then
return nil, "Load Time Fail"
end
if root == nil then
return nil, "Load Asset Fail"
end
if not root:IsA("Model") then
return nil, "Load Type Fail"
end
local instances = root:GetChildren()
if #instances == 0 then
return nil, "Empty Model Fail"
end
--Unlock all parts that are inserted, to make sure they are editable
UnlockInstances(root)
--Continue the insert process
root = root:GetChildren()[1]
--Examine the contents and decide what it looks like
for pos, instance in pairs(instances) do
if instance:IsA("Team") then
instance.Parent = game:GetService("Teams")
elseif instance:IsA("Sky") then
local lightingService = game:GetService("Lighting")
for index,child in pairs(lightingService:GetChildren()) do
if child:IsA("Sky") then
child:Remove();
end
end
instance.Parent = lightingService
return
end
end
-- ...and tag all inserted models for subsequent origin identification
-- if no RobloxModel tag already exists, then add it.
if root:FindFirstChild("RobloxModel") == nil then
local stringTag = Instance.new("BoolValue", root)
stringTag.Name = "RobloxModel"
if root:FindFirstChild("RobloxStamper") == nil then
local stringTag2 = Instance.new("BoolValue", root)
stringTag2.Name = "RobloxStamper"
end
end
if terrainShape then
if root.Name == "MegaClusterCube" then
if (terrainShape == 6) then -- insert an autowedging tag
local autowedgeTag = Instance.new("BoolValue")
autowedgeTag.Name = "AutoWedge"
autowedgeTag.Parent = root
else
local clusterTag = root:FindFirstChild("ClusterMaterial")
if clusterTag then
if clusterTag:IsA("Vector3Value") then
root = setupFakeTerrainPart(clusterTag.Value.X, terrainShape, clusterTag.Value.Z)
else
root = setupFakeTerrainPart(clusterTag.Value, terrainShape, 0)
end
else
root = setupFakeTerrainPart(1, terrainShape, 0)
end
end
end
end
return root
end
t.SetupStamperDragger = function(modelToStamp, Mouse, StampInModel, AllowedStampRegion, StampFailedFunc)
if not modelToStamp then
error("SetupStamperDragger: modelToStamp (first arg) is nil! Should be a stamper model")
return nil
end
if not modelToStamp:IsA("Model") and not modelToStamp:IsA("BasePart") then
error("SetupStamperDragger: modelToStamp (first arg) is neither a Model or Part!")
return nil
end
if not Mouse then
error("SetupStamperDragger: Mouse (second arg) is nil! Should be a mouse object")
return nil
end
if not Mouse:IsA("Mouse") then
error("SetupStamperDragger: Mouse (second arg) is not of type Mouse!")
return nil
end
local stampInModel = nil
local allowedStampRegion = nil
local stampFailedFunc = nil
if StampInModel then
if not StampInModel:IsA("Model") then
error("SetupStamperDragger: StampInModel (optional third arg) is not of type 'Model'")
return nil
end
if not AllowedStampRegion then
error("SetupStamperDragger: AllowedStampRegion (optional fourth arg) is nil when StampInModel (optional third arg) is defined")
return nil
end
stampFailedFunc = StampFailedFunc
stampInModel = StampInModel
allowedStampRegion = AllowedStampRegion
end
-- Init all state variables
local gInitial90DegreeRotations = 0
local stampData = nil
local mouseTarget = nil
local errorBox = Instance.new("SelectionBox")
errorBox.Color = BrickColor.new("Bright red")
errorBox.Transparency = 0
errorBox.Archivable = false
-- for megacluster MEGA STAMPING
local adornPart = Instance.new("Part")
adornPart.Parent = nil
adornPart.formFactor = "Custom"
adornPart.Size = Vector3.new(4, 4, 4)
adornPart.CFrame = CFrame.new()
adornPart.Archivable = false
local adorn = Instance.new("SelectionBox")
adorn.Color = BrickColor.new("Toothpaste")
adorn.Adornee = adornPart
adorn.Visible = true
adorn.Transparency = 0
adorn.Name = "HighScalabilityStamperLine"
adorn.Archivable = false
local HighScalabilityLine = {}
HighScalabilityLine.Start = nil
HighScalabilityLine.End = nil
HighScalabilityLine.Adorn = adorn
HighScalabilityLine.AdornPart = adornPart
HighScalabilityLine.InternalLine = nil
HighScalabilityLine.NewHint = true
HighScalabilityLine.MorePoints = {nil, nil}
HighScalabilityLine.MoreLines = {nil, nil}
HighScalabilityLine.Dimensions = 1
local control = {}
local movingLock = false
local stampUpLock = false
local unstampableSurface = false
local mouseCons = {}
local keyCon = nil
local stamped = Instance.new("BoolValue")
stamped.Archivable = false
stamped.Value = false
local lastTarget = {}
lastTarget.TerrainOrientation = 0
lastTarget.CFrame = 0
local cellInfo = {}
cellInfo.Material = 1
cellInfo.clusterType = 0
cellInfo.clusterOrientation = 0
local function isMegaClusterPart()
if not stampData then return false end
if not stampData.CurrentParts then return false end
return ( stampData.CurrentParts:FindFirstChild("ClusterMaterial",true) or (stampData.CurrentParts.Name == "MegaClusterCube") )
end
local function DoHighScalabilityRegionSelect()
local megaCube = stampData.CurrentParts:FindFirstChild("MegaClusterCube")
if not megaCube then
if not stampData.CurrentParts.Name == "MegaClusterCube" then
return
else
megaCube = stampData.CurrentParts
end
end
HighScalabilityLine.End = megaCube.CFrame.p
local line = nil
local line2 = Vector3.new(0, 0, 0)
local line3 = Vector3.new(0, 0, 0)
if HighScalabilityLine.Dimensions == 1 then
-- extract the line from these positions and limit to a 2D plane made from 2 of the world axes
-- then use dominating axis to limit line to be at 45-degree intervals
-- will use this internal representation of the line for the actual stamping
line = (HighScalabilityLine.End - HighScalabilityLine.Start)
if math.abs(line.X) < math.abs(line.Y) then
if math.abs(line.X) < math.abs(line.Z) then
-- limit to Y/Z plane, domination unknown
local newY, newZ
if (math.abs(line.Y) > math.abs(line.Z)) then
newY, newZ = truncateToCircleEighth(line.Y, line.Z)
else
newZ, newY = truncateToCircleEighth(line.Z, line.Y)
end
line = Vector3.new(0, newY, newZ)
else
-- limit to X/Y plane, with Y dominating
local newY, newX = truncateToCircleEighth(line.Y, line.X)
line = Vector3.new(newX, newY, 0)
end
else
if math.abs(line.Y) < math.abs(line.Z) then
-- limit to X/Z plane, domination unknown
local newX, newZ
if math.abs(line.X) > math.abs(line.Z) then
newX, newZ = truncateToCircleEighth(line.X, line.Z)
else
newZ, newX = truncateToCircleEighth(line.Z, line.X)
end
line = Vector3.new(newX, 0, newZ)
else
-- limit to X/Y plane, with X dominating
local newX, newY = truncateToCircleEighth(line.X, line.Y)
line = Vector3.new(newX, newY, 0)
end
end
HighScalabilityLine.InternalLine = line
elseif HighScalabilityLine.Dimensions == 2 then
line = HighScalabilityLine.MoreLines[1]
line2 = HighScalabilityLine.End - HighScalabilityLine.MorePoints[1]
-- take out any component of line2 along line1, so you get perpendicular to line1 component
line2 = line2 - line.unit*line.unit:Dot(line2)
tempCFrame = CFrame.new(HighScalabilityLine.Start, HighScalabilityLine.Start + line)
-- then zero out whichever is the smaller component
local yAxis = tempCFrame:vectorToWorldSpace(Vector3.new(0, 1, 0))
local xAxis = tempCFrame:vectorToWorldSpace(Vector3.new(1, 0, 0))
local xComp = xAxis:Dot(line2)
local yComp = yAxis:Dot(line2)
if math.abs(yComp) > math.abs(xComp) then
line2 = line2 - xAxis * xComp
else
line2 = line2 - yAxis * yComp
end
HighScalabilityLine.InternalLine = line2
elseif HighScalabilityLine.Dimensions == 3 then
line = HighScalabilityLine.MoreLines[1]
line2 = HighScalabilityLine.MoreLines[2]
line3 = HighScalabilityLine.End - HighScalabilityLine.MorePoints[2]
-- zero out all components of previous lines
line3 = line3 - line.unit * line.unit:Dot(line3)
line3 = line3 - line2.unit * line2.unit:Dot(line3)
HighScalabilityLine.InternalLine = line3
end
-- resize the "line" graphic to be the correct size and orientation
tempCFrame = CFrame.new(HighScalabilityLine.Start, HighScalabilityLine.Start + line)
if HighScalabilityLine.Dimensions == 1 then -- faster calculation for line
HighScalabilityLine.AdornPart.Size = Vector3.new(4, 4, line.magnitude + 4)
HighScalabilityLine.AdornPart.CFrame = tempCFrame + tempCFrame:vectorToWorldSpace(Vector3.new(2, 2, 2) - HighScalabilityLine.AdornPart.Size/2)
else
local boxSize = tempCFrame:vectorToObjectSpace(line + line2 + line3)
HighScalabilityLine.AdornPart.Size = Vector3.new(4, 4, 4) + Vector3.new(math.abs(boxSize.X), math.abs(boxSize.Y), math.abs(boxSize.Z))
HighScalabilityLine.AdornPart.CFrame = tempCFrame + tempCFrame:vectorToWorldSpace(boxSize/2)
end
-- make player able to see this ish
local gui = nil
if game:GetService("Players")["LocalPlayer"] then
gui = game:GetService("Players").LocalPlayer:FindFirstChild("PlayerGui")
if gui and gui:IsA("PlayerGui") then
if HighScalabilityLine.Dimensions == 1 and line.magnitude > 3 then -- don't show if mouse hasn't moved enough
HighScalabilityLine.Adorn.Parent = gui
elseif HighScalabilityLine.Dimensions > 1 then
HighScalabilityLine.Adorn.Parent = gui
end
end
end
if gui == nil then -- we are in studio
gui = game:GetService("CoreGui")
if HighScalabilityLine.Dimensions == 1 and line.magnitude > 3 then -- don't show if mouse hasn't moved enough
HighScalabilityLine.Adorn.Parent = gui
elseif HighScalabilityLine.Dimensions > 1 then
HighScalabilityLine.Adorn.Parent = gui
end
end
end
local function DoStamperMouseMove(Mouse)
if not Mouse then
error("Error: RbxStamper.DoStamperMouseMove: Mouse is nil")
return
end
if not Mouse:IsA("Mouse") then
error("Error: RbxStamper.DoStamperMouseMove: Mouse is of type", Mouse.className,"should be of type Mouse")
return
end
-- There wasn't a target (no part or terrain), so check for plane intersection.
if not Mouse.Target then
local cellPos = GetTerrainForMouse(Mouse)
if nil == cellPos then
return
end
end
if not stampData then
return
end
-- don't move with dragger - will move in one step on mouse down
-- draw ghost at acceptable positions
configFound, targetCFrame, targetSurface = findConfigAtMouseTarget(Mouse, stampData)
if not configFound then
error("RbxStamper.DoStamperMouseMove No configFound, returning")
return
end
local numRotations = 0 -- update this according to how many rotations you need to get it to target surface
if autoAlignToFace(stampData.CurrentParts) and targetSurface ~= 1 and targetSurface ~= 4 then -- pre-rotate the flag or portrait so it's aligned correctly
if targetSurface == 3 then numRotations = 0 - gInitial90DegreeRotations + autoAlignToFace(stampData.CurrentParts)
elseif targetSurface == 0 then numRotations = 2 - gInitial90DegreeRotations + autoAlignToFace(stampData.CurrentParts)
elseif targetSurface == 5 then numRotations = 3 - gInitial90DegreeRotations + autoAlignToFace(stampData.CurrentParts)
elseif targetSurface == 2 then numRotations = 1 - gInitial90DegreeRotations + autoAlignToFace(stampData.CurrentParts)
end
end
local ry = math.pi/2
gInitial90DegreeRotations = gInitial90DegreeRotations + numRotations
if stampData.CurrentParts:IsA("Model") or stampData.CurrentParts:IsA("Tool") then
--stampData.CurrentParts:Rotate(0, ry*numRotations, 0)
modelRotate(stampData.CurrentParts, ry*numRotations)
else
stampData.CurrentParts.CFrame = CFrame.fromEulerAnglesXYZ(0, ry*numRotations, 0) * stampData.CurrentParts.CFrame
end
-- CODE TO CHECK FOR DRAGGING GHOST PART INTO A COLLIDING STATE
local minBB, maxBB = getBoundingBoxInWorldCoordinates(stampData.CurrentParts)
-- need to offset by distance to be dragged
local currModelCFrame = nil
if stampData.CurrentParts:IsA("Model") then
currModelCFrame = stampData.CurrentParts:GetModelCFrame()
else
currModelCFrame = stampData.CurrentParts.CFrame
end
minBB = minBB + targetCFrame.p - currModelCFrame.p
maxBB = maxBB + targetCFrame.p - currModelCFrame.p
-- don't drag into terrain
if clusterPartsInRegion(minBB + insertBoundingBoxOverlapVector, maxBB - insertBoundingBoxOverlapVector) then
if lastTarget.CFrame then
if (stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)) then
local theClusterMaterial = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
if theClusterMaterial:IsA("Vector3Value") then
local stampClusterMaterial = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
if stampClusterMaterial then
stampClusterMaterial = clusterMat
end
end
end
end
return
end
-- if we are stamping a terrain part, make sure it goes on the grid! Otherwise preview block could be placed off grid, but stamped on grid
if isMegaClusterPart() then
local cellToStamp = game:GetService("Workspace").Terrain:WorldToCell(targetCFrame.p)
local newCFramePosition = game:GetService("Workspace").Terrain:CellCenterToWorld(cellToStamp.X, cellToStamp.Y, cellToStamp.Z)
local x, y, z, R00, R01, R02, R10, R11, R12, R20, R21, R22 = targetCFrame:components()
targetCFrame = CFrame.new(newCFramePosition.X,newCFramePosition.Y,newCFramePosition.Z,R00, R01, R02, R10, R11, R12, R20, R21, R22)
end
positionPartsAtCFrame3(targetCFrame, stampData.CurrentParts)
lastTarget.CFrame = targetCFrame -- successful positioning, so update 'dat cframe
if stampData.CurrentParts:FindFirstChild("ClusterMaterial", true) then
local clusterMat = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
if clusterMat:IsA("Vector3Value") then
lastTarget.TerrainOrientation = clusterMat.Value.Z
end
end
-- auto break joints code
if Mouse and Mouse.Target and Mouse.Target.Parent then
local modelInfo = Mouse.Target:FindFirstChild("RobloxModel")
if not modelInfo then modelInfo = Mouse.Target.Parent:FindFirstChild("RobloxModel") end
local myModelInfo = stampData.CurrentParts:FindFirstChild("UnstampableFaces")
--if (modelInfo and modelInfo.Parent:FindFirstChild("UnstampableFaces")) or (modelInfo and myModelInfo) then -- need better targetSurface calcs
if (true) then
local breakingFaces = ""
local myBreakingFaces = ""
if modelInfo and modelInfo.Parent:FindFirstChild("UnstampableFaces") then breakingFaces = modelInfo.Parent.UnstampableFaces.Value end
if myModelInfo then myBreakingFaces = myModelInfo.Value end
local hitFace = 0
if modelInfo then hitFace = modelTargetSurface(modelInfo.Parent, game:GetService("Workspace").CurrentCamera.CoordinateFrame.p, Mouse.Hit.p) end
-- are we stamping TO an unstampable surface?
for bf in string.gmatch(breakingFaces, "[^,]+") do
if hitFace == tonumber(bf) then
-- return before we hit the JointsService code below!
unstampableSurface = true
game.JointsService:ClearJoinAfterMoveJoints() -- clear the JointsService cache
return
end
end
-- now we have to cast the ray back in the other direction to find the surface we're stamping FROM
hitFace = modelTargetSurface(stampData.CurrentParts, Mouse.Hit.p, game:GetService("Workspace").CurrentCamera.CoordinateFrame.p)
-- are we stamping WITH an unstampable surface?
for bf in string.gmatch(myBreakingFaces, "[^,]+") do
if hitFace == tonumber(bf) then
unstampableSurface = true
game.JointsService:ClearJoinAfterMoveJoints() -- clear the JointsService cache
return
end
end
-- just need to match breakingFace against targetSurface using rotation supplied by modelCFrame
-- targetSurface: 1 is top, 4 is bottom,
end
end
-- to show joints during the mouse move
unstampableSurface = false
game.JointsService:SetJoinAfterMoveInstance(stampData.CurrentParts)
-- most common mouse inactive error occurs here, so check mouse active one more time in a pcall
if not pcall(function()
if Mouse and Mouse.Target and Mouse.Target.Parent:FindFirstChild("RobloxModel") == nil then
return
else
return
end
end)
then
error("Error: RbxStamper.DoStamperMouseMove Mouse is nil on second check")
game.JointsService:ClearJoinAfterMoveJoints()
Mouse = nil
return
end
if Mouse and Mouse.Target and Mouse.Target.Parent:FindFirstChild("RobloxModel") == nil then
game.JointsService:SetJoinAfterMoveTarget(Mouse.Target)
else
game.JointsService:SetJoinAfterMoveTarget(nil)
end
game.JointsService:ShowPermissibleJoints()
-- here we allow for a line of high-scalability parts
if isMegaClusterPart() and HighScalabilityLine and HighScalabilityLine.Start then
DoHighScalabilityRegionSelect()
end
end
local function setupKeyListener(key, Mouse)
if control and control["Paused"] then return end -- don't do this if we have no stamp
key = string.lower(key)
if key == 'r' and not autoAlignToFace(stampData.CurrentParts) then -- rotate the model
gInitial90DegreeRotations = gInitial90DegreeRotations + 1
-- Update orientation value if this is a fake terrain part
local clusterValues = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
if clusterValues and clusterValues:IsA("Vector3Value") then
clusterValues.Value = Vector3.new(clusterValues.Value.X, clusterValues.Value.Y, (clusterValues.Value.Z + 1) % 4)
end
-- Rotate the parts or all the parts in the model
local ry = math.pi/2
if stampData.CurrentParts:IsA("Model") or stampData.CurrentParts:IsA("Tool") then
--stampData.CurrentParts:Rotate(0, ry, 0)
modelRotate(stampData.CurrentParts, ry)
else
stampData.CurrentParts.CFrame = CFrame.fromEulerAnglesXYZ(0, ry, 0) * stampData.CurrentParts.CFrame
end
-- After rotating, update the position
configFound, targetCFrame = findConfigAtMouseTarget(Mouse, stampData)
if configFound then
positionPartsAtCFrame3(targetCFrame, stampData.CurrentParts)
-- update everything else in MouseMove
DoStamperMouseMove(Mouse)
end
elseif key == 'c' then -- try to expand our high scalability dragger dimension
if HighScalabilityLine.InternalLine and HighScalabilityLine.InternalLine.magnitude > 0 and HighScalabilityLine.Dimensions < 3 then
HighScalabilityLine.MorePoints[HighScalabilityLine.Dimensions] = HighScalabilityLine.End
HighScalabilityLine.MoreLines[HighScalabilityLine.Dimensions] = HighScalabilityLine.InternalLine
HighScalabilityLine.Dimensions = HighScalabilityLine.Dimensions + 1
HighScalabilityLine.NewHint = true
end
end
end
keyCon = Mouse.KeyDown:connect(function(key) -- init key connection (keeping code close to func)
setupKeyListener(key, Mouse)
end)
local function resetHighScalabilityLine()
if HighScalabilityLine then
HighScalabilityLine.Start = nil
HighScalabilityLine.End = nil
HighScalabilityLine.InternalLine = nil
HighScalabilityLine.NewHint = true
end
end
local function flashRedBox()
local gui = game:GetService("CoreGui")
if game:GetService("Players") then
if game:GetService("Players")["LocalPlayer"] then
if game:GetService("Players").LocalPlayer:FindFirstChild("PlayerGui") then
gui = game:GetService("Players").LocalPlayer.PlayerGui
end
end
end
if not stampData["ErrorBox"] then return end
stampData.ErrorBox.Parent = gui
if stampData.CurrentParts:IsA("Tool") then
stampData.ErrorBox.Adornee = stampData.CurrentParts.Handle
else
stampData.ErrorBox.Adornee = stampData.CurrentParts
end
delay(0,function()
for i = 1, 3 do
if stampData["ErrorBox"] then stampData.ErrorBox.Visible = true end
wait(0.13)
if stampData["ErrorBox"] then stampData.ErrorBox.Visible = false end
wait(0.13)
end
if stampData["ErrorBox"] then
stampData.ErrorBox.Adornee = nil
stampData.ErrorBox.Parent = Tool
end
end)
end
local function DoStamperMouseDown(Mouse)
if not Mouse then
error("Error: RbxStamper.DoStamperMouseDown: Mouse is nil")
return
end
if not Mouse:IsA("Mouse") then
error("Error: RbxStamper.DoStamperMouseDown: Mouse is of type", Mouse.className,"should be of type Mouse")
return
end
if not stampData then
return
end
if isMegaClusterPart() then
if Mouse and HighScalabilityLine then
local megaCube = stampData.CurrentParts:FindFirstChild("MegaClusterCube", true)
local terrain = game:GetService("Workspace").Terrain
if megaCube then
HighScalabilityLine.Dimensions = 1
local tempCell = terrain:WorldToCell(megaCube.CFrame.p)
HighScalabilityLine.Start = terrain:CellCenterToWorld(tempCell.X, tempCell.Y, tempCell.Z)
return
else
HighScalabilityLine.Dimensions = 1
local tempCell = terrain:WorldToCell(stampData.CurrentParts.CFrame.p)
HighScalabilityLine.Start = terrain:CellCenterToWorld(tempCell.X, tempCell.Y, tempCell.Z)
return
end
end
end
end
local function loadSurfaceTypes(part, surfaces)
part.TopSurface = surfaces[1]
part.BottomSurface = surfaces[2]
part.LeftSurface = surfaces[3]
part.RightSurface = surfaces[4]
part.FrontSurface = surfaces[5]
part.BackSurface = surfaces[6]
end
local function saveSurfaceTypes(part, myTable)
local tempTable = {}
tempTable[1] = part.TopSurface
tempTable[2] = part.BottomSurface
tempTable[3] = part.LeftSurface
tempTable[4] = part.RightSurface
tempTable[5] = part.FrontSurface
tempTable[6] = part.BackSurface
myTable[part] = tempTable
end
local function makeSurfaceUnjoinable(part, surface)
-- TODO: FILL OUT!
end
local function prepareModel(model)
if not model then return nil end
local gDesiredTrans = 0.7
local gStaticTrans = 1
local clone = model:Clone()
local scripts = {}
local parts = {}
local decals = {}
stampData = {}
stampData.DisabledScripts = {}
stampData.TransparencyTable = {}
stampData.MaterialTable = {}
stampData.CanCollideTable = {}
stampData.AnchoredTable = {}
stampData.ArchivableTable = {}
stampData.DecalTransparencyTable = {}
stampData.SurfaceTypeTable = {}
collectParts(clone, parts, scripts, decals)
if #parts <= 0 then return nil, "no parts found in modelToStamp" end
for index,script in pairs(scripts) do
if not(script.Disabled) then
script.Disabled = true
stampData.DisabledScripts[#stampData.DisabledScripts + 1] = script
end
end
for index, part in pairs(parts) do
stampData.TransparencyTable[part] = part.Transparency
part.Transparency = gStaticTrans + (1 - gStaticTrans) * part.Transparency
stampData.MaterialTable[part] = part.Material
part.Material = Enum.Material.Plastic
stampData.CanCollideTable[part] = part.CanCollide
part.CanCollide = false
stampData.AnchoredTable[part] = part.Anchored
part.Anchored = true
stampData.ArchivableTable[part] = part.Archivable
part.Archivable = false
saveSurfaceTypes(part, stampData.SurfaceTypeTable)
local fadeInDelayTime = 0.5
local transFadeInTime = 0.5
delay(0,function()
wait(fadeInDelayTime) -- give it some time to be completely transparent
local begTime = tick()
local currTime = begTime
while (currTime - begTime) < transFadeInTime and part and part:IsA("BasePart") and part.Transparency > gDesiredTrans do
local newTrans = 1 - (((currTime - begTime)/transFadeInTime) * (gStaticTrans - gDesiredTrans))
if stampData["TransparencyTable"] and stampData.TransparencyTable[part] then
part.Transparency = newTrans + (1 - newTrans) * stampData.TransparencyTable[part]
end
wait(0.03)
currTime = tick()
end
if part and part:IsA("BasePart") then
if stampData["TransparencyTable"] and stampData.TransparencyTable[part] then
part.Transparency = gDesiredTrans + (1 - gDesiredTrans) * stampData.TransparencyTable[part]
end
end
end)
end
for index, decal in pairs(decals) do
stampData.DecalTransparencyTable[decal] = decal.Transparency
decal.Transparency = gDesiredTrans + (1 - gDesiredTrans) * decal.Transparency
end
-- disable all seats
setSeatEnabledStatus(clone, true)
setSeatEnabledStatus(clone, false)
stampData.CurrentParts = clone
-- if auto-alignable, we enforce a pre-rotation to the canonical "0-frame"
if autoAlignToFace(clone) then
stampData.CurrentParts:ResetOrientationToIdentity()
gInitial90DegreeRotations = 0
else -- pre-rotate if necessary
local ry = gInitial90DegreeRotations * math.pi/2
if stampData.CurrentParts:IsA("Model") or stampData.CurrentParts:IsA("Tool") then
--stampData.CurrentParts:Rotate(0, ry, 0)
modelRotate(stampData.CurrentParts, ry)
else
stampData.CurrentParts.CFrame = CFrame.fromEulerAnglesXYZ(0, ry, 0) * stampData.CurrentParts.CFrame
end
end
-- since we're cloning the old model instead of the new one, we will need to update the orientation based on the original value AND how many more
-- rotations we expect since then [either that or we need to store the just-stamped clusterMaterial.Value.Z somewhere]. This should fix the terrain rotation
-- issue (fingers crossed) [HotThoth]
local clusterMaterial = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
if clusterMaterial and clusterMaterial:IsA("Vector3Value") then
clusterMaterial.Value = Vector3.new(clusterMaterial.Value.X, clusterMaterial.Value.Y, (clusterMaterial.Value.Z + gInitial90DegreeRotations) % 4)
end
-- After rotating, update the position
local configFound, targetCFrame = findConfigAtMouseTarget(Mouse, stampData)
if configFound then
stampData.CurrentParts = positionPartsAtCFrame3(targetCFrame, stampData.CurrentParts)
end
-- to show joints during the mouse move
game.JointsService:SetJoinAfterMoveInstance(stampData.CurrentParts)
return clone, parts
end
local function checkTerrainBlockCollisions(cellPos, checkHighScalabilityStamp)
local cellCenterToWorld = game:GetService("Workspace").Terrain.CellCenterToWorld
local cellCenter = cellCenterToWorld(game:GetService("Workspace").Terrain, cellPos.X, cellPos.Y, cellPos.Z)
local cellBlockingParts = game:GetService("Workspace"):FindPartsInRegion3(Region3.new(cellCenter - Vector3.new(2, 2, 2) + insertBoundingBoxOverlapVector, cellCenter + Vector3.new(2, 2, 2) - insertBoundingBoxOverlapVector), stampData.CurrentParts, 100)
local skipThisCell = false
for b = 1, #cellBlockingParts do
if isBlocker(cellBlockingParts[b]) then skipThisCell = true break end
end
if not skipThisCell then
-- pop players up above any set cells
local alreadyPushedUp = {}
-- if no blocking model below, then see if stamping on top of a character
for b = 1, #cellBlockingParts do
if cellBlockingParts[b].Parent and
not alreadyPushedUp[cellBlockingParts[b].Parent] and
cellBlockingParts[b].Parent:FindFirstChild("Humanoid") and
cellBlockingParts[b].Parent:FindFirstChild("Humanoid"):IsA("Humanoid") then
-----------------------------------------------------------------------------------
local blockingPersonTorso = cellBlockingParts[b].Parent:FindFirstChild("Torso")
alreadyPushedUp[cellBlockingParts[b].Parent] = true
if blockingPersonTorso then
-- if so, let's push the person upwards so they pop on top of the stamped model/part (but only if there's space above them)
local newY = cellCenter.Y + 5
if spaceAboveCharacter(blockingPersonTorso, newY, stampData) then
blockingPersonTorso.CFrame = blockingPersonTorso.CFrame + Vector3.new(0, newY - blockingPersonTorso.CFrame.p.Y, 0)
else
-- if no space, we just skip this one
skipThisCell = true
break
end
end
-----------------------------------------------------------------------------------
end
end
end
if not skipThisCell then -- if we STILL aren't skipping... then we're good to go!
local canSetCell = true
if checkHighScalabilityStamp then -- check to see if cell is in region, if not we'll skip set
if allowedStampRegion then
local cellPos = cellCenterToWorld(game:GetService("Workspace").Terrain, cellPos.X, cellPos.Y, cellPos.Z)
if cellPos.X + 2 > allowedStampRegion.CFrame.p.X + allowedStampRegion.Size.X/2 then
canSetCell = false
elseif cellPos.X - 2 < allowedStampRegion.CFrame.p.X - allowedStampRegion.Size.X/2 then
canSetCell = false
elseif cellPos.Y + 2 > allowedStampRegion.CFrame.p.Y + allowedStampRegion.Size.Y/2 then
canSetCell = false
elseif cellPos.Y - 2 < allowedStampRegion.CFrame.p.Y - allowedStampRegion.Size.Y/2 then
canSetCell = false
elseif cellPos.Z + 2 > allowedStampRegion.CFrame.p.Z + allowedStampRegion.Size.Z/2 then
canSetCell = false
elseif cellPos.Z - 2 < allowedStampRegion.CFrame.p.Z - allowedStampRegion.Size.Z/2 then
canSetCell = false
end
end
end
return canSetCell
end
return false
end
local function ResolveMegaClusterStamp(checkHighScalabilityStamp)
local cellSet = false
local cluser = game:GetService("Workspace").Terrain
local line = HighScalabilityLine.InternalLine
local cMax = game:GetService("Workspace").Terrain.MaxExtents.Max
local cMin = game:GetService("Workspace").Terrain.MaxExtents.Min
local clusterMaterial = 1 -- default is grass
local clusterType = 0 -- default is brick
local clusterOrientation = 0 -- default is 0 rotation
local autoWedgeClusterParts = false
if stampData.CurrentParts:FindFirstChild("AutoWedge") then autoWedgeClusterParts = true end
if stampData.CurrentParts:FindFirstChild("ClusterMaterial", true) then
clusterMaterial = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
if clusterMaterial:IsA("Vector3Value") then
clusterType = clusterMaterial.Value.Y
clusterOrientation = clusterMaterial.Value.Z
clusterMaterial = clusterMaterial.Value.X
elseif clusterMaterial:IsA("IntValue") then
clusterMaterial = clusterMaterial.Value
end
end
if HighScalabilityLine.Adorn.Parent and HighScalabilityLine.Start and ((HighScalabilityLine.Dimensions > 1) or (line and line.magnitude > 0)) then
local startCell = game:GetService("Workspace").Terrain:WorldToCell(HighScalabilityLine.Start)
local xInc = {0,0,0}
local yInc = {0,0,0}
local zInc = {0,0,0}
local cluster = game:GetService("Workspace").Terrain
local incrementVect = {nil, nil, nil}
local stepVect = {Vector3.new(0, 0, 0), Vector3.new(0, 0, 0), Vector3.new(0, 0, 0)}
local worldAxes = {Vector3.new(1, 0, 0), Vector3.new(0, 1, 0), Vector3.new(0, 0, 1)}
local lines = {}
if HighScalabilityLine.Dimensions > 1 then table.insert(lines, HighScalabilityLine.MoreLines[1]) end
if line and line.magnitude > 0 then table.insert(lines, line) end
if HighScalabilityLine.Dimensions > 2 then table.insert(lines, HighScalabilityLine.MoreLines[2]) end
for i = 1, #lines do
lines[i] = Vector3.new(math.floor(lines[i].X+.5), math.floor(lines[i].Y+.5), math.floor(lines[i].Z+.5)) -- round to integers
if lines[i].X > 0 then xInc[i] = 1 elseif lines[i].X < 0 then xInc[i] = -1 end
if lines[i].Y > 0 then yInc[i] = 1 elseif lines[i].Y < 0 then yInc[i] = -1 end
if lines[i].Z > 0 then zInc[i] = 1 elseif lines[i].Z < 0 then zInc[i] = -1 end
incrementVect[i] = Vector3.new(xInc[i], yInc[i], zInc[i])
if incrementVect[i].magnitude < .9 then incrementVect[i] = nil end
end
if not lines[2] then lines[2] = Vector3.new(0, 0, 0) end
if not lines[3] then lines[3] = Vector3.new(0, 0, 0) end
local waterForceTag = stampData.CurrentParts:FindFirstChild("WaterForceTag", true)
local waterForceDirectionTag = stampData.CurrentParts:FindFirstChild("WaterForceDirectionTag", true)
while (stepVect[3].magnitude*4 <= lines[3].magnitude) do
local outerStepVectIndex = 1
while outerStepVectIndex < 4 do
stepVect[2] = Vector3.new(0, 0, 0)
while (stepVect[2].magnitude*4 <= lines[2].magnitude) do
local innerStepVectIndex = 1
while innerStepVectIndex < 4 do
stepVect[1] = Vector3.new(0, 0, 0)
while (stepVect[1].magnitude*4 <= lines[1].magnitude) do
local stepVectSum = stepVect[1] + stepVect[2] + stepVect[3]
local cellPos = Vector3int16.new(startCell.X + stepVectSum.X, startCell.Y + stepVectSum.Y, startCell.Z + stepVectSum.Z)
if cellPos.X >= cMin.X and cellPos.Y >= cMin.Y and cellPos.Z >= cMin.Z and cellPos.X < cMax.X and cellPos.Y < cMax.Y and cellPos.Z < cMax.Z then
-- check if overlaps player or part
local okToStampTerrainBlock = checkTerrainBlockCollisions(cellPos, checkHighScalabilityStamp)
if okToStampTerrainBlock then
if waterForceTag then
cluster:SetWaterCell(cellPos.X, cellPos.Y, cellPos.Z, Enum.WaterForce[waterForceTag.Value], Enum.WaterDirection[waterForceDirectionTag.Value])
else
cluster:SetCell(cellPos.X, cellPos.Y, cellPos.Z, clusterMaterial, clusterType, clusterOrientation)
end
cellSet = true
-- auto-wedge it?
if (autoWedgeClusterParts) then
game:GetService("Workspace").Terrain:AutowedgeCells(Region3int16.new(Vector3int16.new(cellPos.x - 1, cellPos.y - 1, cellPos.z - 1),
Vector3int16.new(cellPos.x + 1, cellPos.y + 1, cellPos.z + 1)))
end
end
end
stepVect[1] = stepVect[1] + incrementVect[1]
end
if incrementVect[2] then
while innerStepVectIndex < 4 and worldAxes[innerStepVectIndex]:Dot(incrementVect[2]) == 0 do
innerStepVectIndex = innerStepVectIndex + 1
end
if innerStepVectIndex < 4 then
stepVect[2] = stepVect[2] + worldAxes[innerStepVectIndex] * worldAxes[innerStepVectIndex]:Dot(incrementVect[2])
end
innerStepVectIndex = innerStepVectIndex + 1
else
stepVect[2] = Vector3.new(1, 0, 0)
innerStepVectIndex = 4 -- skip all remaining loops
end
if (stepVect[2].magnitude*4 > lines[2].magnitude) then innerStepVectIndex = 4 end
end
end
if incrementVect[3] then
while outerStepVectIndex < 4 and worldAxes[outerStepVectIndex]:Dot(incrementVect[3]) == 0 do
outerStepVectIndex = outerStepVectIndex + 1
end
if outerStepVectIndex < 4 then
stepVect[3] = stepVect[3] + worldAxes[outerStepVectIndex] * worldAxes[outerStepVectIndex]:Dot(incrementVect[3])
end
outerStepVectIndex = outerStepVectIndex + 1
else -- skip all remaining loops
stepVect[3] = Vector3.new(1, 0, 0) outerStepVectIndex = 4
end
if (stepVect[3].magnitude*4 > lines[3].magnitude) then outerStepVectIndex = 4 end
end
end
end
-- and also get rid of any HighScalabilityLine stuff if it's there
HighScalabilityLine.Start = nil
HighScalabilityLine.Adorn.Parent = nil
-- Mark for undo.
if cellSet then
stampData.CurrentParts.Parent = nil
pcall(function() game:GetService("ChangeHistoryService"): SetWaypoint("StamperMulti") end)
end
return cellSet
end
local function DoStamperMouseUp(Mouse)
if not Mouse then
error("Error: RbxStamper.DoStamperMouseUp: Mouse is nil")
return false
end
if not Mouse:IsA("Mouse") then
error("Error: RbxStamper.DoStamperMouseUp: Mouse is of type", Mouse.className,"should be of type Mouse")
return false
end
if not stampData.Dragger then
error("Error: RbxStamper.DoStamperMouseUp: stampData.Dragger is nil")
return false
end
if not HighScalabilityLine then
return false
end
local checkHighScalabilityStamp = nil
if stampInModel then
local canStamp = nil
local isHSLPart = isMegaClusterPart()
if isHSLPart and
HighScalabilityLine and
HighScalabilityLine.Start and
HighScalabilityLine.InternalLine and
HighScalabilityLine.InternalLine.magnitude > 0 then -- we have an HSL line, test later
canStamp = true
checkHighScalabilityStamp = true
else
canStamp, checkHighScalabilityStamp = t.CanEditRegion(stampData.CurrentParts, allowedStampRegion)
end
if not canStamp then
if stampFailedFunc then
stampFailedFunc()
end
return false
end
end
-- if unstampable face, then don't let us stamp there!
if unstampableSurface then
flashRedBox()
return false
end
-- recheck if we can stamp, as we just moved part
canStamp, checkHighScalabilityStamp = t.CanEditRegion(stampData.CurrentParts, allowedStampRegion)
if not canStamp then
if stampFailedFunc then
stampFailedFunc()
end
return false
end
-- Prevent part from being stamped on top of a player
local minBB, maxBB = getBoundingBoxInWorldCoordinates(stampData.CurrentParts)
-- HotThoth's note: Now that above CurrentParts positioning has been commented out, to be truly correct, we would need to use the
-- value of configFound from the previous onStamperMouseMove call which moved the CurrentParts
-- Shouldn't this be true when lastTargetCFrame has been set and false otherwise?
configFound, targetCFrame = findConfigAtMouseTarget(Mouse, stampData)
if configFound and not HighScalabilityLine.Adorn.Parent then
if clusterPartsInRegion(minBB + insertBoundingBoxOverlapVector, maxBB - insertBoundingBoxOverlapVector) then
flashRedBox()
return false
end
local blockingParts = game:GetService("Workspace"):FindPartsInRegion3(Region3.new(minBB + insertBoundingBoxOverlapVector,
maxBB - insertBoundingBoxOverlapVector),
stampData.CurrentParts,
100)
for b = 1, #blockingParts do
if isBlocker(blockingParts[b]) then
flashRedBox()
return false
end
end
local alreadyPushedUp = {}
-- if no blocking model below, then see if stamping on top of a character
for b = 1, #blockingParts do
if blockingParts[b].Parent and
not alreadyPushedUp[blockingParts[b].Parent] and
blockingParts[b].Parent:FindFirstChild("Humanoid") and
blockingParts[b].Parent:FindFirstChild("Humanoid"):IsA("Humanoid") then
---------------------------------------------------------------------------
local blockingPersonTorso = blockingParts[b].Parent:FindFirstChild("Torso")
alreadyPushedUp[blockingParts[b].Parent] = true
if blockingPersonTorso then
-- if so, let's push the person upwards so they pop on top of the stamped model/part (but only if there's space above them)
local newY = maxBB.Y + 3
if spaceAboveCharacter(blockingPersonTorso, newY, stampData) then
blockingPersonTorso.CFrame = blockingPersonTorso.CFrame + Vector3.new(0, newY - blockingPersonTorso.CFrame.p.Y, 0)
else
-- if no space, we just error
flashRedBox()
return false
end
end
---------------------------------------------------------------------------
end
end
elseif (not configFound) and not (HighScalabilityLine.Start and HighScalabilityLine.Adorn.Parent) then -- if no config then only stamp if it's a real HSL!
resetHighScalabilityLine()
return false
end
-- something will be stamped! so set the "StampedSomething" toggle to true
if game:FindFirstChild("Players") then
if game:GetService("Players")["LocalPlayer"] then
if game:GetService("Players").LocalPlayer["Character"] then
local localChar = game:GetService("Players").LocalPlayer.Character
local stampTracker = localChar:FindFirstChild("StampTracker")
if stampTracker and not stampTracker.Value then
stampTracker.Value = true
end
end
end
end
-- if we drew a line of mega parts, stamp them out
if HighScalabilityLine.Start and HighScalabilityLine.Adorn.Parent and isMegaClusterPart() then
if ResolveMegaClusterStamp(checkHighScalabilityStamp) or checkHighScalabilityStamp then
-- kill the ghost part
stampData.CurrentParts.Parent = nil
return true
end
end
-- not High-Scalability-Line-Based, so behave normally [and get rid of any HSL stuff]
HighScalabilityLine.Start = nil
HighScalabilityLine.Adorn.Parent = nil
local cluster = game:GetService("Workspace").Terrain
-- if target point is in cluster, just use cluster:SetCell
if isMegaClusterPart() then
-- if targetCFrame is inside cluster, just set that cell to 1 and return
--local cellPos = cluster:WorldToCell(targetCFrame.p)
local cellPos
if stampData.CurrentParts:IsA("Model") then cellPos = cluster:WorldToCell(stampData.CurrentParts:GetModelCFrame().p)
else cellPos = cluster:WorldToCell(stampData.CurrentParts.CFrame.p) end
local cMax = game:GetService("Workspace").Terrain.MaxExtents.Max
local cMin = game:GetService("Workspace").Terrain.MaxExtents.Min
if checkTerrainBlockCollisions(cellPos, false) then
local clusterValues = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
local waterForceTag = stampData.CurrentParts:FindFirstChild("WaterForceTag", true)
local waterForceDirectionTag = stampData.CurrentParts:FindFirstChild("WaterForceDirectionTag", true)
if cellPos.X >= cMin.X and cellPos.Y >= cMin.Y and cellPos.Z >= cMin.Z and cellPos.X < cMax.X and cellPos.Y < cMax.Y and cellPos.Z < cMax.Z then
if waterForceTag then
cluster:SetWaterCell(cellPos.X, cellPos.Y, cellPos.Z, Enum.WaterForce[waterForceTag.Value], Enum.WaterDirection[waterForceDirectionTag.Value])
elseif not clusterValues then
cluster:SetCell(cellPos.X, cellPos.Y, cellPos.Z, cellInfo.Material, cellInfo.clusterType, gInitial90DegreeRotations % 4)
elseif clusterValues:IsA("Vector3Value") then
cluster:SetCell(cellPos.X, cellPos.Y, cellPos.Z, clusterValues.Value.X, clusterValues.Value.Y, clusterValues.Value.Z)
else
cluster:SetCell(cellPos.X, cellPos.Y, cellPos.Z, clusterValues.Value, 0, 0)
end
local autoWedgeClusterParts = false
if stampData.CurrentParts:FindFirstChild("AutoWedge") then autoWedgeClusterParts = true end
-- auto-wedge it
if (autoWedgeClusterParts) then
game:GetService("Workspace").Terrain:AutowedgeCells(
Region3int16.new(
Vector3int16.new(cellPos.x - 1, cellPos.y - 1, cellPos.z - 1),
Vector3int16.new(cellPos.x + 1, cellPos.y + 1, cellPos.z + 1)
)
)
end
-- kill the ghost part
stampData.CurrentParts.Parent = nil
-- Mark for undo. It has to happen here or the selection display will come back also.
pcall(function() game:GetService("ChangeHistoryService"):SetWaypoint("StamperSingle") end)
return true
end
else
-- you tried to stamp a HSL-single part where one does not belong!
flashRedBox()
return false
end
end
local function getPlayer()
if game:FindFirstChild("Players") then
if game:GetService("Players")["LocalPlayer"] then
return game:GetService("Players").LocalPlayer
end
end
return nil
end
-- Post process: after positioning the part or model, restore transparency, material, anchored and collide states and create joints
if stampData.CurrentParts:IsA("Model") or stampData.CurrentParts:IsA("Tool") then
if stampData.CurrentParts:IsA("Model") then
-- Tyler's magical hack-code for allowing/preserving clones of both Surface and Manual Welds... just don't ask X<
local manualWeldTable = {}
local manualWeldParentTable = {}
saveTheWelds(stampData.CurrentParts, manualWeldTable, manualWeldParentTable)
stampData.CurrentParts:BreakJoints()
stampData.CurrentParts:MakeJoints()
restoreTheWelds(manualWeldTable, manualWeldParentTable)
end
-- if it's a model, we also want to fill in the playerID and playerName tags, if it has those (e.g. for the friend-only door)
playerIdTag = stampData.CurrentParts:FindFirstChild("PlayerIdTag")
playerNameTag = stampData.CurrentParts:FindFirstChild("PlayerNameTag")
if playerIdTag ~= nil then
tempPlayerValue = getPlayer()
if tempPlayerValue ~= nil then playerIdTag.Value = tempPlayerValue.userId end
end
if playerNameTag ~= nil then
if game:FindFirstChild("Players") and game:GetService("Players")["LocalPlayer"] then
tempPlayerValue = game:GetService("Players").LocalPlayer
if tempPlayerValue ~= nil then playerNameTag.Value = tempPlayerValue.Name end
end
end
-- ...and tag all inserted models for subsequent origin identification
-- if no RobloxModel tag already exists, then add it.
if stampData.CurrentParts:FindFirstChild("RobloxModel") == nil then
local stringTag = Instance.new("BoolValue", stampData.CurrentParts)
stringTag.Name = "RobloxModel"
if stampData.CurrentParts:FindFirstChild("RobloxStamper") == nil then
local stringTag2 = Instance.new("BoolValue", stampData.CurrentParts)
stringTag2.Name = "RobloxStamper"
end
end
else
stampData.CurrentParts:BreakJoints()
if stampData.CurrentParts:FindFirstChild("RobloxStamper") == nil then
local stringTag2 = Instance.new("BoolValue", stampData.CurrentParts)
stringTag2.Name = "RobloxStamper"
end
end
-- make sure all the joints are activated before restoring anchor states
if not createJoints then game.JointsService:CreateJoinAfterMoveJoints() end
-- Restore the original properties for all parts being stamped
for part, transparency in pairs(stampData.TransparencyTable) do
part.Transparency = transparency
end
for part, archivable in pairs(stampData.ArchivableTable) do
part.Archivable = archivable
end
for part, material in pairs(stampData.MaterialTable) do
part.Material = material
end
for part, collide in pairs(stampData.CanCollideTable) do
part.CanCollide = collide
end
for part, anchored in pairs(stampData.AnchoredTable) do
part.Anchored = anchored
end
for decal, transparency in pairs(stampData.DecalTransparencyTable) do
decal.Transparency = transparency
end
for part, surfaces in pairs(stampData.SurfaceTypeTable) do
loadSurfaceTypes(part, surfaces)
end
if isMegaClusterPart() then
stampData.CurrentParts.Transparency = 0
end
-- re-enable all seats
setSeatEnabledStatus(stampData.CurrentParts, true)
stampData.TransparencyTable = nil
stampData.ArchivableTable = nil
stampData.MaterialTable = nil
stampData.CanCollideTable = nil
stampData.AnchoredTable = nil
stampData.SurfaceTypeTable = nil
-- ...and tag all inserted models for subsequent origin identification
-- if no RobloxModel tag already exists, then add it.
if stampData.CurrentParts:FindFirstChild("RobloxModel") == nil then
local stringTag = Instance.new("BoolValue", stampData.CurrentParts)
stringTag.Name = "RobloxModel"
end
-- and make sure we don't delete it, now that it's not a ghost part
if ghostRemovalScript then ghostRemovalScript.Parent = nil end
--Re-enable the scripts
for index,script in pairs(stampData.DisabledScripts) do
script.Disabled = false
end
--Now that they are all marked enabled, reinsert them into the world so they start running
for index,script in pairs(stampData.DisabledScripts) do
local oldParent = script.Parent
script.Parent = nil
script:Clone().Parent = oldParent
end
-- clear out more data
stampData.DisabledScripts = nil
stampData.Dragger = nil
stampData.CurrentParts = nil
pcall(function() game:GetService("ChangeHistoryService"): SetWaypoint("StampedObject") end)
return true
end
local function pauseStamper()
for i = 1, #mouseCons do -- stop the mouse from doing anything
mouseCons[i]:disconnect()
mouseCons[i] = nil
end
mouseCons = {}
if stampData and stampData.CurrentParts then -- remove our ghost part
stampData.CurrentParts.Parent = nil
stampData.CurrentParts:Remove()
end
resetHighScalabilityLine()
game.JointsService:ClearJoinAfterMoveJoints()
end
local function prepareUnjoinableSurfaces(modelCFrame, parts, whichSurface)
local AXIS_VECTORS = {Vector3.new(1, 0, 0), Vector3.new(0, 1, 0), Vector3.new(0, 0, 1)} -- maybe last one is negative? TODO: check this!
local isPositive = 1
if whichSurface < 0 then isPositive = isPositive * -1 whichSurface = whichSurface*-1 end
local surfaceNormal = isPositive * modelCFrame:vectorToWorldSpace(AXIS_VECTORS[whichSurface])
for i = 1, #parts do
local currPart = parts[i]
-- now just need to find which surface of currPart most closely match surfaceNormal and then set that to Unjoinable
local surfaceNormalInLocalCoords = currPart.CFrame:vectorToObjectSpace(surfaceNormal)
if math.abs(surfaceNormalInLocalCoords.X) > math.abs(surfaceNormalInLocalCoords.Y) then
if math.abs(surfaceNormalInLocalCoords.X) > math.abs(surfaceNormalInLocalCoords.Z) then
if surfaceNormalInLocalCoords.X > 0 then currPart.RightSurface = "Unjoinable" else currPart.LeftSurface = "Unjoinable" end
else
if surfaceNormalInLocalCoords.Z > 0 then currPart.BackSurface = "Unjoinable" else currPart.FrontSurface = "Unjoinable" end
end
else
if math.abs(surfaceNormalInLocalCoords.Y) > math.abs(surfaceNormalInLocalCoords.Z) then
if surfaceNormalInLocalCoords.Y > 0 then currPart.TopSurface = "Unjoinable" else currPart.BottomSurface = "Unjoinable" end
else
if surfaceNormalInLocalCoords.Z > 0 then currPart.BackSurface = "Unjoinable" else currPart.FrontSurface = "Unjoinable" end
end
end
end
end
local function resumeStamper()
clone, parts = prepareModel(modelToStamp)
if not clone or not parts then
return
end
-- if we have unjoinable faces, then we want to change those surfaces to be Unjoinable
local unjoinableTag = clone:FindFirstChild("UnjoinableFaces", true)
if unjoinableTag then
for unjoinableSurface in string.gmatch(unjoinableTag.Value, "[^,]*") do
if tonumber(unjoinableSurface) then
if clone:IsA("Model") then
prepareUnjoinableSurfaces(clone:GetModelCFrame(), parts, tonumber(unjoinableSurface))
else
prepareUnjoinableSurfaces(clone.CFrame, parts, tonumber(unjoinableSurface))
end
end
end
end
stampData.ErrorBox = errorBox
if stampInModel then
clone.Parent = stampInModel
else
clone.Parent = game:GetService("Workspace")
end
if clone:FindFirstChild("ClusterMaterial", true) then -- extract all info from vector
clusterMaterial = clone:FindFirstChild("ClusterMaterial", true)
if (clusterMaterial:IsA("Vector3Value")) then
cellInfo.Material = clusterMaterial.Value.X
cellInfo.clusterType = clusterMaterial.Value.Y
cellInfo.clusterOrientation = clusterMaterial.Value.Z
elseif clusterMaterial:IsA("IntValue") then
cellInfo.Material = clusterMaterial.Value
end
end
pcall(function() mouseTarget = Mouse.Target end)
if mouseTarget and mouseTarget.Parent:FindFirstChild("RobloxModel") == nil then
game.JointsService:SetJoinAfterMoveTarget(mouseTarget)
else
game.JointsService:SetJoinAfterMoveTarget(nil)
end
game.JointsService:ShowPermissibleJoints()
for index, object in pairs(stampData.DisabledScripts) do
if object.Name == "GhostRemovalScript" then
object.Parent = stampData.CurrentParts
end
end
stampData.Dragger = Instance.new("Dragger")
--Begin a movement by faking a MouseDown signal
stampData.Dragger:MouseDown(parts[1], Vector3.new(0,0,0), parts)
stampData.Dragger:MouseUp()
DoStamperMouseMove(Mouse)
table.insert(mouseCons,Mouse.Move:connect(function()
if movingLock or stampUpLock then return end
movingLock = true
DoStamperMouseMove(Mouse)
movingLock = false
end))
table.insert(mouseCons,Mouse.Button1Down:connect(function()
DoStamperMouseDown(Mouse)
end))
table.insert(mouseCons,Mouse.Button1Up:connect(function()
stampUpLock = true
while movingLock do wait() end
stamped.Value = DoStamperMouseUp(Mouse)
resetHighScalabilityLine()
stampUpLock = false
end))
stamped.Value = false
end
local function resetStamperState(newModelToStamp)
-- if we have a new model, swap it out
if newModelToStamp then
if not newModelToStamp:IsA("Model") and not newModelToStamp:IsA("BasePart") then
error("resetStamperState: newModelToStamp (first arg) is not nil, but not a model or part!")
end
modelToStamp = newModelToStamp
end
-- first clear our state
pauseStamper()
-- now lets load in the new model
resumeStamper()
end
-- load the model initially
resetStamperState()
-- setup the control table we pass back to the user
control.Stamped = stamped -- BoolValue that fires when user stamps
control.Paused = false
control.LoadNewModel = function(newStampModel) -- allows us to specify a new stamper model to be used with this stamper
if newStampModel and not newStampModel:IsA("Model") and not newStampModel:IsA("BasePart") then
error("Control.LoadNewModel: newStampModel (first arg) is not a Model or Part!")
return nil
end
resetStamperState(newStampModel)
end
control.ReloadModel = function() -- will automatically set stamper to get a new model of current model and start stamping with new model
resetStamperState()
end
control.Pause = function() -- temporarily stops stamping, use resume to start up again
if not control.Paused then
pauseStamper()
control.Paused = true
else
print("RbxStamper Warning: Tried to call Control.Pause() when already paused")
end
end
control.Resume = function() -- resumes stamping, if currently paused
if control.Paused then
resumeStamper()
control.Paused = false
else
print("RbxStamper Warning: Tried to call Control.Resume() without Pausing First")
end
end
control.ResetRotation = function() -- resets the model rotation so new models are at default orientation
-- gInitial90DegreeRotations = 0
-- Note: This function will not always work quite the way we want it to; we will have to build this out further so it works with
-- High-Scalability and with the new model orientation setting methods (model:ResetOrientationToIdentity()) [HotThoth]
end
control.Destroy = function() -- Stops current Stamp operation and destroys control construct
for i = 1, #mouseCons do
mouseCons[i]:disconnect()
mouseCons[i] = nil
end
if keyCon then
keyCon:disconnect()
end
game.JointsService:ClearJoinAfterMoveJoints()
if adorn then adorn:Destroy() end
if adornPart then adornPart:Destroy() end
if errorBox then errorBox:Destroy() end
if stampData then
if stampData["Dragger"] then
stampData.Dragger:Destroy()
end
if stampData.CurrentParts then
stampData.CurrentParts:Destroy()
end
end
if control and control["Stamped"] then
control.Stamped:Destroy()
end
control = nil
end
return control
end
t.Help =
function(funcNameOrFunc)
--input argument can be a string or a function. Should return a description (of arguments and expected side effects)
if funcNameOrFunc == "GetStampModel" or funcNameOrFunc == t.GetStampModel then
return "Function GetStampModel. Arguments: assetId, useAssetVersionId. assetId is the asset to load in, define useAssetVersionId as true if assetId is a version id instead of a relative assetId. Side effect: returns a model of the assetId, or a string with error message if something fails"
end
if funcNameOrFunc == "SetupStamperDragger" or funcNameOrFunc == t.SetupStamperDragger then
return "Function SetupStamperDragger. Side Effect: Creates 4x4 stamping mechanism for building out parts quickly. Arguments: ModelToStamp, Mouse, LegalStampCheckFunction. ModelToStamp should be a Model or Part, preferrably loaded from RbxStamper.GetStampModel and should have extents that are multiples of 4. Mouse should be a mouse object (obtained from things such as Tool.OnEquipped), used to drag parts around 'stamp' them out. LegalStampCheckFunction is optional, used as a callback with a table argument (table is full of instances about to be stamped). Function should return either true or false, false stopping the stamp action."
end
end
return t