2013/luau/73157242.luau

3000 lines
82 KiB
Plaintext

-- RbxStamper
print "[Mercury]: Loaded corescript 73157242"
local ChangeHistoryService = game:GetService "ChangeHistoryService"
local InsertService = game:GetService "InsertService"
local JointsService = game:GetService "JointsService"
local Lighting = game:GetService "Lighting"
local Players = game:GetService "Players"
local News = require "../Modules/New"
local New = News.New
local Hydrate = News.Hydrate
local BaseUrl = require "../Modules/BaseUrl"
local path = BaseUrl.path
local RbxStamper = {}
-- 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.
local function PlaneIntersection(vectorPos)
local hit = false
local currCamera = 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 = 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.
local 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 = workspace.Terrain:WorldToCellPreferSolid(
Vector3.new(mouse.hit.x, mouse.hit.y, mouse.hit.z)
)
-- If nothing was hit, do the plane intersection.
if 0 == workspace.Terrain:GetCell(cell.X, cell.Y, cell.Z).Value then
cell = nil
local 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(0.3, 0.3, 0.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 _, child in pairs(object:GetChildren()) do
collectParts(child, baseParts, scripts, decals)
end
end
local cluster
local function clusterPartsInRegion(startVector, endVector)
cluster = 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
elseif 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
end
return false
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
end
return 3
elseif
math.abs(yDot) > math.abs(xDot) and math.abs(yDot) > math.abs(zDot)
then
if yDot > 0 then
return 1
end
return 4
end
if zDot > 0 then
return 2
end
return 5
end
local function positionPartsAtCFrame3(aCFrame, currentParts)
local insertCFrame
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) < 0.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
local modelSize
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
local justify = justifyValue.Value
local two = Vector3.new(2, 2, 2)
local actualBox = maxVec - minVec - Vector3.new(0.01, 0.01, 0.01)
local containingGridBox = Vector3.new(
4 * math.ceil(actualBox.x / 4),
4 * math.ceil(actualBox.y / 4),
4 * math.ceil(actualBox.z / 4)
)
local adjustment = containingGridBox - actualBox
minVec -= 0.5 * adjustment * justify
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
local vec1 =
partOrModel.CFrame:pointToWorldSpace(-0.5 * partOrModel.Size)
local 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 not 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
local vec1 = partOrModel
:GetModelCFrame()
:pointToWorldSpace(-0.5 * partOrModel:GetModelSize())
local 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" then
return getBoundingBox2(targetPart.Parent)
end
return getBoundingBox2(targetPart)
end
local function getMouseTargetCFrame(targetPart)
if not targetPart.Parent:FindFirstChild "RobloxModel" then
return targetPart.CFrame
elseif targetPart.Parent:IsA "Tool" then
return targetPart.Parent.Handle.CFrame
end
return targetPart.Parent:GetModelCFrame()
end
local function isBlocker(part: Instance) -- returns whether or not we want to cancel the stamp because we're blocked by this part
if not part or not part.Parent or part:FindFirstChild "Humanoid" then
return false
elseif
part:FindFirstChild "RobloxStamper"
or part:FindFirstChild "RobloxModel"
then
return true
elseif (part:IsA "Part" and not part.CanCollide) or (part == 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: BasePart,
newTorsoY,
stampData: Instance
)
local partsAboveChar = workspace:FindPartsInRegion3(
Region3.new(
Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z)
- Vector3.new(0.75, 2.75, 0.75),
Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z)
+ Vector3.new(0.75, 1.75, 0.75)
),
charTorso.Parent :: Instance,
100
)
for j = 1, #partsAboveChar do
if
partsAboveChar[j].CanCollide
and not partsAboveChar[j]:IsDescendantOf(stampData.CurrentParts)
then
return false
end
end
return not clusterPartsInRegion(
Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z)
- Vector3.new(0.75, 2.75, 0.75),
Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z)
+ Vector3.new(0.75, 1.75, 0.75)
)
end
local function findConfigAtMouseTarget(
Mouse: Mouse,
stampData: Instance & {
CurrentParts: Instance,
}
)
-- *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.
-- This can happen sometimes, return if so
if not Mouse then
return
elseif not stampData then
error "findConfigAtMouseTarget: stampData is nil"
return
elseif not stampData.CurrentParts then
return
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 and stampData.CurrentParts:IsA "Tool" then
Mouse.TargetFilter = stampData.CurrentParts.Handle
else
Mouse.TargetFilter = stampData.CurrentParts
end
local hitPlane = false
local targetPart
local ok = pcall(function()
targetPart = Mouse.Target
end)
if not ok 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
-- 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
end
targetPart = workspace.Terrain
hitPlane = true
-- Take into account error that will occur.
cellPos = Vector3.new(cellPos.X - 1, cellPos.Y, cellPos.Z)
mouseHitInWorld =
workspace.Terrain:CellCenterToWorld(cellPos.x, cellPos.y, cellPos.z)
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 = workspace:FindFirstChild "Terrain" :: Terrain
end
local cellID = cluster:WorldToCellPreferSolid(mouseHitInWorld)
if hitPlane then
cellID = cellPos
end
targetCFrame = CFrame.new(
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,
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, insertRefPointInInsert
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 *= (0.5 * diagBBTarget) + 0.5 * (maxBBTarget + minBBTarget)
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 *= clampToSurface
local targetTouchInTarget = deltaClamped + targetRefPointInTarget
local TargetTouchRelToWorld =
targetCFrame:pointToWorldSpace(targetTouchInTarget)
local InsertTouchInWorld =
insertCFrame:vectorToWorldSpace(insertRefPointInInsert)
local posInsertOriginInWorld = TargetTouchRelToWorld - InsertTouchInWorld
local _, _, _, 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 > 0.382683432 then
-- between 22.5 and 45 degrees, so truncate to 45-degree tilt
return 0.707106781 * hypotenuse * bigSign,
0.707106781 * hypotenuse * littleSign
end
-- between 0 and 22.5 degrees, so truncate to 0-degree tilt
return hypotenuse * bigSign, 0
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
function RbxStamper.CanEditRegion(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
elseif
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
local function UnlockInstances(object)
if object:IsA "BasePart" then
object.Locked = false
end
for _, child in pairs(object:GetChildren()) do
UnlockInstances(child)
end
end
local function closestTerrainMaterialColour(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"
end
return BrickColor.new "Bright green"
end
local function setupFakeTerrainPart(cellMat, cellType, cellOrient)
local newTerrainPiece
if cellType == 1 or cellType == 4 then
newTerrainPiece = New "WedgePart" {
formFactor = "Custom",
}
elseif cellType == 2 then
newTerrainPiece = Instance.new "CornerWedgePart"
else
newTerrainPiece = New "Part" {
formFactor = "Custom",
}
end
Hydrate(newTerrainPiece) {
Name = "MegaClusterCube",
Size = Vector3.new(4, 4, 4),
BottomSurface = "Smooth",
TopSurface = "Smooth",
}
-- can add decals or textures here if feeling particularly adventurous... for now, can make a table of look-up colors
newTerrainPiece.BrickColor = closestTerrainMaterialColour(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
New "SpecialMesh" {
MeshType = "FileMesh",
MeshId = path "asset?id=66832495",
Scale = Vector3.new(2, 2, 2),
Parent = newTerrainPiece,
}
end
New "Vector3Value" {
Value = Vector3.new(cellMat, cellType, cellOrient),
Name = "ClusterMaterial",
Parent = newTerrainPiece,
}
return newTerrainPiece
end
function RbxStamper.GetStampModel(assetId, terrainShape, useAssetVersionId)
if assetId == 0 then
return nil, "No Asset"
elseif assetId < 0 then
return nil, "Negative Asset"
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
local loading = true
if useAssetVersionId then
loader = coroutine.create(function()
root = InsertService:LoadAssetVersion(assetId)
loading = false
end)
coroutine.resume(loader)
else
loader = coroutine.create(function()
root = 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 += tick() - lastGameTime
end
loading = false
if totalTime >= maxWait then
return nil, "Load Time Fail"
elseif root == nil then
return nil, "Load Asset Fail"
elseif 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 _, 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 _, 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"
stringTag.Name = "RobloxModel"
stringTag.Parent = root
if root:FindFirstChild "RobloxStamper" == nil then
local stringTag2 = Instance.new "BoolValue"
stringTag2.Name = "RobloxStamper"
stringTag2.Parent = root
end
end
if terrainShape and 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
return root
end
local function isMegaClusterPart(stampData: {
CurrentParts: BasePart,
})
if not (stampData and stampData.CurrentParts) then
return false
end
return (
stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
or (stampData.CurrentParts.Name == "MegaClusterCube")
)
end
type HighScalabilityLine = {
Adorn: Instance,
Start: Vector3,
End: Vector3,
Dimensions: number,
MoreLines: { Vector3 },
MorePoints: { Vector3 },
InternalLine: Vector3,
AdornPart: BasePart,
}
local function DoHighScalabilityRegionSelect(
stampData: {
CurrentParts: BasePart,
},
HighScalabilityLine: HighScalabilityLine
)
local megaCube = stampData.CurrentParts:FindFirstChild "MegaClusterCube"
if not megaCube then
if stampData.CurrentParts.Name ~= "MegaClusterCube" then
return
end
megaCube = stampData.CurrentParts
end
HighScalabilityLine.End = megaCube.CFrame.p
local line
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 -= line.unit * line.unit:Dot(line2)
local 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 -= xAxis * xComp
else
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 -= line.unit * line.unit:Dot(line3)
line3 -= line2.unit * line2.unit:Dot(line3)
HighScalabilityLine.InternalLine = line3
end
-- resize the "line" graphic to be the correct size and orientation
local 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
if Players.LocalPlayer then
gui = Players.LocalPlayer:FindFirstChild "PlayerGui"
if
gui
and gui:IsA "PlayerGui"
and (
(HighScalabilityLine.Dimensions == 1 and line.magnitude > 3)
or HighScalabilityLine.Dimensions > 1
)
then -- don't show if mouse hasn't moved enough
HighScalabilityLine.Adorn.Parent = gui
end
end
if not gui then -- we are in studio
gui = game:GetService "CoreGui"
if
(HighScalabilityLine.Dimensions == 1 and line.magnitude > 3)
or HighScalabilityLine.Dimensions > 1
then -- don't show if mouse hasn't moved enough
HighScalabilityLine.Adorn.Parent = gui
end
end
end
local function flashRedBox(stampData: {
CurrentParts: BasePart,
ErrorBox: SelectionBox,
})
local gui = game.CoreGui
if
game:FindFirstChild "Players"
and Players.LocalPlayer
and Players.LocalPlayer:FindFirstChild "PlayerGui"
then
gui = Players.LocalPlayer.PlayerGui
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 _ = 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 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 checkTerrainBlockCollisions(
stampData,
allowedStampRegion,
cellPos,
checkHighScalabilityStamp
)
local cellCenterToWorld = workspace.Terrain.CellCenterToWorld
local cellCenter =
cellCenterToWorld(workspace.Terrain, cellPos.X, cellPos.Y, cellPos.Z)
local cellBlockingParts = 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
cellPos = cellCenterToWorld(
workspace.Terrain,
cellPos.X,
cellPos.Y,
cellPos.Z
)
if
(
cellPos.X + 2
> allowedStampRegion.CFrame.p.X
+ allowedStampRegion.Size.X / 2
)
or (cellPos.X - 2 < allowedStampRegion.CFrame.p.X - allowedStampRegion.Size.X / 2)
or (cellPos.Y + 2 > allowedStampRegion.CFrame.p.Y + allowedStampRegion.Size.Y / 2)
or (cellPos.Y - 2 < allowedStampRegion.CFrame.p.Y - allowedStampRegion.Size.Y / 2)
or (cellPos.Z + 2 > allowedStampRegion.CFrame.p.Z + allowedStampRegion.Size.Z / 2)
or (
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(
stampData: {
CurrentParts: BasePart,
},
HighScalabilityLine: HighScalabilityLine,
checkHighScalabilityStamp,
allowedStampRegion
)
local cellSet = false
cluster = workspace.Terrain
local line = HighScalabilityLine.InternalLine
local cMax = workspace.Terrain.MaxExtents.Max
local cMin = 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 =
workspace.Terrain:WorldToCell(HighScalabilityLine.Start)
local xInc = { 0, 0, 0 }
local yInc = { 0, 0, 0 }
local zInc = { 0, 0, 0 }
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 + 0.5),
math.floor(lines[i].Y + 0.5),
math.floor(lines[i].Z + 0.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 < 0.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(
stampData,
allowedStampRegion,
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
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] += incrementVect[1]
end
if incrementVect[2] then
while
innerStepVectIndex < 4
and worldAxes[innerStepVectIndex]:Dot(
incrementVect[2]
)
== 0
do
innerStepVectIndex += 1
end
if innerStepVectIndex < 4 then
stepVect[2] = stepVect[2]
+ worldAxes[innerStepVectIndex]
* worldAxes[innerStepVectIndex]:Dot(
incrementVect[2]
)
end
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 += 1
end
if outerStepVectIndex < 4 then
stepVect[3] = stepVect[3]
+ worldAxes[outerStepVectIndex]
* worldAxes[outerStepVectIndex]:Dot(
incrementVect[3]
)
end
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()
ChangeHistoryService:SetWaypoint "StamperMulti"
end)
end
return cellSet
end
function RbxStamper.SetupStamperDragger(
modelToStamp: Model | BasePart,
Mouse: Mouse,
StampInModel: Model?,
AllowedStampRegion,
StampFailedFunc: () -> ()?
)
if not modelToStamp then
error "SetupStamperDragger: modelToStamp (first arg) is nil! Should be a stamper model"
return nil
elseif not modelToStamp:IsA "Model" and not modelToStamp:IsA "BasePart" then
error "SetupStamperDragger: modelToStamp (first arg) is neither a Model or Part!"
return nil
elseif not Mouse then
error "SetupStamperDragger: Mouse (second arg) is nil! Should be a mouse object"
return nil
elseif not Mouse:IsA "Mouse" then
error "SetupStamperDragger: Mouse (second arg) is not of type Mouse!"
return nil
end
local configFound, targetCFrame, targetSurface
local stampInModel
local allowedStampRegion
local stampFailedFunc
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: {
DisabledScripts: { LuaSourceContainer }?,
TransparencyTable: {}?,
MaterialTable: {}?,
CanCollideTable: {}?,
AnchoredTable: {}?,
ArchivableTable: {}?,
DecalTransparencyTable: {}?,
SurfaceTypeTable: {}?,
CurrentParts: BasePart & {
ErrorBox: SelectionBox?,
},
}
local mouseTarget
local errorBox = New "SelectionBox" {
Color = BrickColor.new "Bright red",
Transparency = 0,
Archivable = false,
}
-- for megacluster MEGA STAMPING
local adornPart = New "Part" {
formFactor = "Custom",
Size = Vector3.new(4, 4, 4),
CFrame = CFrame.new(),
Archivable = false,
}
local adorn = New "SelectionBox" {
Color = BrickColor.new "Toothpaste",
Adornee = adornPart,
Visible = true,
Transparency = 0,
Name = "HighScalabilityStamperLine",
Archivable = false,
}
local HighScalabilityLine: HighScalabilityLine = {
Start = nil,
End = nil,
Adorn = adorn,
AdornPart = adornPart,
InternalLine = nil,
NewHint = true,
MorePoints = { nil, nil },
MoreLines = { nil, nil },
Dimensions = 1,
}
local control = {}
local movingLock = false
local stampUpLock = false
local unstampableSurface = false
local mouseCons = {}
local keyCon
local stamped = New "BoolValue" {
Archivable = false,
Value = false,
}
local lastTarget = {
TerrainOrientation = 0,
CFrame = 0,
}
local cellInfo = {
Material = 1,
clusterType = 0,
clusterOrientation = 0,
}
local function DoStamperMouseMove(mouse)
if not mouse then
error "Error: RbxStamper.DoStamperMouseMove: Mouse is nil"
return
elseif 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 or GetTerrainForMouse(mouse)) or 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 += 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
if stampData.CurrentParts:IsA "Model" then
currModelCFrame = stampData.CurrentParts:GetModelCFrame()
else
currModelCFrame = stampData.CurrentParts.CFrame
end
minBB += targetCFrame.p - currModelCFrame.p
maxBB += targetCFrame.p - currModelCFrame.p
local clusterMat: Instance
-- don't drag into terrain
if
clusterPartsInRegion(
minBB + insertBoundingBoxOverlapVector,
maxBB - insertBoundingBoxOverlapVector
)
then
if
lastTarget.CFrame
and 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
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(stampData) then
local cellToStamp = workspace.Terrain:WorldToCell(targetCFrame.p)
local newCFramePosition = workspace.Terrain:CellCenterToWorld(
cellToStamp.X,
cellToStamp.Y,
cellToStamp.Z
)
local _, _, _, 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
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,
(workspace.CurrentCamera :: Camera).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
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,
(workspace.CurrentCamera :: Camera).CoordinateFrame.p
)
-- are we stamping WITH an unstampable surface?
for bf in string.gmatch(myBreakingFaces, "[^,]+") do
if hitFace == tonumber(bf) then
unstampableSurface = true
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
JointsService:SetJoinAfterMoveInstance(stampData.CurrentParts)
-- most common mouse inactive error occurs here, so check mouse active one more time in a pcall
local ok = pcall(function()
return mouse -- ? - Heliodex
and mouse.Target
and not mouse.Target.Parent:FindFirstChild "RobloxModel"
end)
if not ok then
JointsService:ClearJoinAfterMoveJoints()
mouse = nil
error "Error: RbxStamper.DoStamperMouseMove Mouse is nil on second check"
return
end
if
mouse
and mouse.Target
and mouse.Target.Parent:FindFirstChild "RobloxModel" == nil
then
JointsService:SetJoinAfterMoveTarget(mouse.Target)
else
JointsService:SetJoinAfterMoveTarget(nil)
end
JointsService:ShowPermissibleJoints()
-- here we allow for a line of high-scalability parts
if
isMegaClusterPart(stampData)
and HighScalabilityLine
and HighScalabilityLine.Start
then
DoHighScalabilityRegionSelect(stampData, HighScalabilityLine)
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 += 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"
and HighScalabilityLine.InternalLine
and HighScalabilityLine.InternalLine.magnitude > 0
and HighScalabilityLine.Dimensions < 3
then -- try to expand our high scalability dragger dimension
HighScalabilityLine.MorePoints[HighScalabilityLine.Dimensions] =
HighScalabilityLine.End
HighScalabilityLine.MoreLines[HighScalabilityLine.Dimensions] =
HighScalabilityLine.InternalLine
HighScalabilityLine.Dimensions = HighScalabilityLine.Dimensions + 1
HighScalabilityLine.NewHint = true
end
end
keyCon = Mouse.KeyDown:connect(
function(key) -- init key connection (keeping code close to func)
setupKeyListener(key, Mouse)
end
)
local function resetHighScalabilityLine()
if not HighScalabilityLine then
return
end
HighScalabilityLine.Start = nil
HighScalabilityLine.End = nil
HighScalabilityLine.InternalLine = nil
HighScalabilityLine.NewHint = true
end
local function DoStamperMouseDown(mouse)
if not mouse then
error "Error: RbxStamper.DoStamperMouseDown: Mouse is nil"
return
elseif not mouse:IsA "Mouse" then
error(
`Error: RbxStamper.DoStamperMouseDown: Mouse is of type {mouse.className}, should be of type Mouse`
)
return
elseif
not (
stampData
and isMegaClusterPart(stampData)
and mouse
and HighScalabilityLine
)
then
return
end
local megaCube =
stampData.CurrentParts:FindFirstChild("MegaClusterCube", true)
local terrain = 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
-- 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 = {
DisabledScripts = {},
TransparencyTable = {},
MaterialTable = {},
CanCollideTable = {},
AnchoredTable = {},
ArchivableTable = {},
DecalTransparencyTable = {},
SurfaceTypeTable = {},
}
collectParts(clone, parts, scripts, decals)
if #parts <= 0 then
return nil, "no parts found in modelToStamp"
end
for _, script in pairs(scripts) do
if not script.Disabled then
script.Disabled = true
stampData.DisabledScripts[#stampData.DisabledScripts + 1] =
script
end
end
for _, 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"
and stampData.TransparencyTable
and stampData.TransparencyTable[part]
then
part.Transparency = gDesiredTrans
+ (1 - gDesiredTrans)
* stampData.TransparencyTable[part]
end
end)
end
for _, 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
configFound, targetCFrame = findConfigAtMouseTarget(Mouse, stampData)
if configFound then
stampData.CurrentParts =
positionPartsAtCFrame3(targetCFrame, stampData.CurrentParts)
end
-- to show joints during the mouse move
JointsService:SetJoinAfterMoveInstance(stampData.CurrentParts)
return clone, parts
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
if stampInModel then
local canStamp
local isHSLPart = isMegaClusterPart(stampData)
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 = RbxStamper.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(stampData)
return false
end
-- recheck if we can stamp, as we just moved part
local canStamp
canStamp, checkHighScalabilityStamp =
RbxStamper.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(stampData)
return false
end
local blockingParts = workspace:FindPartsInRegion3(
Region3.new(
minBB + insertBoundingBoxOverlapVector,
maxBB - insertBoundingBoxOverlapVector
),
stampData.CurrentParts,
100
)
for b = 1, #blockingParts do
if isBlocker(blockingParts[b]) then
flashRedBox(stampData)
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(stampData)
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"
and Players.LocalPlayer
and Players.LocalPlayer.Character
then
local stampTracker =
Players.LocalPlayer.Character:FindFirstChild "StampTracker"
if stampTracker and not stampTracker.Value then
stampTracker.Value = true
end
end
-- if we drew a line of mega parts, stamp them out
if
HighScalabilityLine.Start
and HighScalabilityLine.Adorn.Parent
and isMegaClusterPart(stampData)
and (
ResolveMegaClusterStamp(
stampData,
HighScalabilityLine,
checkHighScalabilityStamp,
allowedStampRegion
) or checkHighScalabilityStamp
)
then
-- kill the ghost part
stampData.CurrentParts.Parent = nil
return true
end
-- not High-Scalability-Line-Based, so behave normally [and get rid of any HSL stuff]
HighScalabilityLine.Start = nil
HighScalabilityLine.Adorn.Parent = nil
cluster = workspace.Terrain
-- if target point is in cluster, just use cluster:SetCell
if isMegaClusterPart(stampData) 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 = workspace.Terrain.MaxExtents.Max
local cMin = workspace.Terrain.MaxExtents.Min
if
checkTerrainBlockCollisions(
stampData,
allowedStampRegion,
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
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()
ChangeHistoryService:SetWaypoint "StamperSingle"
end)
return true
end
else
-- you tried to stamp a HSL-single part where one does not belong!
flashRedBox(stampData)
return false
end
end
local function getPlayer()
if game:FindFirstChild "Players" and Players.LocalPlayer then
return Players.LocalPlayer
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)
local playerIdTag =
stampData.CurrentParts:FindFirstChild "PlayerIdTag"
local playerNameTag =
stampData.CurrentParts:FindFirstChild "PlayerNameTag"
local tempPlayerValue
if playerIdTag then
tempPlayerValue = getPlayer()
if tempPlayerValue then
playerIdTag.Value = tempPlayerValue.userId
end
end
if
playerNameTag
and game:FindFirstChild "Players"
and Players.LocalPlayer
then
tempPlayerValue = Players.LocalPlayer
if tempPlayerValue then
playerNameTag.Value = tempPlayerValue.Name
end
end
-- ...and tag all inserted models for subsequent origin identification
-- if no RobloxModel tag already exists, then add it.
if not stampData.CurrentParts:FindFirstChild "RobloxModel" then
New "BoolValue" {
Name = "RobloxModel",
Parent = stampData.CurrentParts,
}
if
not stampData.CurrentParts:FindFirstChild "RobloxStamper"
then
New "BoolValue" {
Name = "RobloxStamper",
Parent = stampData.CurrentParts,
}
end
end
else
stampData.CurrentParts:BreakJoints()
if stampData.CurrentParts:FindFirstChild "RobloxStamper" == nil then
New "BoolValue" {
Name = "RobloxStamper",
Parent = stampData.CurrentParts,
}
end
end
-- make sure all the joints are activated before restoring anchor states
-- if not createJoints then
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(stampData) 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"
stringTag.Name = "RobloxModel"
stringTag.Parent = stampData.CurrentParts
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 _, 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 _, 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()
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()
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 *= -1
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()
local 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
clone.Parent = stampInModel or workspace
if clone:FindFirstChild("ClusterMaterial", true) then -- extract all info from vector
local 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
JointsService:SetJoinAfterMoveTarget(mouseTarget)
else
JointsService:SetJoinAfterMoveTarget(nil)
end
JointsService:ShowPermissibleJoints()
for _, 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
function control.LoadNewModel(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)
return -- bruh
end
function control.ReloadModel() -- will automatically set stamper to get a new model of current model and start stamping with new model
resetStamperState()
end
function control.Pause() -- 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
function control.Resume() -- 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
function control.ResetRotation() -- 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
function control.Destroy() -- 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
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
function RbxStamper.Help(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 == RbxStamper.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]]
elseif
funcNameOrFunc == "SetupStamperDragger"
or funcNameOrFunc == RbxStamper.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
return "No help available for this function"
end
return RbxStamper