@kunosyn/shatterbox 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/package.json +36 -0
- package/src/Effects.d.ts +134 -0
- package/src/Effects.luau +114 -0
- package/src/Settings.luau +75 -0
- package/src/index.d.ts +617 -0
- package/src/init.luau +2580 -0
- package/src/lib/Client.luau +4020 -0
- package/src/lib/InitializeShatterboxClients.client.luau +10 -0
- package/src/lib/ObjectCache.luau +198 -0
- package/src/lib/PartOperations.luau +673 -0
- package/src/lib/Server.luau +4425 -0
- package/src/lib/VertexMath.luau +403 -0
- package/src/types.ts +504 -0
- package/src/util/HitboxVisualizer.luau +101 -0
- package/src/util/TaggedArray.luau +66 -0
package/src/init.luau
ADDED
|
@@ -0,0 +1,2580 @@
|
|
|
1
|
+
--!native
|
|
2
|
+
--!optimize 2
|
|
3
|
+
|
|
4
|
+
-- Shatterbox release 8
|
|
5
|
+
|
|
6
|
+
--#selene: allow(multiple_statements)
|
|
7
|
+
--#selene: allow(unused_variable)
|
|
8
|
+
--#selene: allow(incorrect_standard_library_use)
|
|
9
|
+
--#selene: allow(shadowing)
|
|
10
|
+
|
|
11
|
+
-- Constants
|
|
12
|
+
local IndestructibleTag = "Indestructible"
|
|
13
|
+
local FallingGroupTag = "FallingGroup"
|
|
14
|
+
local DebrisTag = "ShatterboxDebris"
|
|
15
|
+
local DirtyGroupIDAttribute = "DirtyGroupID"
|
|
16
|
+
local FallingGroupIDAttribute = "FallingGroupID"
|
|
17
|
+
local IMAGINARY, NORMAL = "IMAGINARY", "NORMAL"
|
|
18
|
+
local EPSILON = 1e-3
|
|
19
|
+
|
|
20
|
+
-- Services
|
|
21
|
+
local HttpService = game:GetService("HttpService")
|
|
22
|
+
local CollectionService = game:GetService("CollectionService")
|
|
23
|
+
local RunService = game:GetService("RunService")
|
|
24
|
+
local Players = game:GetService("Players")
|
|
25
|
+
|
|
26
|
+
local isServer, isClient = RunService:IsServer(), RunService:IsClient()
|
|
27
|
+
|
|
28
|
+
-- Module references
|
|
29
|
+
local src = script
|
|
30
|
+
local lib = src:WaitForChild("lib")
|
|
31
|
+
local PathToClient = lib:WaitForChild("Client")
|
|
32
|
+
local PathToServer = lib:WaitForChild("Server")
|
|
33
|
+
|
|
34
|
+
local Effects = require(src:WaitForChild('Effects'))
|
|
35
|
+
local DefaultSettings = require(src:WaitForChild("Settings"))
|
|
36
|
+
|
|
37
|
+
-- Library imports
|
|
38
|
+
local VertexMath = require(lib:WaitForChild("VertexMath"))
|
|
39
|
+
local PartEncapsulatesBlockPart = VertexMath.PartEncapsulatesBlockPart
|
|
40
|
+
local GetVerts, GetNormals = VertexMath.GetVerts, VertexMath.GetNormals
|
|
41
|
+
local BlockVerts, BlockNormals = GetVerts.Block, GetNormals.Block
|
|
42
|
+
local PartContainsAVert, PartContainsAllVerts = VertexMath.PartContainsAVert, VertexMath.PartContainsAllVerts
|
|
43
|
+
|
|
44
|
+
local PartOperations = require(lib:WaitForChild("PartOperations"))
|
|
45
|
+
local PartFromTemplate = PartOperations.PartFromTemplate
|
|
46
|
+
local DeserializedPartInstance = PartOperations.DeserializedPartInstance
|
|
47
|
+
local SerializedPartInstance = PartOperations.SerializedPartInstance
|
|
48
|
+
local GridInfo = PartOperations.GridInfo
|
|
49
|
+
local ImaginaryVoxelize = PartOperations.ImaginaryVoxelize
|
|
50
|
+
local Voxelize = PartOperations.Voxelize
|
|
51
|
+
|
|
52
|
+
local SubdivideFunctions = {
|
|
53
|
+
KD = PartOperations.SubdivideKD,
|
|
54
|
+
MOT = PartOperations.SubdivideOctree
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
-- UV mapping tables
|
|
58
|
+
local textUVMap = {
|
|
59
|
+
[0] = Vector3.new( 0, 1, 1),
|
|
60
|
+
[1] = Vector3.new( 1, 0, 1),
|
|
61
|
+
[2] = Vector3.new(-1, 1, 0),
|
|
62
|
+
[3] = Vector3.new( 0, 1, -1),
|
|
63
|
+
[4] = Vector3.new(-1, 0, 1),
|
|
64
|
+
[5] = Vector3.new( 1, 1, 0)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
local axisUVMap = {
|
|
68
|
+
[0] = { "Z", "Y" },
|
|
69
|
+
[1] = { "X", "Z" },
|
|
70
|
+
[2] = { "X", "Y" },
|
|
71
|
+
[3] = { "Z", "Y" },
|
|
72
|
+
[4] = { "X", "Z" },
|
|
73
|
+
[5] = { "X", "Y" }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
-- Puppet replication constants
|
|
77
|
+
local U82RAD = 0.0246399423811
|
|
78
|
+
local PRECISE_45_DEGREES = {
|
|
79
|
+
[32] = math.pi * 0.25,
|
|
80
|
+
[64] = math.pi * 0.5,
|
|
81
|
+
[96] = math.pi * 0.75,
|
|
82
|
+
[128] = math.pi,
|
|
83
|
+
[159] = -math.pi * 0.75,
|
|
84
|
+
[191] = -math.pi * 0.5,
|
|
85
|
+
[223] = -math.pi * 0.25,
|
|
86
|
+
[255] = 0
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
local TAU = 2 * math.pi
|
|
90
|
+
local RAD2U8 = 40.5845104884
|
|
91
|
+
|
|
92
|
+
-- DFS search offsets
|
|
93
|
+
local DFSWeldSearchXSizeOffset = Vector3.new(1 + EPSILON, 1 - EPSILON, 1 - EPSILON)
|
|
94
|
+
local DFSWeldSearchYSizeOffset = Vector3.new(1 - EPSILON, 1 + EPSILON, 1 - EPSILON)
|
|
95
|
+
local DFSWeldSearchZSizeOffset = Vector3.new(1 - EPSILON, 1 - EPSILON, 1 + EPSILON)
|
|
96
|
+
|
|
97
|
+
-- Pre-allocated instances
|
|
98
|
+
local overlapParams = OverlapParams.new()
|
|
99
|
+
local raycastParams = RaycastParams.new()
|
|
100
|
+
raycastParams.FilterType = Enum.RaycastFilterType.Include
|
|
101
|
+
local IntersectingPart = Instance.new("Part")
|
|
102
|
+
|
|
103
|
+
-- Shatterbox Class
|
|
104
|
+
local Shatterbox = {}
|
|
105
|
+
Shatterbox.__index = Shatterbox
|
|
106
|
+
|
|
107
|
+
Shatterbox.VertexMath = VertexMath
|
|
108
|
+
Shatterbox.DefaultEffects = Effects
|
|
109
|
+
Shatterbox.Settings = DefaultSettings :: Settings
|
|
110
|
+
Shatterbox.USE_CLIENT_SERVER = DefaultSettings.USE_CLIENT_SERVER :: boolean
|
|
111
|
+
Shatterbox.started = false
|
|
112
|
+
|
|
113
|
+
--[[ CONSTRUCTOR ]]--
|
|
114
|
+
|
|
115
|
+
function Shatterbox.new (settings: { [string]: unknown })
|
|
116
|
+
local self = setmetatable({}, Shatterbox)
|
|
117
|
+
|
|
118
|
+
-- Settings
|
|
119
|
+
self.Settings = settings or DefaultSettings
|
|
120
|
+
self.USE_CLIENT_SERVER = self:UseSetting('USE_CLIENT_SERVER') :: boolean
|
|
121
|
+
|
|
122
|
+
-- State tracking
|
|
123
|
+
self.started = false
|
|
124
|
+
|
|
125
|
+
-- Network events
|
|
126
|
+
self.ClientEvents = nil
|
|
127
|
+
self.ServerEvents = nil
|
|
128
|
+
self.mapContainer = nil
|
|
129
|
+
|
|
130
|
+
-- Registry for voxel destruction callbacks
|
|
131
|
+
self.OnVoxelDestructRegistry = setmetatable({}, {
|
|
132
|
+
__index = function(tbl, k) return (k and rawget(tbl, k:lower())) end
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
-- Dirty group tracking
|
|
136
|
+
self.DirtyGroupData = {}
|
|
137
|
+
self.DirtyGroupMembers = {}
|
|
138
|
+
self.SmoothCleanupQueue = {}
|
|
139
|
+
|
|
140
|
+
-- Operation tracking
|
|
141
|
+
self.ShatterQueue = {}
|
|
142
|
+
self.CountOperationsProcessing = {}
|
|
143
|
+
self.OperationAffectedDirtyGroups = {}
|
|
144
|
+
self.OperationBlacklistedVoxels = {}
|
|
145
|
+
self.OperationAffectedFallingGroups = {}
|
|
146
|
+
|
|
147
|
+
-- Greedy meshing
|
|
148
|
+
self.GreedyMeshingQueue = {}
|
|
149
|
+
self.GreedyWorkers = {}
|
|
150
|
+
self.GreedyWorkersTerminationFlags = {}
|
|
151
|
+
self.ActiveGMWorkerCount = 0
|
|
152
|
+
self.ActiveGMWorkerGridSize = {}
|
|
153
|
+
|
|
154
|
+
-- Puppet system
|
|
155
|
+
self.Puppets = {}
|
|
156
|
+
self.PuppetsLerpData = {}
|
|
157
|
+
self.PuppetNextIndex = 1
|
|
158
|
+
self.PuppetNextReplicationIndex = 1
|
|
159
|
+
self.CreatePuppetQueue = {}
|
|
160
|
+
self.ClientPuppetStepDelta = 1
|
|
161
|
+
self.puppetTimeAccumulation = 0
|
|
162
|
+
self.FakePuppetOverrideIndex = 1
|
|
163
|
+
|
|
164
|
+
-- Falling groups
|
|
165
|
+
self.FallingGroupMembers = {}
|
|
166
|
+
self.FallingGroupSectionAnchors = {}
|
|
167
|
+
|
|
168
|
+
-- Frame tracking
|
|
169
|
+
self.frameDivs = 0
|
|
170
|
+
self.clientInitComplete = false
|
|
171
|
+
|
|
172
|
+
-- Debug counters
|
|
173
|
+
self.ci = 0
|
|
174
|
+
self.cd = 0
|
|
175
|
+
|
|
176
|
+
-- Object cache (currently unused)
|
|
177
|
+
self.ObjectCache = nil
|
|
178
|
+
self.CachedParts = nil
|
|
179
|
+
|
|
180
|
+
return self
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
--[[ UTILITY FUNCTIONS ]]--
|
|
184
|
+
|
|
185
|
+
function Shatterbox.FindHumanoidAsAncestorChild(self: Shatterbox, i: Instance)
|
|
186
|
+
while i do
|
|
187
|
+
local hum = i:FindFirstChildWhichIsA("Humanoid")
|
|
188
|
+
if hum then return hum end
|
|
189
|
+
i = i.Parent :: Instance
|
|
190
|
+
end
|
|
191
|
+
return nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
function Shatterbox.ShouldSkipInstance(self: Shatterbox, i: Instance)
|
|
195
|
+
local SkipInstanceCheck: (i: Instance) -> boolean = self:UseSetting('SkipInstanceCheck')
|
|
196
|
+
if SkipInstanceCheck(i) then return true end
|
|
197
|
+
if isClient and (not i:IsA("Part") or i.Shape ~= Enum.PartType.Block) then return true end
|
|
198
|
+
if i:HasTag(IndestructibleTag) then return true end
|
|
199
|
+
if self:FindHumanoidAsAncestorChild(i) then return true end
|
|
200
|
+
return false
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
function Shatterbox:FindFirstAncestorGroup(i: Instance): (Folder|Model)?
|
|
204
|
+
local Ancestor = i.Parent
|
|
205
|
+
while Ancestor and not (Ancestor:IsA("Folder") or Ancestor:IsA("Model")) do
|
|
206
|
+
Ancestor = Ancestor.Parent
|
|
207
|
+
end
|
|
208
|
+
return Ancestor
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
function Shatterbox:AncestorGroupTags(i: Instance): {string}
|
|
212
|
+
local tags = {}
|
|
213
|
+
local Ancestor = self:FindFirstAncestorGroup(i)
|
|
214
|
+
|
|
215
|
+
while Ancestor do
|
|
216
|
+
local t = Ancestor:GetTags()
|
|
217
|
+
local found = table.find(t, FallingGroupTag)
|
|
218
|
+
if found then table.remove(t, found) end
|
|
219
|
+
|
|
220
|
+
if #t > 0 then
|
|
221
|
+
table.move(t, 1, #t, #tags + 1, tags)
|
|
222
|
+
end
|
|
223
|
+
Ancestor = self:FindFirstAncestorGroup(Ancestor)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
return tags
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
function Shatterbox:FindFirstAncestorGroupWithTag(i: Instance, tag: string): Instance?
|
|
230
|
+
local Ancestor = self:FindFirstAncestorGroup(i)
|
|
231
|
+
while Ancestor and not Ancestor:HasTag(tag) do
|
|
232
|
+
Ancestor = self:FindFirstAncestorGroup(Ancestor)
|
|
233
|
+
end
|
|
234
|
+
return Ancestor
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
function Shatterbox:OverlapParamsOfTagged(tags: {string}): OverlapParams
|
|
238
|
+
local arr
|
|
239
|
+
|
|
240
|
+
if #tags == 1 then
|
|
241
|
+
arr = CollectionService:GetTagged(tags[1])
|
|
242
|
+
else
|
|
243
|
+
arr = {}
|
|
244
|
+
for _, tag in ipairs(tags) do
|
|
245
|
+
local taggedArray = CollectionService:GetTagged(tag)
|
|
246
|
+
table.move(taggedArray, 1, #taggedArray, #arr + 1, arr)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
overlapParams.FilterType = Enum.RaycastFilterType.Include
|
|
251
|
+
overlapParams.FilterDescendantsInstances = arr
|
|
252
|
+
|
|
253
|
+
return overlapParams
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
--[[ PART MANAGEMENT ]]--
|
|
257
|
+
|
|
258
|
+
function Shatterbox:InstantiatePart(DirtyGroupID, cframe, size)
|
|
259
|
+
local dirtyGroupData = self.DirtyGroupData[DirtyGroupID]
|
|
260
|
+
local dirtyGroupMembers = self.DirtyGroupMembers[DirtyGroupID]
|
|
261
|
+
|
|
262
|
+
local part = PartFromTemplate(dirtyGroupData.OriginalPart, cframe, self.CachedParts)
|
|
263
|
+
part.Size = size
|
|
264
|
+
|
|
265
|
+
local partIsDivisible = part:IsA("Part") and part.Shape == Enum.PartType.Block
|
|
266
|
+
|
|
267
|
+
if not self.USE_CLIENT_SERVER or not partIsDivisible then
|
|
268
|
+
part.Parent = dirtyGroupData.OriginalParent
|
|
269
|
+
else
|
|
270
|
+
part.Parent = self.mapContainer
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
if partIsDivisible and part:FindFirstChildWhichIsA("Texture") then
|
|
274
|
+
local OriginalPart = dirtyGroupData.OriginalPart
|
|
275
|
+
local HalfSizeOriginal = OriginalPart.Size * 0.5
|
|
276
|
+
local HalfSizePart = part.Size * 0.5
|
|
277
|
+
|
|
278
|
+
for _, texture in ipairs(part:GetChildren()) do
|
|
279
|
+
if not texture:IsA("Texture") then continue end
|
|
280
|
+
local Face = texture.Face.Value
|
|
281
|
+
local TowardOrigin = textUVMap[Face]
|
|
282
|
+
|
|
283
|
+
local GroupOrigin = OriginalPart.CFrame + OriginalPart.CFrame:VectorToWorldSpace(TowardOrigin * HalfSizeOriginal)
|
|
284
|
+
local partOrigin = part.CFrame + part.CFrame:VectorToWorldSpace(TowardOrigin * HalfSizePart)
|
|
285
|
+
|
|
286
|
+
local Offset = partOrigin:VectorToObjectSpace(GroupOrigin.Position - partOrigin.Position):Abs()
|
|
287
|
+
|
|
288
|
+
texture.OffsetStudsU += Offset[axisUVMap[Face][1]]
|
|
289
|
+
texture.OffsetStudsV += Offset[axisUVMap[Face][2]]
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
dirtyGroupMembers[part] = true
|
|
294
|
+
self.ci += 1
|
|
295
|
+
|
|
296
|
+
return part
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
function Shatterbox:DestroyPart(part)
|
|
300
|
+
local DirtyGroupID = part:GetAttribute(DirtyGroupIDAttribute)
|
|
301
|
+
if not DirtyGroupID then return end
|
|
302
|
+
|
|
303
|
+
local dirtyGroupData = self.DirtyGroupData[DirtyGroupID]
|
|
304
|
+
local dirtyGroupMembers = self.DirtyGroupMembers[DirtyGroupID]
|
|
305
|
+
|
|
306
|
+
if part == dirtyGroupData.OriginalPart then
|
|
307
|
+
part.Parent = nil
|
|
308
|
+
else
|
|
309
|
+
dirtyGroupMembers[part] = nil
|
|
310
|
+
part:Destroy()
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
self.cd += 1
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
function Shatterbox:PopulateDirtyGroup(Part: Part, DirtyGroupID: string?)
|
|
317
|
+
DirtyGroupID = DirtyGroupID or HttpService:GenerateGUID(false)
|
|
318
|
+
Part:SetAttribute(DirtyGroupIDAttribute, DirtyGroupID)
|
|
319
|
+
|
|
320
|
+
self.DirtyGroupData[DirtyGroupID] = {
|
|
321
|
+
OriginalPart = Part,
|
|
322
|
+
OriginalParent = Part.Parent,
|
|
323
|
+
GridSize = Part:GetAttribute("GridSize")
|
|
324
|
+
}
|
|
325
|
+
self.DirtyGroupMembers[DirtyGroupID] = {}
|
|
326
|
+
|
|
327
|
+
if self.USE_CLIENT_SERVER and Part:IsA("Part") and Part.Shape == Enum.PartType.Block then
|
|
328
|
+
for _, tag in ipairs(self:AncestorGroupTags(Part)) do
|
|
329
|
+
Part:AddTag(tag)
|
|
330
|
+
end
|
|
331
|
+
Part.Parent = self.mapContainer
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
return DirtyGroupID
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
function Shatterbox.UndoDirtyGroup(self: Shatterbox, DirtyGroupID: string)
|
|
338
|
+
local groupData = self.DirtyGroupData[DirtyGroupID]
|
|
339
|
+
local groupMembers = self.DirtyGroupMembers[DirtyGroupID]
|
|
340
|
+
|
|
341
|
+
local useGreedyMeshing = self:UseSetting('UseGreedyMeshing')
|
|
342
|
+
|
|
343
|
+
if useGreedyMeshing and self.GreedyWorkers[DirtyGroupID] then
|
|
344
|
+
self.GreedyWorkersTerminationFlags[DirtyGroupID] = true
|
|
345
|
+
repeat RunService.Heartbeat:Wait() until not self.GreedyWorkers[DirtyGroupID]
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
local OriginalPart = groupData.OriginalPart
|
|
349
|
+
if not self.USE_CLIENT_SERVER or not OriginalPart:IsA("Part") or OriginalPart.Shape ~= Enum.PartType.Block then
|
|
350
|
+
OriginalPart.Parent = groupData.OriginalParent
|
|
351
|
+
else
|
|
352
|
+
OriginalPart.Parent = self.mapContainer
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
for member in pairs(groupMembers) do
|
|
356
|
+
self:DestroyPart(member)
|
|
357
|
+
end
|
|
358
|
+
self.DirtyGroupMembers[DirtyGroupID] = {}
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
--[[ GREEDY MESHING ]]--
|
|
362
|
+
|
|
363
|
+
function Shatterbox.QueueGreedyMeshing(self: Shatterbox, DirtyGroupID: string, GridSize: number)
|
|
364
|
+
if self.GreedyWorkers[DirtyGroupID] then
|
|
365
|
+
self.GreedyWorkersTerminationFlags[DirtyGroupID] = true
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
local previousGridSize = self.GreedyMeshingQueue[DirtyGroupID]
|
|
369
|
+
self.GreedyMeshingQueue[DirtyGroupID] = previousGridSize and math.min(GridSize, previousGridSize) or GridSize
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
function Shatterbox.InitGreedyState(self: Shatterbox, part, GridSize, groupMin, State, DIMYDIMZ, DIMZ)
|
|
373
|
+
local minCFrame, _, S, _, _, _, NVX, NVY, NVZ = GridInfo(part.CFrame:Orthonormalize(), part.Size, GridSize)
|
|
374
|
+
|
|
375
|
+
if not minCFrame.Rotation:FuzzyEq(groupMin.Rotation, EPSILON) then
|
|
376
|
+
return 0
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
local off = minCFrame.Position - groupMin.Position
|
|
380
|
+
|
|
381
|
+
local ODX = off:Dot(groupMin.XVector) / S.X
|
|
382
|
+
local ODY = off:Dot(groupMin.YVector) / S.Y
|
|
383
|
+
local ODZ = off:Dot(groupMin.ZVector) / S.Z
|
|
384
|
+
|
|
385
|
+
local DX = math.floor(ODX + 0.5)
|
|
386
|
+
local DY = math.floor(ODY + 0.5)
|
|
387
|
+
local DZ = math.floor(ODZ + 0.5)
|
|
388
|
+
|
|
389
|
+
if math.abs(DX - ODX) > EPSILON or math.abs(DY - ODY) > EPSILON or math.abs(DZ - ODZ) > EPSILON then
|
|
390
|
+
return 0
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
DX -= 1
|
|
394
|
+
DY -= 1
|
|
395
|
+
|
|
396
|
+
local traversed = 0
|
|
397
|
+
for X = 1, NVX do
|
|
398
|
+
local dixz = (X + DX) * DIMYDIMZ + DZ
|
|
399
|
+
for Y = 1, NVY do
|
|
400
|
+
local di = dixz + (Y + DY) * DIMZ
|
|
401
|
+
for Z = 1, NVZ do
|
|
402
|
+
if traversed == self:UseSetting('GMTraversalsPerFrame') then
|
|
403
|
+
coroutine.yield()
|
|
404
|
+
traversed = 0
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
State[di + Z] = true
|
|
408
|
+
traversed += 1
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
DX += 1
|
|
414
|
+
DY += 1
|
|
415
|
+
|
|
416
|
+
return {1 + DX, 1 + DY, 1 + DZ, DX + NVX, DY + NVY, DZ + NVZ}, NVX * NVY * NVZ
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
function Shatterbox:GreedyMeshWorker(DirtyGroupID: string, GridSize: number?)
|
|
420
|
+
local dirtyGroupData = self.DirtyGroupData[DirtyGroupID]
|
|
421
|
+
local dirtyGroupMembers = self.DirtyGroupMembers[DirtyGroupID]
|
|
422
|
+
local Original = dirtyGroupData.OriginalPart
|
|
423
|
+
|
|
424
|
+
local groupMin, _, VoxelSize, _, _, _, DIMX, DIMY, DIMZ = GridInfo(Original.CFrame, Original.Size, GridSize)
|
|
425
|
+
local DIMYDIMZ = DIMY * DIMZ
|
|
426
|
+
local VSX, VSY, VSZ = VoxelSize.X, VoxelSize.Y, VoxelSize.Z
|
|
427
|
+
|
|
428
|
+
local State, Visited = {}, {}
|
|
429
|
+
local OriginalPartsInfo = {}
|
|
430
|
+
local count = 0
|
|
431
|
+
|
|
432
|
+
for member in pairs(dirtyGroupMembers) do
|
|
433
|
+
if member:HasTag(DebrisTag) then continue end
|
|
434
|
+
|
|
435
|
+
local gridInfo, n = self:InitGreedyState(member, GridSize, groupMin, State, DIMYDIMZ, DIMZ)
|
|
436
|
+
if n > 0 then
|
|
437
|
+
count += n
|
|
438
|
+
table.insert(OriginalPartsInfo, {
|
|
439
|
+
Part = member,
|
|
440
|
+
PartGridInfo = gridInfo,
|
|
441
|
+
Contributed = {}
|
|
442
|
+
})
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
local meshed = {}
|
|
447
|
+
local traversed = 0
|
|
448
|
+
local sx, sy, sz = 1, 1, 1
|
|
449
|
+
|
|
450
|
+
-- Greedy meshing algorithm
|
|
451
|
+
while count > 0 do
|
|
452
|
+
local X, Y, Z = sx, sy, sz
|
|
453
|
+
local iyz = (Y - 1) * DIMZ + Z
|
|
454
|
+
local si = (X - 1) * DIMYDIMZ + iyz
|
|
455
|
+
|
|
456
|
+
while Visited[si] or not State[si] do
|
|
457
|
+
if traversed == self:UseSetting('GMTraversalsPerFrame') then
|
|
458
|
+
coroutine.yield()
|
|
459
|
+
traversed = 0
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
if X < DIMX then
|
|
463
|
+
X += 1
|
|
464
|
+
else
|
|
465
|
+
X = 1
|
|
466
|
+
if Y < DIMY then
|
|
467
|
+
Y += 1
|
|
468
|
+
else
|
|
469
|
+
Y = 1
|
|
470
|
+
if Z < DIMZ then
|
|
471
|
+
Z += 1
|
|
472
|
+
else
|
|
473
|
+
Z = 1
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
iyz = (Y - 1) * DIMZ + Z
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
traversed += 1
|
|
480
|
+
si = (X - 1) * DIMYDIMZ + iyz
|
|
481
|
+
end
|
|
482
|
+
sx, sy, sz = X, Y, Z
|
|
483
|
+
Visited[si] = true
|
|
484
|
+
|
|
485
|
+
-- Planar expansion along X axis
|
|
486
|
+
while X < DIMX do
|
|
487
|
+
local DX = X + 1
|
|
488
|
+
if traversed == self:UseSetting('GMTraversalsPerFrame') then
|
|
489
|
+
coroutine.yield()
|
|
490
|
+
traversed = 0
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
local index = (DX - 1) * DIMYDIMZ + (Y - 1) * DIMZ + Z
|
|
494
|
+
if Visited[index] or not State[index] then break end
|
|
495
|
+
|
|
496
|
+
Visited[index] = true
|
|
497
|
+
traversed += 1
|
|
498
|
+
X = DX
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
-- Planar expansion along Y axis
|
|
502
|
+
while Y < DIMY do
|
|
503
|
+
local DY = Y + 1
|
|
504
|
+
local iyz = (DY - 1) * DIMZ + Z
|
|
505
|
+
local breakExpansion = false
|
|
506
|
+
local indices = {}
|
|
507
|
+
|
|
508
|
+
for ix = sx, X do
|
|
509
|
+
if traversed == self:UseSetting('GMTraversalsPerFrame') then
|
|
510
|
+
coroutine.yield()
|
|
511
|
+
traversed = 0
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
local index = (ix - 1) * DIMYDIMZ + iyz
|
|
515
|
+
if Visited[index] or not State[index] then
|
|
516
|
+
breakExpansion = true
|
|
517
|
+
break
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
indices[index] = true
|
|
521
|
+
traversed += 1
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
if breakExpansion then break end
|
|
525
|
+
|
|
526
|
+
for index in pairs(indices) do
|
|
527
|
+
Visited[index] = true
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
Y = DY
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
-- Planar expansion along Z axis
|
|
534
|
+
while Z < DIMZ do
|
|
535
|
+
local DZ = Z + 1
|
|
536
|
+
local breakExpansion = false
|
|
537
|
+
local indices = {}
|
|
538
|
+
|
|
539
|
+
for ix = sx, X do
|
|
540
|
+
local ixz = (ix - 1) * DIMYDIMZ + DZ
|
|
541
|
+
for iy = sy, Y do
|
|
542
|
+
if traversed == self:UseSetting('GMTraversalsPerFrame') then
|
|
543
|
+
coroutine.yield()
|
|
544
|
+
traversed = 0
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
local index = ixz + (iy - 1) * DIMZ
|
|
548
|
+
if Visited[index] or not State[index] then
|
|
549
|
+
breakExpansion = true
|
|
550
|
+
break
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
indices[index] = true
|
|
554
|
+
traversed += 1
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
if breakExpansion then break end
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
if breakExpansion then break end
|
|
561
|
+
|
|
562
|
+
for index in pairs(indices) do
|
|
563
|
+
Visited[index] = true
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
Z = DZ
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
local NX, NY, NZ = X - sx + 1, Y - sy + 1, Z - sz + 1
|
|
570
|
+
count -= NX * NY * NZ
|
|
571
|
+
|
|
572
|
+
table.insert(meshed, {
|
|
573
|
+
CFrame = (groupMin + groupMin.XVector * VSX * ((X + sx) * 0.5 - 1) + groupMin.YVector * VSY * ((Y + sy) * 0.5 - 1) + groupMin.ZVector * VSZ * ((Z + sz) * 0.5 - 1)):Orthonormalize(),
|
|
574
|
+
Size = VoxelSize * Vector3.new(NX, NY, NZ),
|
|
575
|
+
Contributed = {},
|
|
576
|
+
MINX = sx,
|
|
577
|
+
MINY = sy,
|
|
578
|
+
MINZ = sz,
|
|
579
|
+
MAXX = X,
|
|
580
|
+
MAXY = Y,
|
|
581
|
+
MAXZ = Z
|
|
582
|
+
})
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
-- Process voxel count contributions
|
|
586
|
+
for meshedIndex, MI in ipairs(meshed) do
|
|
587
|
+
local contributedToMeshed = MI.Contributed
|
|
588
|
+
local MINX, MINY, MINZ = MI.MINX, MI.MINY, MI.MINZ
|
|
589
|
+
local MAXX, MAXY, MAXZ = MI.MAXX, MI.MAXY, MI.MAXZ
|
|
590
|
+
|
|
591
|
+
for i, partInfo in ipairs(OriginalPartsInfo) do
|
|
592
|
+
local PMINX, PMINY, PMINZ, PMAXX, PMAXY, PMAXZ = unpack(partInfo.PartGridInfo)
|
|
593
|
+
|
|
594
|
+
if math.max(PMINX, MINX) > math.min(PMAXX, MAXX) or
|
|
595
|
+
math.max(PMINY, MINY) > math.min(PMAXY, MAXY) or
|
|
596
|
+
math.max(PMINZ, MINZ) > math.min(PMAXZ, MAXZ) then
|
|
597
|
+
continue
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
table.insert(contributedToMeshed, i)
|
|
601
|
+
table.insert(partInfo.Contributed, meshedIndex)
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
local InstancedMeshIDs, numInstanced, numMeshed = {}, 0, #meshed
|
|
606
|
+
count = 0
|
|
607
|
+
|
|
608
|
+
-- Dual DFS for smooth display of meshing
|
|
609
|
+
while numInstanced < numMeshed do
|
|
610
|
+
if count >= self:UseSetting('GMPartCreationsPerFrame') then
|
|
611
|
+
coroutine.yield()
|
|
612
|
+
count = 0
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
local startIndex
|
|
616
|
+
for i, meshedInfo in ipairs(meshed) do
|
|
617
|
+
if InstancedMeshIDs[i] then continue end
|
|
618
|
+
if #meshedInfo.Contributed == 1 then
|
|
619
|
+
InstancedMeshIDs[i] = true
|
|
620
|
+
numInstanced += 1
|
|
621
|
+
continue
|
|
622
|
+
end
|
|
623
|
+
startIndex = i
|
|
624
|
+
break
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
local visitedOriginals, visitedMeshed, pathStack = {}, {[startIndex] = true}, {startIndex}
|
|
628
|
+
|
|
629
|
+
while #pathStack > 0 do
|
|
630
|
+
local mi = table.remove(pathStack)
|
|
631
|
+
local meshedInfo = meshed[mi]
|
|
632
|
+
|
|
633
|
+
for _, io in ipairs(meshedInfo.Contributed) do
|
|
634
|
+
if visitedOriginals[io] then continue end
|
|
635
|
+
visitedOriginals[io] = true
|
|
636
|
+
|
|
637
|
+
local partInfo = OriginalPartsInfo[io]
|
|
638
|
+
|
|
639
|
+
for _, im in ipairs(partInfo.Contributed) do
|
|
640
|
+
if visitedMeshed[im] then continue end
|
|
641
|
+
visitedMeshed[im] = true
|
|
642
|
+
table.insert(pathStack, im)
|
|
643
|
+
end
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
for meshedID in pairs(visitedMeshed) do
|
|
648
|
+
InstancedMeshIDs[meshedID] = true
|
|
649
|
+
numInstanced += 1
|
|
650
|
+
|
|
651
|
+
local meshedInfo = meshed[meshedID]
|
|
652
|
+
self:InstantiatePart(DirtyGroupID, meshedInfo.CFrame, meshedInfo.Size)
|
|
653
|
+
|
|
654
|
+
count += 1
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
for originalID in pairs(visitedOriginals) do
|
|
658
|
+
self:DestroyPart(OriginalPartsInfo[originalID].Part)
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
function Shatterbox:GreedyMeshingBehavior()
|
|
664
|
+
if self.ActiveGMWorkerCount < self:UseSetting('GMWorkerCount') then
|
|
665
|
+
local toRemove = {}
|
|
666
|
+
for DirtyGroupID, GridSize in pairs(self.GreedyMeshingQueue) do
|
|
667
|
+
if self.CountOperationsProcessing[DirtyGroupID] or self.GreedyWorkers[DirtyGroupID] then continue end
|
|
668
|
+
if not next(self.DirtyGroupMembers[DirtyGroupID]) then
|
|
669
|
+
toRemove[DirtyGroupID] = true
|
|
670
|
+
continue
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
local thr = coroutine.create(function()
|
|
674
|
+
self:GreedyMeshWorker(DirtyGroupID, GridSize)
|
|
675
|
+
end)
|
|
676
|
+
|
|
677
|
+
toRemove[DirtyGroupID] = true
|
|
678
|
+
self.GreedyWorkers[DirtyGroupID] = thr
|
|
679
|
+
self.ActiveGMWorkerCount += 1
|
|
680
|
+
self.ActiveGMWorkerGridSize[DirtyGroupID] = GridSize
|
|
681
|
+
|
|
682
|
+
coroutine.resume(thr)
|
|
683
|
+
|
|
684
|
+
if self.ActiveGMWorkerCount >= self:UseSetting('GMWorkerCount') then break end
|
|
685
|
+
end
|
|
686
|
+
for DirtyGroupID in pairs(toRemove) do
|
|
687
|
+
self.GreedyMeshingQueue[DirtyGroupID] = nil
|
|
688
|
+
end
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
for DirtyGroupID, thr in pairs(self.GreedyWorkers) do
|
|
692
|
+
local status = coroutine.status(thr)
|
|
693
|
+
|
|
694
|
+
if status == "dead" or (status == "suspended" and self.GreedyWorkersTerminationFlags[DirtyGroupID]) then
|
|
695
|
+
self.GreedyWorkersTerminationFlags[DirtyGroupID] = nil
|
|
696
|
+
self.GreedyWorkers[DirtyGroupID] = nil
|
|
697
|
+
self.ActiveGMWorkerCount -= 1
|
|
698
|
+
self.ActiveGMWorkerGridSize[DirtyGroupID] = nil
|
|
699
|
+
elseif status == "suspended" then
|
|
700
|
+
coroutine.resume(thr)
|
|
701
|
+
end
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
--[[ MARKING AND DESTRUCTION ]]--
|
|
706
|
+
|
|
707
|
+
function Shatterbox:MarkDirty(Part: Part, destructionParams)
|
|
708
|
+
local CreatedDirtyGroups = {}
|
|
709
|
+
|
|
710
|
+
if Part:HasTag(DebrisTag) then return CreatedDirtyGroups end
|
|
711
|
+
|
|
712
|
+
local VisitedDirtyGroups = {}
|
|
713
|
+
local GridSize = Part:GetAttribute("GridSize")
|
|
714
|
+
local PartDirtyGroupID = Part:GetAttribute(DirtyGroupIDAttribute)
|
|
715
|
+
|
|
716
|
+
local CreatedFallingGroupID
|
|
717
|
+
local fallingGroupContainer = self:FindFirstAncestorGroupWithTag(Part, FallingGroupTag)
|
|
718
|
+
|
|
719
|
+
local PartIsDivisible = Part:IsA("Part") and Part.Shape == Enum.PartType.Block
|
|
720
|
+
local NonDivisibleInteraction = self:UseSetting('NonDivisibleInteraction')
|
|
721
|
+
|
|
722
|
+
if not Part:GetAttribute(FallingGroupIDAttribute) and fallingGroupContainer then
|
|
723
|
+
CreatedFallingGroupID = HttpService:GenerateGUID(false)
|
|
724
|
+
local fallingGroupMembers = {}
|
|
725
|
+
|
|
726
|
+
for _, part in ipairs(fallingGroupContainer:GetDescendants()) do
|
|
727
|
+
if not part:IsA("BasePart") then continue end
|
|
728
|
+
|
|
729
|
+
part:SetAttribute(FallingGroupIDAttribute, CreatedFallingGroupID)
|
|
730
|
+
local DirtyGroupID = self:PopulateDirtyGroup(part, nil)
|
|
731
|
+
VisitedDirtyGroups[DirtyGroupID] = true
|
|
732
|
+
|
|
733
|
+
if part:IsA("Part") and part.Shape == Enum.PartType.Block then
|
|
734
|
+
CreatedDirtyGroups[DirtyGroupID] = true
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
part.Parent = nil
|
|
738
|
+
self:InstantiatePart(DirtyGroupID, part.CFrame, part.Size)
|
|
739
|
+
|
|
740
|
+
fallingGroupMembers[DirtyGroupID] = true
|
|
741
|
+
end
|
|
742
|
+
self.FallingGroupMembers[CreatedFallingGroupID] = fallingGroupMembers
|
|
743
|
+
|
|
744
|
+
elseif not PartDirtyGroupID and (PartIsDivisible or NonDivisibleInteraction ~= "NONE") then
|
|
745
|
+
PartDirtyGroupID = self:PopulateDirtyGroup(Part, nil)
|
|
746
|
+
|
|
747
|
+
if PartIsDivisible then
|
|
748
|
+
CreatedDirtyGroups[PartDirtyGroupID] = true
|
|
749
|
+
end
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
if PartDirtyGroupID then
|
|
753
|
+
VisitedDirtyGroups[PartDirtyGroupID] = true
|
|
754
|
+
if self:UseSetting('UseGreedyMeshing') and not Part:GetAttribute(FallingGroupIDAttribute) and Part:IsA("Part") and Part.Shape == Enum.PartType.Block then
|
|
755
|
+
self:QueueGreedyMeshing(PartDirtyGroupID, GridSize or destructionParams.GridSize or self:UseSetting('DefaultGridSize'))
|
|
756
|
+
end
|
|
757
|
+
if not PartIsDivisible and NonDivisibleInteraction ~= "NONE" and not next(self.DirtyGroupMembers[PartDirtyGroupID]) then
|
|
758
|
+
Part.Parent = nil
|
|
759
|
+
if NonDivisibleInteraction == "FALL" then
|
|
760
|
+
local cloned = self:InstantiatePart(PartDirtyGroupID, Part.CFrame, Part.Size)
|
|
761
|
+
cloned.Anchored = false
|
|
762
|
+
end
|
|
763
|
+
end
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
local CleanupDelay = destructionParams.CleanupDelay
|
|
767
|
+
if self:UseSetting('UseSmoothCleanup') and CleanupDelay > 0 then
|
|
768
|
+
for DirtyGroupID in pairs(VisitedDirtyGroups) do
|
|
769
|
+
local prev = self.SmoothCleanupQueue[DirtyGroupID]
|
|
770
|
+
self.SmoothCleanupQueue[DirtyGroupID] = prev and math.max(prev, CleanupDelay) or CleanupDelay
|
|
771
|
+
end
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
return CreatedDirtyGroups, CreatedFallingGroupID
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
function Shatterbox:GetDestructibleParts(destructionParams)
|
|
778
|
+
local cuttingPart, replicated = destructionParams.CuttingPart, destructionParams.replicated
|
|
779
|
+
local skipFloors, skipWalls = destructionParams.SkipFloors, destructionParams.SkipWalls
|
|
780
|
+
local skipFloorsOrWalls = skipFloors or skipWalls
|
|
781
|
+
|
|
782
|
+
local partsInPart, createdDirtyGroups, createdFallingGroups = {}, {}, {}
|
|
783
|
+
|
|
784
|
+
IntersectingPart.CFrame = cuttingPart.CFrame
|
|
785
|
+
IntersectingPart.Size = cuttingPart.Size
|
|
786
|
+
IntersectingPart.Shape = cuttingPart.Shape
|
|
787
|
+
|
|
788
|
+
local AffectedFallingGroups = self.OperationAffectedFallingGroups[destructionParams.ID] or {}
|
|
789
|
+
|
|
790
|
+
for _, part in ipairs(workspace:GetPartsInPart(IntersectingPart, destructionParams.FilterTagged and self:OverlapParamsOfTagged(destructionParams.FilterTagged))) do
|
|
791
|
+
if (replicated and not part:IsDescendantOf(self.mapContainer)) or self:ShouldSkipInstance(part) then continue end
|
|
792
|
+
|
|
793
|
+
if skipFloorsOrWalls then
|
|
794
|
+
raycastParams.FilterDescendantsInstances = {part}
|
|
795
|
+
local res = workspace:Raycast(cuttingPart.CFrame.Position, part.Position - cuttingPart.CFrame.Position, raycastParams)
|
|
796
|
+
if res then
|
|
797
|
+
local isFloor = res.Normal:FuzzyEq(Vector3.yAxis, 0.001)
|
|
798
|
+
if (skipFloors and isFloor) or (skipWalls and not isFloor) then continue end
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
local dirtyGroups, fallingGroupID = self:MarkDirty(part, destructionParams)
|
|
803
|
+
for DirtyGroupID in pairs(dirtyGroups) do
|
|
804
|
+
createdDirtyGroups[DirtyGroupID] = true
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
if fallingGroupID then
|
|
808
|
+
createdFallingGroups[fallingGroupID] = true
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
fallingGroupID = part:GetAttribute(FallingGroupIDAttribute)
|
|
812
|
+
if fallingGroupID then
|
|
813
|
+
AffectedFallingGroups[fallingGroupID] = true
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
if part:IsA("Part") and part.Shape == Enum.PartType.Block then
|
|
817
|
+
table.insert(partsInPart, part)
|
|
818
|
+
end
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
if self.USE_CLIENT_SERVER and not replicated and isClient then
|
|
822
|
+
local ToSendToServer = {}
|
|
823
|
+
for DirtyGroupID in pairs(createdDirtyGroups) do
|
|
824
|
+
local groupData = self.DirtyGroupData[DirtyGroupID]
|
|
825
|
+
local PartToSend = groupData.OriginalPart
|
|
826
|
+
ToSendToServer[DirtyGroupID] = PartToSend
|
|
827
|
+
PartToSend.Parent = nil
|
|
828
|
+
groupData.OriginalPart = PartFromTemplate(PartToSend, PartToSend.CFrame)
|
|
829
|
+
groupData.OriginalPart.Parent = self.mapContainer
|
|
830
|
+
end
|
|
831
|
+
self.ClientEvents.CreateDirtyGroupsFromClient.Fire(ToSendToServer)
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
self.OperationAffectedFallingGroups[destructionParams.ID] = AffectedFallingGroups
|
|
835
|
+
|
|
836
|
+
return partsInPart, createdDirtyGroups, createdFallingGroups
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
--[[ VALIDATION AND SERIALIZATION ]]--
|
|
840
|
+
|
|
841
|
+
function Shatterbox:SerializedCuttingPart(p)
|
|
842
|
+
return {
|
|
843
|
+
CFrame = p.CFrame,
|
|
844
|
+
Size = p.Size,
|
|
845
|
+
Shape = p.Shape.Value
|
|
846
|
+
}
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
function Shatterbox.ValidateDestructionParams(self: Shatterbox, intersectingPart, FilterTagged, CleanupDelay, OnVoxelDestruct, GridSize, SkipEncapsulatedVoxels, OnDestructCompleted, UserData, ExcludePlayersReplication, SkipFloors, SkipWalls)
|
|
850
|
+
local CuttingPart = {}
|
|
851
|
+
local params = {}
|
|
852
|
+
local tbl = {} :: { [string]: unknown }
|
|
853
|
+
|
|
854
|
+
if typeof(intersectingPart) == "table" then
|
|
855
|
+
tbl = intersectingPart
|
|
856
|
+
if tbl.CuttingPart then
|
|
857
|
+
local cp = tbl.CuttingPart
|
|
858
|
+
if typeof(cp) == "table" or (typeof(cp) == "Instance" and cp:IsA("Part")) then
|
|
859
|
+
assert(cp.CFrame and cp.Size and cp.Shape, "You must provide a CFrame, Size, and Shape for the '{ CuttingPart : { CFrame : CFrame, Size : Vector3, Shape : Enum.PartType } }' syntax.")
|
|
860
|
+
CuttingPart.CFrame = cp.CFrame
|
|
861
|
+
CuttingPart.Size = cp.Size
|
|
862
|
+
CuttingPart.Shape = cp.Shape
|
|
863
|
+
end
|
|
864
|
+
else
|
|
865
|
+
assert(tbl.CFrame and tbl.Size and tbl.Shape, "You must provide a CFrame, Size, and Shape for the '{ CFrame : CFrame, Size : Vector3, Shape : Enum.PartType }' syntax.")
|
|
866
|
+
CuttingPart.CFrame = tbl.CFrame
|
|
867
|
+
CuttingPart.Size = tbl.Size
|
|
868
|
+
CuttingPart.Shape = tbl.Shape
|
|
869
|
+
end
|
|
870
|
+
elseif intersectingPart:IsA("Part") then
|
|
871
|
+
CuttingPart.CFrame = intersectingPart.CFrame
|
|
872
|
+
CuttingPart.Size = intersectingPart.Size
|
|
873
|
+
CuttingPart.Shape = intersectingPart.Shape
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
local filterTagged = tbl.FilterTagged or FilterTagged
|
|
877
|
+
if filterTagged then
|
|
878
|
+
if typeof(filterTagged) == "table" then
|
|
879
|
+
for _, k in pairs(filterTagged) do
|
|
880
|
+
assert(typeof(k) == "string", "FilterTagged must be an array of strings only")
|
|
881
|
+
end
|
|
882
|
+
else
|
|
883
|
+
assert(typeof(filterTagged) == "string", "FilterTagged must be a string or an array of strings")
|
|
884
|
+
filterTagged = {filterTagged}
|
|
885
|
+
end
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
local playerExclusions = tbl.ExcludePlayersReplication or ExcludePlayersReplication
|
|
889
|
+
if playerExclusions then
|
|
890
|
+
if typeof(playerExclusions) == "table" then
|
|
891
|
+
for _, plr in ipairs(playerExclusions) do
|
|
892
|
+
assert(typeof(plr) == "Instance" and plr:IsA("Player"), "If ExcludePlayersReplication is an array, it must only contain Players.")
|
|
893
|
+
end
|
|
894
|
+
else
|
|
895
|
+
assert(typeof(playerExclusions) == "Instance" and playerExclusions:IsA("Player"), "If ExcludePlayersReplication is not an array, it must be a Player.")
|
|
896
|
+
playerExclusions = {playerExclusions}
|
|
897
|
+
end
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
params.CuttingPart = CuttingPart
|
|
901
|
+
params.FilterTagged = filterTagged
|
|
902
|
+
params.CleanupDelay = tbl.CleanupDelay or CleanupDelay or self:UseSetting('DefaultSmoothCleanupDelay')
|
|
903
|
+
params.OnVoxelDestruct = tbl.OnVoxelDestruct or OnVoxelDestruct
|
|
904
|
+
params.GridSize = tbl.GridSize or GridSize
|
|
905
|
+
params.SkipEncapsulatedVoxels = tbl.SkipEncapsulatedVoxels or SkipEncapsulatedVoxels
|
|
906
|
+
params.SkipFloors = tbl.SkipFloors or SkipFloors
|
|
907
|
+
params.SkipWalls = tbl.SkipWalls or SkipWalls
|
|
908
|
+
params.OnDestructCompleted = tbl.OnDestructCompleted or OnDestructCompleted
|
|
909
|
+
params.UserData = tbl.UserData or UserData or {}
|
|
910
|
+
params.ExcludePlayersReplication = playerExclusions or {}
|
|
911
|
+
params.ID = tbl.ID or HttpService:GenerateGUID(false)
|
|
912
|
+
|
|
913
|
+
assert(not params.CleanupDelay or typeof(params.CleanupDelay) == "number", "CleanupDelay must be a number if it is set.")
|
|
914
|
+
assert(not params.OnVoxelDestruct or typeof(params.OnVoxelDestruct) == "string", "OnVoxelDestruct must be a string indicating the name of the OnVoxelDestruct callback.")
|
|
915
|
+
assert(not params.GridSize or typeof(params.GridSize) == "number", "GridSize must be a number.")
|
|
916
|
+
assert(not params.SkipEncapsulatedVoxels or typeof(params.SkipEncapsulatedVoxels) == "boolean", "SkipEncapsulatedVoxels must be a boolean.")
|
|
917
|
+
assert(not params.OnDestructCompleted or typeof(params.OnDestructCompleted) == "function", "OnDestructCompleted must be a function.")
|
|
918
|
+
|
|
919
|
+
return params
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
function Shatterbox:SerializedDestructionParams(destructionParams)
|
|
923
|
+
destructionParams = table.clone(destructionParams)
|
|
924
|
+
destructionParams.CuttingPart = self:SerializedCuttingPart(destructionParams.CuttingPart)
|
|
925
|
+
return destructionParams
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
function Shatterbox:DeserializedDestructionParams(serialDestructionParams)
|
|
929
|
+
serialDestructionParams.CuttingPart.Shape = Enum.PartType:FromValue(serialDestructionParams.CuttingPart.Shape)
|
|
930
|
+
local destructionParams = self:ValidateDestructionParams(serialDestructionParams)
|
|
931
|
+
destructionParams.replicated = true
|
|
932
|
+
return destructionParams
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
function Shatterbox:ModelQueueBehavior(funcToUse, intersectingPart, FilterTagged, CleanupDelay, OnVoxelDestruct, GridSize, SkipEncapsulatedVoxels, OnDestructCompleted, UserData, ExcludePlayersReplication, SkipFloors, SkipWalls)
|
|
936
|
+
local cuttingPart = if typeof(intersectingPart) == "table" then intersectingPart.CuttingPart else intersectingPart
|
|
937
|
+
local retA, retB = {}, {}
|
|
938
|
+
|
|
939
|
+
if cuttingPart and typeof(cuttingPart) == "Instance" and cuttingPart:IsA("Model") then
|
|
940
|
+
for _, desc in ipairs(cuttingPart:GetDescendants()) do
|
|
941
|
+
if not desc:IsA("Part") then continue end
|
|
942
|
+
local rA, rB = funcToUse(self, desc, FilterTagged, CleanupDelay, OnVoxelDestruct, GridSize, SkipEncapsulatedVoxels, OnDestructCompleted, UserData, ExcludePlayersReplication, SkipFloors, SkipWalls)
|
|
943
|
+
if rA then
|
|
944
|
+
table.move(rA, 1, #rA, #retA + 1, retA)
|
|
945
|
+
end
|
|
946
|
+
if rB then
|
|
947
|
+
table.move(rB, 1, #rB, #retB + 1, retB)
|
|
948
|
+
end
|
|
949
|
+
end
|
|
950
|
+
return true, retA, retB
|
|
951
|
+
end
|
|
952
|
+
return false, retA, retB
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
--[[ FALLING GROUPS (DFS WELDING) ]]--
|
|
956
|
+
|
|
957
|
+
function Shatterbox:DFSWeld(FallingGroupID)
|
|
958
|
+
local sectionAnchors = self.FallingGroupSectionAnchors[FallingGroupID] or {}
|
|
959
|
+
for _, part in ipairs(sectionAnchors) do
|
|
960
|
+
part:Destroy()
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
local toProcess = {}
|
|
964
|
+
for DirtyGroupID in pairs(self.FallingGroupMembers[FallingGroupID]) do
|
|
965
|
+
for Part in pairs(self.DirtyGroupMembers[DirtyGroupID]) do
|
|
966
|
+
table.insert(toProcess, Part)
|
|
967
|
+
end
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
while #toProcess > 0 do
|
|
971
|
+
local StartingPart = table.remove(toProcess)
|
|
972
|
+
local SectionMembers = {}
|
|
973
|
+
local visited, pathStack = {[StartingPart] = true}, {StartingPart}
|
|
974
|
+
|
|
975
|
+
while #pathStack > 0 do
|
|
976
|
+
local currentPart = table.remove(pathStack)
|
|
977
|
+
local c, s = currentPart.CFrame, currentPart.Size
|
|
978
|
+
|
|
979
|
+
table.insert(SectionMembers, currentPart)
|
|
980
|
+
|
|
981
|
+
local nodeVisited = {}
|
|
982
|
+
|
|
983
|
+
for _, inAxis in ipairs(workspace:GetPartBoundsInBox(c, s + DFSWeldSearchXSizeOffset)) do
|
|
984
|
+
local inAxisFallingGroup = inAxis:GetAttribute(FallingGroupIDAttribute)
|
|
985
|
+
if (inAxisFallingGroup and inAxisFallingGroup ~= FallingGroupID) or visited[inAxis] or inAxis == currentPart or self:FindHumanoidAsAncestorChild(inAxis) then continue end
|
|
986
|
+
visited[inAxis] = true
|
|
987
|
+
table.insert(SectionMembers, inAxis)
|
|
988
|
+
nodeVisited[inAxis] = true
|
|
989
|
+
|
|
990
|
+
if not inAxisFallingGroup or self:ShouldSkipInstance(inAxis) then continue end
|
|
991
|
+
table.insert(pathStack, inAxis)
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
for _, inAxis in ipairs(workspace:GetPartBoundsInBox(c, s + DFSWeldSearchYSizeOffset)) do
|
|
995
|
+
local inAxisFallingGroup = inAxis:GetAttribute(FallingGroupIDAttribute)
|
|
996
|
+
if (inAxisFallingGroup and inAxisFallingGroup ~= FallingGroupID) or visited[inAxis] or nodeVisited[inAxis] or inAxis == currentPart or self:FindHumanoidAsAncestorChild(inAxis) then continue end
|
|
997
|
+
visited[inAxis] = true
|
|
998
|
+
table.insert(SectionMembers, inAxis)
|
|
999
|
+
nodeVisited[inAxis] = true
|
|
1000
|
+
|
|
1001
|
+
if not inAxisFallingGroup or self:ShouldSkipInstance(inAxis) then continue end
|
|
1002
|
+
table.insert(pathStack, inAxis)
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
for _, inAxis in ipairs(workspace:GetPartBoundsInBox(c, s + DFSWeldSearchZSizeOffset)) do
|
|
1006
|
+
local inAxisFallingGroup = inAxis:GetAttribute(FallingGroupIDAttribute)
|
|
1007
|
+
if (inAxisFallingGroup and inAxisFallingGroup ~= FallingGroupID) or visited[inAxis] or nodeVisited[inAxis] or inAxis == currentPart or self:FindHumanoidAsAncestorChild(inAxis) then continue end
|
|
1008
|
+
visited[inAxis] = true
|
|
1009
|
+
table.insert(SectionMembers, inAxis)
|
|
1010
|
+
|
|
1011
|
+
if not inAxisFallingGroup or self:ShouldSkipInstance(inAxis) then continue end
|
|
1012
|
+
table.insert(pathStack, inAxis)
|
|
1013
|
+
end
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
local SectionAnchor = Instance.new("Part")
|
|
1017
|
+
SectionAnchor.Size = Vector3.one
|
|
1018
|
+
SectionAnchor.Transparency = 1
|
|
1019
|
+
SectionAnchor.CanCollide = false
|
|
1020
|
+
SectionAnchor.CanQuery = false
|
|
1021
|
+
SectionAnchor.Parent = workspace
|
|
1022
|
+
|
|
1023
|
+
local avgPos = Vector3.zero
|
|
1024
|
+
for _, Part in ipairs(SectionMembers) do
|
|
1025
|
+
avgPos += Part.Position
|
|
1026
|
+
|
|
1027
|
+
local found = table.find(toProcess, Part)
|
|
1028
|
+
if found then table.remove(toProcess, found) end
|
|
1029
|
+
end
|
|
1030
|
+
avgPos /= #SectionMembers
|
|
1031
|
+
|
|
1032
|
+
SectionAnchor.CFrame = CFrame.new(avgPos)
|
|
1033
|
+
local InverseSectionAnchorCF = SectionAnchor.CFrame:Inverse()
|
|
1034
|
+
|
|
1035
|
+
for _, Part in ipairs(SectionMembers) do
|
|
1036
|
+
local weld = Instance.new("Weld")
|
|
1037
|
+
weld.C0 = InverseSectionAnchorCF
|
|
1038
|
+
weld.C1 = Part.CFrame:Inverse()
|
|
1039
|
+
weld.Part0 = SectionAnchor
|
|
1040
|
+
weld.Part1 = Part
|
|
1041
|
+
weld.Parent = SectionAnchor
|
|
1042
|
+
end
|
|
1043
|
+
|
|
1044
|
+
for _, Part in ipairs(SectionMembers) do
|
|
1045
|
+
if Part:GetAttribute(FallingGroupIDAttribute) then
|
|
1046
|
+
Part.Anchored = false
|
|
1047
|
+
end
|
|
1048
|
+
end
|
|
1049
|
+
|
|
1050
|
+
table.insert(sectionAnchors, SectionAnchor)
|
|
1051
|
+
end
|
|
1052
|
+
|
|
1053
|
+
self.FallingGroupSectionAnchors[FallingGroupID] = sectionAnchors
|
|
1054
|
+
end
|
|
1055
|
+
|
|
1056
|
+
--[[ IMAGINARY VOXELS ]]--
|
|
1057
|
+
|
|
1058
|
+
function Shatterbox:ImaginaryVoxels_Internal(params, frameLimit, toDestroy)
|
|
1059
|
+
local voxels, existingDebris = {}, {}
|
|
1060
|
+
local voxelCount = 0
|
|
1061
|
+
|
|
1062
|
+
toDestroy = toDestroy or self:GetDestructibleParts(params)
|
|
1063
|
+
|
|
1064
|
+
if #toDestroy == 0 then return voxels, existingDebris end
|
|
1065
|
+
|
|
1066
|
+
local partsInPart = {}
|
|
1067
|
+
for i, part in ipairs(toDestroy) do
|
|
1068
|
+
if part:HasTag(DebrisTag) then
|
|
1069
|
+
table.insert(existingDebris, part)
|
|
1070
|
+
continue
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
part.CanQuery = false
|
|
1074
|
+
|
|
1075
|
+
local DirtyGroupID = part:GetAttribute(DirtyGroupIDAttribute)
|
|
1076
|
+
|
|
1077
|
+
table.insert(partsInPart, {
|
|
1078
|
+
CFrame = part.CFrame,
|
|
1079
|
+
Size = part.Size,
|
|
1080
|
+
DestructionID = params.ID,
|
|
1081
|
+
DirtyGroupID = DirtyGroupID,
|
|
1082
|
+
GridSize = params.GridSize or self.DirtyGroupData[DirtyGroupID].GridSize or self:UseSetting('DefaultGridSize')
|
|
1083
|
+
})
|
|
1084
|
+
end
|
|
1085
|
+
|
|
1086
|
+
local cuttingPart = params.CuttingPart
|
|
1087
|
+
local cpcf, cps, cpsh = cuttingPart.CFrame, cuttingPart.Size, cuttingPart.Shape.Name
|
|
1088
|
+
local iBl, iBa, iC = cpsh == "Block", cpsh == "Ball", cpsh == "Cylinder"
|
|
1089
|
+
|
|
1090
|
+
local intersectingVerts, intersectingNormals
|
|
1091
|
+
if iBl or iC then
|
|
1092
|
+
intersectingVerts = BlockVerts(cpcf, cps)
|
|
1093
|
+
intersectingNormals = BlockNormals(cpcf)
|
|
1094
|
+
elseif not iBa then
|
|
1095
|
+
intersectingVerts = GetVerts[cpsh](cpcf, cps)
|
|
1096
|
+
intersectingNormals = GetNormals[cpsh](cpcf, cps)
|
|
1097
|
+
end
|
|
1098
|
+
|
|
1099
|
+
local dividing = partsInPart
|
|
1100
|
+
local creating = {}
|
|
1101
|
+
|
|
1102
|
+
while #dividing > 0 do
|
|
1103
|
+
if frameLimit and self.frameDivs > self:UseSetting('MaxDivisionsPerFrame') then
|
|
1104
|
+
table.move(dividing, 1, #dividing, #creating + 1, creating)
|
|
1105
|
+
break
|
|
1106
|
+
end
|
|
1107
|
+
|
|
1108
|
+
local toDivide = table.remove(dividing, 1)
|
|
1109
|
+
local verts = BlockVerts(toDivide.CFrame, toDivide.Size)
|
|
1110
|
+
|
|
1111
|
+
local intersected = PartContainsAVert(cuttingPart, verts)
|
|
1112
|
+
|
|
1113
|
+
if not intersected then
|
|
1114
|
+
if iBa then
|
|
1115
|
+
intersected = VertexMath.BallIntersectsBlock(cpcf, cps, toDivide.CFrame, toDivide.Size)
|
|
1116
|
+
elseif iC then
|
|
1117
|
+
intersected = VertexMath.CylinderIntersectsBlock(cpcf, cps, intersectingVerts, intersectingNormals, toDivide.CFrame, toDivide.Size, verts, BlockNormals(toDivide.CFrame))
|
|
1118
|
+
else
|
|
1119
|
+
intersected = VertexMath.SAT(intersectingVerts, intersectingNormals, verts, BlockNormals(toDivide.CFrame))
|
|
1120
|
+
end
|
|
1121
|
+
else
|
|
1122
|
+
table.remove(verts, intersected)
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
if intersected then
|
|
1126
|
+
local s, gridSize, DirtyGroupID, destructionID = toDivide.Size, toDivide.GridSize, toDivide.DirtyGroupID, toDivide.DestructionID
|
|
1127
|
+
|
|
1128
|
+
local voxelCountAxis = Vector3.one:Max(s // gridSize)
|
|
1129
|
+
local toDivideVoxelContributions = voxelCountAxis.X * voxelCountAxis.Y * voxelCountAxis.Z
|
|
1130
|
+
|
|
1131
|
+
if toDivideVoxelContributions == 1 then
|
|
1132
|
+
voxelCount += toDivideVoxelContributions
|
|
1133
|
+
table.insert(voxels, toDivide)
|
|
1134
|
+
continue
|
|
1135
|
+
end
|
|
1136
|
+
|
|
1137
|
+
if PartContainsAllVerts(cuttingPart, verts or BlockVerts(toDivide.CFrame, toDivide.Size)) then
|
|
1138
|
+
voxelCount += toDivideVoxelContributions
|
|
1139
|
+
|
|
1140
|
+
if params.SkipEncapsulatedVoxels then continue end
|
|
1141
|
+
|
|
1142
|
+
local newVoxels = ImaginaryVoxelize(toDivide, gridSize)
|
|
1143
|
+
for _, created in ipairs(newVoxels) do
|
|
1144
|
+
created.GridSize = gridSize
|
|
1145
|
+
created.DirtyGroupID = DirtyGroupID
|
|
1146
|
+
created.DestructionID = destructionID
|
|
1147
|
+
end
|
|
1148
|
+
table.move(newVoxels, 1, #newVoxels, #voxels + 1, voxels)
|
|
1149
|
+
|
|
1150
|
+
continue
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1153
|
+
if frameLimit then self.frameDivs += 1 end
|
|
1154
|
+
|
|
1155
|
+
toDivide.isEdge = true
|
|
1156
|
+
|
|
1157
|
+
local sx, sy, sz = s.X, s.Y, s.Z
|
|
1158
|
+
local minsize, maxsize = math.min(sx, sy, sz), math.max(sx, sy, sz)
|
|
1159
|
+
|
|
1160
|
+
for _, created in ipairs(SubdivideFunctions[(maxsize > 2 * minsize) and "KD" or "MOT"](toDivide, gridSize)) do
|
|
1161
|
+
if PartContainsAllVerts(cuttingPart, BlockVerts(created.CFrame, created.Size)) then
|
|
1162
|
+
local countAxis = Vector3.one:Max(created.Size // gridSize)
|
|
1163
|
+
voxelCount += countAxis.X * countAxis.Y * countAxis.Z
|
|
1164
|
+
|
|
1165
|
+
if params.SkipEncapsulatedVoxels then continue end
|
|
1166
|
+
|
|
1167
|
+
local newVoxels = ImaginaryVoxelize(created, gridSize)
|
|
1168
|
+
for _, v in ipairs(newVoxels) do
|
|
1169
|
+
v.GridSize = gridSize
|
|
1170
|
+
v.DirtyGroupID = DirtyGroupID
|
|
1171
|
+
v.DestructionID = destructionID
|
|
1172
|
+
end
|
|
1173
|
+
table.move(newVoxels, 1, #newVoxels, #voxels + 1, voxels)
|
|
1174
|
+
|
|
1175
|
+
continue
|
|
1176
|
+
end
|
|
1177
|
+
|
|
1178
|
+
created.GridSize = gridSize
|
|
1179
|
+
created.DirtyGroupID = DirtyGroupID
|
|
1180
|
+
created.DestructionID = destructionID
|
|
1181
|
+
created.isEdge = true
|
|
1182
|
+
table.insert(dividing, created)
|
|
1183
|
+
end
|
|
1184
|
+
else
|
|
1185
|
+
table.insert(creating, toDivide)
|
|
1186
|
+
end
|
|
1187
|
+
end
|
|
1188
|
+
|
|
1189
|
+
for _, ibox in ipairs(creating) do
|
|
1190
|
+
self:InstantiatePart(ibox.DirtyGroupID, ibox.CFrame, ibox.Size)
|
|
1191
|
+
end
|
|
1192
|
+
|
|
1193
|
+
for _, part in ipairs(toDestroy) do
|
|
1194
|
+
if table.find(existingDebris, part) then continue end
|
|
1195
|
+
self:DestroyPart(part)
|
|
1196
|
+
end
|
|
1197
|
+
|
|
1198
|
+
for FallingGroupID in pairs(self.OperationAffectedFallingGroups[params.ID]) do
|
|
1199
|
+
self:DFSWeld(FallingGroupID)
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
return setmetatable(voxels, {__len = function() return voxelCount end}), existingDebris
|
|
1203
|
+
end
|
|
1204
|
+
|
|
1205
|
+
--[[ VOXEL DESTRUCTION HANDLING ]]--
|
|
1206
|
+
|
|
1207
|
+
function Shatterbox:HandleVoxelDestruction(callbackName: string?, voxel: Part, info)
|
|
1208
|
+
if not info then info = {} end
|
|
1209
|
+
|
|
1210
|
+
local s, e = pcall(self.OnVoxelDestructRegistry[callbackName], voxel, info)
|
|
1211
|
+
|
|
1212
|
+
if not s then warn("Voxel destruction failed: " .. e) end
|
|
1213
|
+
|
|
1214
|
+
if not info.IsAlreadyDebris then voxel:AddTag(DebrisTag) end
|
|
1215
|
+
end
|
|
1216
|
+
|
|
1217
|
+
function Shatterbox:PopShatterQueue()
|
|
1218
|
+
local popped, shatterPart, partsInPart
|
|
1219
|
+
local v = table.remove(self.ShatterQueue)
|
|
1220
|
+
|
|
1221
|
+
while v and not popped do
|
|
1222
|
+
local DestructionID = v.ID
|
|
1223
|
+
local AffectedDirtyGroupIDs = self.OperationAffectedDirtyGroups[DestructionID]
|
|
1224
|
+
local BlacklistedVoxels = self.OperationBlacklistedVoxels[DestructionID]
|
|
1225
|
+
|
|
1226
|
+
shatterPart, partsInPart = v.CuttingPart, {}
|
|
1227
|
+
|
|
1228
|
+
for _, part in ipairs(self:GetDestructibleParts(v)) do
|
|
1229
|
+
if part:HasTag(DebrisTag) then
|
|
1230
|
+
if not BlacklistedVoxels[part] then
|
|
1231
|
+
BlacklistedVoxels[part] = true
|
|
1232
|
+
self:HandleVoxelDestruction(v.OnVoxelDestruct, part, {
|
|
1233
|
+
DirtyGroupID = part:GetAttribute(DirtyGroupIDAttribute),
|
|
1234
|
+
CuttingPart = shatterPart,
|
|
1235
|
+
IsEdge = not PartEncapsulatesBlockPart(shatterPart, part.CFrame, part.Size),
|
|
1236
|
+
IsAlreadyDebris = true,
|
|
1237
|
+
UserData = v.UserData
|
|
1238
|
+
})
|
|
1239
|
+
end
|
|
1240
|
+
continue
|
|
1241
|
+
end
|
|
1242
|
+
|
|
1243
|
+
table.insert(partsInPart, part)
|
|
1244
|
+
end
|
|
1245
|
+
|
|
1246
|
+
if #partsInPart == 0 then
|
|
1247
|
+
if v.OnDestructCompleted then
|
|
1248
|
+
v.OnDestructCompleted(v.DestroyedVoxels, AffectedDirtyGroupIDs)
|
|
1249
|
+
end
|
|
1250
|
+
|
|
1251
|
+
for DirtyGroupID in pairs(AffectedDirtyGroupIDs) do
|
|
1252
|
+
local count = self.CountOperationsProcessing[DirtyGroupID] - 1
|
|
1253
|
+
self.CountOperationsProcessing[DirtyGroupID] = count > 0 and count or nil
|
|
1254
|
+
end
|
|
1255
|
+
self.OperationAffectedDirtyGroups[DestructionID] = nil
|
|
1256
|
+
self.OperationAffectedFallingGroups[DestructionID] = nil
|
|
1257
|
+
self.OperationBlacklistedVoxels[DestructionID] = nil
|
|
1258
|
+
|
|
1259
|
+
v = table.remove(self.ShatterQueue)
|
|
1260
|
+
else
|
|
1261
|
+
popped = v
|
|
1262
|
+
table.insert(self.ShatterQueue, self:UseSetting('UsePriorityQueue') and math.max(1, #self.ShatterQueue + 1 - self:UseSetting('PrioritizeRecentN')) or 1, popped)
|
|
1263
|
+
end
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
return popped, partsInPart
|
|
1267
|
+
end
|
|
1268
|
+
|
|
1269
|
+
function Shatterbox:ShatterWorker()
|
|
1270
|
+
local popped, partsInPart = self:PopShatterQueue()
|
|
1271
|
+
if not popped then return false end
|
|
1272
|
+
|
|
1273
|
+
local blacklistedVoxels = self.OperationBlacklistedVoxels[popped.ID]
|
|
1274
|
+
|
|
1275
|
+
local ivoxels = self:ImaginaryVoxels_Internal(popped, true, partsInPart)
|
|
1276
|
+
popped.DestroyedVoxels += #ivoxels
|
|
1277
|
+
|
|
1278
|
+
local OnVoxelDestruct = popped.OnVoxelDestruct
|
|
1279
|
+
if self.OnVoxelDestructRegistry[OnVoxelDestruct] then
|
|
1280
|
+
local CuttingPart, UserData = popped.CuttingPart, popped.UserData
|
|
1281
|
+
for _, ivoxel in ipairs(ivoxels) do
|
|
1282
|
+
local voxel = self:InstantiateImaginaryVoxel(ivoxel, true)
|
|
1283
|
+
|
|
1284
|
+
blacklistedVoxels[voxel] = true
|
|
1285
|
+
|
|
1286
|
+
self:HandleVoxelDestruction(OnVoxelDestruct, voxel, {
|
|
1287
|
+
DirtyGroupID = ivoxel.DirtyGroupID,
|
|
1288
|
+
CuttingPart = CuttingPart,
|
|
1289
|
+
IsEdge = ivoxel.isEdge,
|
|
1290
|
+
IsAlreadyDebris = false,
|
|
1291
|
+
UserData = UserData
|
|
1292
|
+
})
|
|
1293
|
+
end
|
|
1294
|
+
end
|
|
1295
|
+
|
|
1296
|
+
return true
|
|
1297
|
+
end
|
|
1298
|
+
|
|
1299
|
+
--[[ SMOOTH CLEANUP ]]--
|
|
1300
|
+
|
|
1301
|
+
function Shatterbox:SmoothCleanupBehavior(deltaTime)
|
|
1302
|
+
local toReset = {}
|
|
1303
|
+
|
|
1304
|
+
for DirtyGroupID, countdown in pairs(self.SmoothCleanupQueue) do
|
|
1305
|
+
countdown -= deltaTime
|
|
1306
|
+
if countdown > 0 then
|
|
1307
|
+
self.SmoothCleanupQueue[DirtyGroupID] = countdown
|
|
1308
|
+
else
|
|
1309
|
+
table.insert(toReset, DirtyGroupID)
|
|
1310
|
+
end
|
|
1311
|
+
end
|
|
1312
|
+
|
|
1313
|
+
local waitingToTerminate = {}
|
|
1314
|
+
|
|
1315
|
+
for _, DirtyGroupID in ipairs(toReset) do
|
|
1316
|
+
if self.CountOperationsProcessing[DirtyGroupID] then continue end
|
|
1317
|
+
|
|
1318
|
+
if self.GreedyMeshingQueue[DirtyGroupID] then
|
|
1319
|
+
self.GreedyMeshingQueue[DirtyGroupID] = nil
|
|
1320
|
+
end
|
|
1321
|
+
if self.GreedyWorkers[DirtyGroupID] then
|
|
1322
|
+
waitingToTerminate[DirtyGroupID] = true
|
|
1323
|
+
continue
|
|
1324
|
+
end
|
|
1325
|
+
|
|
1326
|
+
self.SmoothCleanupQueue[DirtyGroupID] = nil
|
|
1327
|
+
self:UndoDirtyGroup(DirtyGroupID)
|
|
1328
|
+
end
|
|
1329
|
+
|
|
1330
|
+
while RunService.Heartbeat:Wait() do
|
|
1331
|
+
local allDone = true
|
|
1332
|
+
for DirtyGroupID in pairs(waitingToTerminate) do
|
|
1333
|
+
if self.GreedyWorkers[DirtyGroupID] then
|
|
1334
|
+
allDone = false
|
|
1335
|
+
break
|
|
1336
|
+
end
|
|
1337
|
+
end
|
|
1338
|
+
if allDone then break end
|
|
1339
|
+
end
|
|
1340
|
+
|
|
1341
|
+
for DirtyGroupID in pairs(waitingToTerminate) do
|
|
1342
|
+
self.SmoothCleanupQueue[DirtyGroupID] = nil
|
|
1343
|
+
self:UndoDirtyGroup(DirtyGroupID)
|
|
1344
|
+
end
|
|
1345
|
+
end
|
|
1346
|
+
|
|
1347
|
+
--[[ PUPPET SYSTEM ]]--
|
|
1348
|
+
|
|
1349
|
+
function Shatterbox:ReadRadianU8(b, bitOff)
|
|
1350
|
+
local X = buffer.readbits(b, bitOff, 8)
|
|
1351
|
+
return PRECISE_45_DEGREES[X] or (X * U82RAD)
|
|
1352
|
+
end
|
|
1353
|
+
|
|
1354
|
+
function Shatterbox:ReadFloatI25(b, bitOff)
|
|
1355
|
+
local X = buffer.readbits(b, bitOff + 1, 24) * 0.001
|
|
1356
|
+
return buffer.readbits(b, bitOff, 1) == 0 and X or -X
|
|
1357
|
+
end
|
|
1358
|
+
|
|
1359
|
+
function Shatterbox:WriteFloatI25(b, bitOff, x)
|
|
1360
|
+
local S = 0
|
|
1361
|
+
if x < 0 then
|
|
1362
|
+
S = 1
|
|
1363
|
+
x = -x
|
|
1364
|
+
end
|
|
1365
|
+
buffer.writebits(b, bitOff, 1, S)
|
|
1366
|
+
buffer.writebits(b, bitOff + 1, 24, math.round(x * 1000))
|
|
1367
|
+
end
|
|
1368
|
+
|
|
1369
|
+
function Shatterbox:DeserializedPuppet(serialPuppet)
|
|
1370
|
+
local Part = DeserializedPartInstance(serialPuppet, self.CachedParts)
|
|
1371
|
+
Part.Anchored = true
|
|
1372
|
+
Part:AddTag(IndestructibleTag)
|
|
1373
|
+
Part.Parent = self.mapContainer
|
|
1374
|
+
return Part
|
|
1375
|
+
end
|
|
1376
|
+
|
|
1377
|
+
function Shatterbox:CreatePuppets(puppets)
|
|
1378
|
+
for _, serialPuppet in ipairs(puppets) do
|
|
1379
|
+
local Part = self:DeserializedPuppet(serialPuppet)
|
|
1380
|
+
|
|
1381
|
+
local OldPuppetData = self.Puppets[self.PuppetNextIndex]
|
|
1382
|
+
if OldPuppetData then
|
|
1383
|
+
OldPuppetData[1]:Destroy()
|
|
1384
|
+
self.PuppetsLerpData[self.PuppetNextIndex] = nil
|
|
1385
|
+
end
|
|
1386
|
+
|
|
1387
|
+
self.Puppets[self.PuppetNextIndex] = {Part}
|
|
1388
|
+
|
|
1389
|
+
self.PuppetNextIndex += 1
|
|
1390
|
+
if self.PuppetNextIndex > self:UseSetting('PuppetMaxCount') then
|
|
1391
|
+
self.PuppetNextIndex = 1
|
|
1392
|
+
end
|
|
1393
|
+
end
|
|
1394
|
+
self.FakePuppetOverrideIndex = self.PuppetNextIndex
|
|
1395
|
+
end
|
|
1396
|
+
|
|
1397
|
+
function Shatterbox:ReplicatePuppetStates(puppetStatesBuffer, stepDelta)
|
|
1398
|
+
self.ClientPuppetStepDelta = stepDelta
|
|
1399
|
+
|
|
1400
|
+
local bitLength = buffer.len(puppetStatesBuffer) * 8
|
|
1401
|
+
local bitOffset = 0
|
|
1402
|
+
|
|
1403
|
+
local i = 1
|
|
1404
|
+
local updatedPuppets, updatedStates = {}, {}
|
|
1405
|
+
|
|
1406
|
+
local clientCamPos = workspace.CurrentCamera.CFrame.Position
|
|
1407
|
+
|
|
1408
|
+
local sqLimit = self:UseSetting('ClientTweenDistanceLimit') ^ 2
|
|
1409
|
+
local doNotTweenPuppets = not self:UseSetting('ClientTweenPuppets')
|
|
1410
|
+
|
|
1411
|
+
while bitOffset < bitLength do
|
|
1412
|
+
local index = buffer.readbits(puppetStatesBuffer, bitOffset + 99, 13)
|
|
1413
|
+
local puppetData = self.Puppets[index]
|
|
1414
|
+
if puppetData then
|
|
1415
|
+
local cframe = CFrame.new(
|
|
1416
|
+
self:ReadFloatI25(puppetStatesBuffer, bitOffset),
|
|
1417
|
+
self:ReadFloatI25(puppetStatesBuffer, bitOffset + 25),
|
|
1418
|
+
self:ReadFloatI25(puppetStatesBuffer, bitOffset + 50)
|
|
1419
|
+
) * CFrame.fromEulerAngles(
|
|
1420
|
+
self:ReadRadianU8(puppetStatesBuffer, bitOffset + 75),
|
|
1421
|
+
self:ReadRadianU8(puppetStatesBuffer, bitOffset + 83),
|
|
1422
|
+
self:ReadRadianU8(puppetStatesBuffer, bitOffset + 91)
|
|
1423
|
+
)
|
|
1424
|
+
|
|
1425
|
+
local toPlr = clientCamPos - cframe.Position
|
|
1426
|
+
if doNotTweenPuppets or (toPlr.X^2 + toPlr.Y^2 + toPlr.Z^2 >= sqLimit) then
|
|
1427
|
+
if self.PuppetsLerpData[index] then
|
|
1428
|
+
self.PuppetsLerpData[index] = nil
|
|
1429
|
+
end
|
|
1430
|
+
updatedPuppets[i] = puppetData[1]
|
|
1431
|
+
updatedStates[i] = cframe
|
|
1432
|
+
i += 1
|
|
1433
|
+
else
|
|
1434
|
+
self.PuppetsLerpData[index] = {
|
|
1435
|
+
puppetData[1].CFrame,
|
|
1436
|
+
cframe,
|
|
1437
|
+
0
|
|
1438
|
+
}
|
|
1439
|
+
end
|
|
1440
|
+
end
|
|
1441
|
+
|
|
1442
|
+
bitOffset += 112
|
|
1443
|
+
end
|
|
1444
|
+
|
|
1445
|
+
if i > 1 then
|
|
1446
|
+
workspace:BulkMoveTo(updatedPuppets, updatedStates, Enum.BulkMoveMode.FireCFrameChanged)
|
|
1447
|
+
end
|
|
1448
|
+
end
|
|
1449
|
+
|
|
1450
|
+
function Shatterbox:DestroyPuppet(index)
|
|
1451
|
+
self.Puppets[index][1]:Destroy()
|
|
1452
|
+
self.Puppets[index] = nil
|
|
1453
|
+
self.PuppetsLerpData[index] = nil
|
|
1454
|
+
end
|
|
1455
|
+
|
|
1456
|
+
function Shatterbox:PuppeteerBehavior(deltaTime)
|
|
1457
|
+
self.puppetTimeAccumulation += deltaTime
|
|
1458
|
+
local P = 1 / self:UseSetting('PuppetReplicationFrequency')
|
|
1459
|
+
|
|
1460
|
+
if self.puppetTimeAccumulation >= P then
|
|
1461
|
+
repeat self.puppetTimeAccumulation -= P until self.puppetTimeAccumulation < P
|
|
1462
|
+
|
|
1463
|
+
for i, puppetData in pairs(self.Puppets) do
|
|
1464
|
+
local puppetVoxel = puppetData[1]
|
|
1465
|
+
|
|
1466
|
+
if puppetVoxel.Anchored then continue end
|
|
1467
|
+
|
|
1468
|
+
if puppetVoxel.AssemblyLinearVelocity.Magnitude <= self:UseSetting('PuppetSleepVelocity') then
|
|
1469
|
+
local d = puppetData[4] + deltaTime
|
|
1470
|
+
if d >= self:UseSetting('PuppetAnchorTimeout') then
|
|
1471
|
+
puppetVoxel.Anchored = true
|
|
1472
|
+
table.insert(self.PuppetsLerpData[i], 1, puppetVoxel.CFrame)
|
|
1473
|
+
puppetData[4] = 0
|
|
1474
|
+
else
|
|
1475
|
+
puppetData[4] = d
|
|
1476
|
+
end
|
|
1477
|
+
continue
|
|
1478
|
+
end
|
|
1479
|
+
|
|
1480
|
+
puppetData[4] = 0
|
|
1481
|
+
table.insert(self.PuppetsLerpData[i], 1, puppetVoxel.CFrame)
|
|
1482
|
+
end
|
|
1483
|
+
|
|
1484
|
+
local AvailablePuppets = {}
|
|
1485
|
+
local StartIndex = self.PuppetNextReplicationIndex
|
|
1486
|
+
|
|
1487
|
+
while #AvailablePuppets < 70 do
|
|
1488
|
+
local ReplicationIndex = self.PuppetNextReplicationIndex
|
|
1489
|
+
self.PuppetNextReplicationIndex += 1
|
|
1490
|
+
if self.PuppetNextReplicationIndex > self:UseSetting('PuppetMaxCount') then
|
|
1491
|
+
self.PuppetNextReplicationIndex = 1
|
|
1492
|
+
end
|
|
1493
|
+
if self.PuppetNextReplicationIndex == StartIndex then break end
|
|
1494
|
+
|
|
1495
|
+
local puppetData = self.Puppets[ReplicationIndex]
|
|
1496
|
+
if not puppetData then continue end
|
|
1497
|
+
|
|
1498
|
+
if #self.PuppetsLerpData[ReplicationIndex] == 0 then continue end
|
|
1499
|
+
|
|
1500
|
+
if table.find(self.CreatePuppetQueue, puppetData[1]) then continue end
|
|
1501
|
+
|
|
1502
|
+
table.insert(AvailablePuppets, ReplicationIndex)
|
|
1503
|
+
end
|
|
1504
|
+
|
|
1505
|
+
local countToReplicate = #AvailablePuppets
|
|
1506
|
+
if countToReplicate > 0 then
|
|
1507
|
+
local puppetStates = buffer.create(countToReplicate * 14)
|
|
1508
|
+
|
|
1509
|
+
for i, ReplicationIndex in ipairs(AvailablePuppets) do
|
|
1510
|
+
local bufferBitOffset = (i - 1) * 112
|
|
1511
|
+
|
|
1512
|
+
local cframe = table.remove(self.PuppetsLerpData[ReplicationIndex])
|
|
1513
|
+
self:WriteFloatI25(puppetStates, bufferBitOffset, cframe.X)
|
|
1514
|
+
self:WriteFloatI25(puppetStates, bufferBitOffset + 25, cframe.Y)
|
|
1515
|
+
self:WriteFloatI25(puppetStates, bufferBitOffset + 50, cframe.Z)
|
|
1516
|
+
|
|
1517
|
+
local rX, rY, rZ = cframe:ToEulerAngles()
|
|
1518
|
+
buffer.writebits(puppetStates, bufferBitOffset + 75, 8, math.round((rX % TAU) * RAD2U8))
|
|
1519
|
+
buffer.writebits(puppetStates, bufferBitOffset + 83, 8, math.round((rY % TAU) * RAD2U8))
|
|
1520
|
+
buffer.writebits(puppetStates, bufferBitOffset + 91, 8, math.round((rZ % TAU) * RAD2U8))
|
|
1521
|
+
|
|
1522
|
+
buffer.writebits(puppetStates, bufferBitOffset + 99, 13, ReplicationIndex)
|
|
1523
|
+
end
|
|
1524
|
+
|
|
1525
|
+
local stepDelta = 0
|
|
1526
|
+
for _, queuedCFrames in pairs(self.PuppetsLerpData) do
|
|
1527
|
+
if #queuedCFrames == 0 then continue end
|
|
1528
|
+
stepDelta += 1
|
|
1529
|
+
end
|
|
1530
|
+
stepDelta = stepDelta > 70 and stepDelta // 70 or 1
|
|
1531
|
+
|
|
1532
|
+
self.ServerEvents.ReplicatePuppetStates.FireAll(puppetStates, stepDelta)
|
|
1533
|
+
end
|
|
1534
|
+
end
|
|
1535
|
+
|
|
1536
|
+
local PQL = #self.CreatePuppetQueue
|
|
1537
|
+
if PQL > 0 then
|
|
1538
|
+
local toReplicate = table.create(PQL)
|
|
1539
|
+
for i, puppetToCreate in ipairs(self.CreatePuppetQueue) do
|
|
1540
|
+
toReplicate[i] = SerializedPartInstance(puppetToCreate)
|
|
1541
|
+
end
|
|
1542
|
+
self.CreatePuppetQueue = {}
|
|
1543
|
+
self.ServerEvents.CreatePuppets.FireAll(toReplicate)
|
|
1544
|
+
end
|
|
1545
|
+
end
|
|
1546
|
+
|
|
1547
|
+
--[[ REPLICATION ]]--
|
|
1548
|
+
|
|
1549
|
+
function Shatterbox:CreateSerialDirtyGroups(SerialDirtyGroups)
|
|
1550
|
+
for DirtyGroupID, serialOriginal in pairs(SerialDirtyGroups) do
|
|
1551
|
+
local newPart = DeserializedPartInstance(serialOriginal)
|
|
1552
|
+
newPart.Parent = self.mapContainer
|
|
1553
|
+
|
|
1554
|
+
self.DirtyGroupData[DirtyGroupID] = {
|
|
1555
|
+
OriginalPart = newPart,
|
|
1556
|
+
OriginalParent = self.mapContainer,
|
|
1557
|
+
GridSize = serialOriginal.Attributes.GridSize
|
|
1558
|
+
}
|
|
1559
|
+
self.DirtyGroupMembers[DirtyGroupID] = {}
|
|
1560
|
+
end
|
|
1561
|
+
end
|
|
1562
|
+
|
|
1563
|
+
function Shatterbox:ReplicateDestructionToClients(destructionType, destructionParams, createdDirtyGroups)
|
|
1564
|
+
local serialNewDirtyGroups = {}
|
|
1565
|
+
for DirtyGroupID in pairs(createdDirtyGroups) do
|
|
1566
|
+
serialNewDirtyGroups[DirtyGroupID] = SerializedPartInstance(self.DirtyGroupData[DirtyGroupID].OriginalPart)
|
|
1567
|
+
end
|
|
1568
|
+
|
|
1569
|
+
local exclusions = destructionParams.ExcludePlayersReplication
|
|
1570
|
+
if #exclusions > 0 then
|
|
1571
|
+
local PlayerList = Players:GetPlayers()
|
|
1572
|
+
for _, plr in ipairs(exclusions) do
|
|
1573
|
+
local found = table.find(PlayerList, plr)
|
|
1574
|
+
if found then table.remove(PlayerList, found) end
|
|
1575
|
+
end
|
|
1576
|
+
|
|
1577
|
+
self.ServerEvents.ReplicateDestruction.FireList(PlayerList, destructionType, self:SerializedDestructionParams(destructionParams), serialNewDirtyGroups, self.OperationAffectedDirtyGroups[destructionParams.ID])
|
|
1578
|
+
else
|
|
1579
|
+
self.ServerEvents.ReplicateDestruction.FireAll(destructionType, self:SerializedDestructionParams(destructionParams), serialNewDirtyGroups, self.OperationAffectedDirtyGroups[destructionParams.ID])
|
|
1580
|
+
end
|
|
1581
|
+
end
|
|
1582
|
+
|
|
1583
|
+
function Shatterbox:ReplicateDestructionFromServer(destructionType, serialParams, serialNewDirtyGroups, affectedDirtyGroupIDs)
|
|
1584
|
+
local destructionParams = self:DeserializedDestructionParams(serialParams)
|
|
1585
|
+
|
|
1586
|
+
self:CreateSerialDirtyGroups(serialNewDirtyGroups)
|
|
1587
|
+
|
|
1588
|
+
while not self.clientInitComplete do RunService.Heartbeat:Wait() end
|
|
1589
|
+
|
|
1590
|
+
if destructionType == IMAGINARY then
|
|
1591
|
+
destructionParams.SkipEncapsulatedVoxels = true
|
|
1592
|
+
RunService.Heartbeat:Wait()
|
|
1593
|
+
self:ImaginaryVoxels_Internal(destructionParams)
|
|
1594
|
+
|
|
1595
|
+
elseif destructionType == NORMAL then
|
|
1596
|
+
local DestructionID = destructionParams.ID
|
|
1597
|
+
|
|
1598
|
+
self.OperationAffectedDirtyGroups[DestructionID] = affectedDirtyGroupIDs
|
|
1599
|
+
for DirtyGroupID in pairs(affectedDirtyGroupIDs) do
|
|
1600
|
+
self.CountOperationsProcessing[DirtyGroupID] = (self.CountOperationsProcessing[DirtyGroupID] or 0) + 1
|
|
1601
|
+
end
|
|
1602
|
+
self.OperationBlacklistedVoxels[DestructionID] = {}
|
|
1603
|
+
|
|
1604
|
+
destructionParams.DestroyedVoxels = 0
|
|
1605
|
+
table.insert(self.ShatterQueue, destructionParams)
|
|
1606
|
+
end
|
|
1607
|
+
end
|
|
1608
|
+
|
|
1609
|
+
function Shatterbox:ReplicateStateToPlayer(plr)
|
|
1610
|
+
local serialDirtyGroupData = {}
|
|
1611
|
+
for DirtyGroupID, groupData in pairs(self.DirtyGroupData) do
|
|
1612
|
+
local OriginalPart = groupData.OriginalPart
|
|
1613
|
+
|
|
1614
|
+
if OriginalPart:IsA("Part") and OriginalPart.Shape == Enum.PartType.Block then
|
|
1615
|
+
serialDirtyGroupData[DirtyGroupID] = SerializedPartInstance(OriginalPart)
|
|
1616
|
+
end
|
|
1617
|
+
end
|
|
1618
|
+
|
|
1619
|
+
local serialDirtyGroupMembers, destroyedDirtyGroups = {}, {}
|
|
1620
|
+
for DirtyGroupID in pairs(serialDirtyGroupData) do
|
|
1621
|
+
local groupMembers = self.DirtyGroupMembers[DirtyGroupID]
|
|
1622
|
+
local OriginalPart = self.DirtyGroupData[DirtyGroupID].OriginalPart
|
|
1623
|
+
|
|
1624
|
+
local serialMembers = {}
|
|
1625
|
+
serialDirtyGroupMembers[DirtyGroupID] = serialMembers
|
|
1626
|
+
|
|
1627
|
+
if next(groupMembers) then
|
|
1628
|
+
for member in pairs(groupMembers) do
|
|
1629
|
+
if member:HasTag(DebrisTag) then
|
|
1630
|
+
local found = false
|
|
1631
|
+
for _, puppetData in pairs(self.Puppets) do
|
|
1632
|
+
if puppetData[1] == member then
|
|
1633
|
+
found = true
|
|
1634
|
+
break
|
|
1635
|
+
end
|
|
1636
|
+
end
|
|
1637
|
+
if found then continue end
|
|
1638
|
+
end
|
|
1639
|
+
table.insert(serialMembers, SerializedPartInstance(member))
|
|
1640
|
+
end
|
|
1641
|
+
elseif not OriginalPart.Parent then
|
|
1642
|
+
destroyedDirtyGroups[DirtyGroupID] = true
|
|
1643
|
+
end
|
|
1644
|
+
end
|
|
1645
|
+
|
|
1646
|
+
local serialShatterQueue = {}
|
|
1647
|
+
for _, destructionParams in ipairs(self.ShatterQueue) do
|
|
1648
|
+
table.insert(serialShatterQueue, self:SerializedDestructionParams(destructionParams))
|
|
1649
|
+
end
|
|
1650
|
+
|
|
1651
|
+
local serialPuppets = {}
|
|
1652
|
+
for puppetID, puppetData in pairs(self.Puppets) do
|
|
1653
|
+
serialPuppets[puppetID] = SerializedPartInstance(puppetData[1])
|
|
1654
|
+
end
|
|
1655
|
+
|
|
1656
|
+
self.ServerEvents.ReplicateState.Fire(plr,
|
|
1657
|
+
serialDirtyGroupData,
|
|
1658
|
+
serialDirtyGroupMembers,
|
|
1659
|
+
destroyedDirtyGroups,
|
|
1660
|
+
serialShatterQueue,
|
|
1661
|
+
self.CountOperationsProcessing,
|
|
1662
|
+
self.OperationAffectedDirtyGroups,
|
|
1663
|
+
self.SmoothCleanupQueue,
|
|
1664
|
+
self.GreedyMeshingQueue,
|
|
1665
|
+
self.ActiveGMWorkerGridSize,
|
|
1666
|
+
serialPuppets,
|
|
1667
|
+
self.PuppetNextIndex
|
|
1668
|
+
)
|
|
1669
|
+
end
|
|
1670
|
+
|
|
1671
|
+
function Shatterbox:ReplicateStateFromServer(serialDirtyGroupData, serialDirtyGroupMembers, destroyedDirtyGroups, serialShatterQueue, serverCountOperationsProcessing, serverOperationAffectedDirtyGroups, serverSmoothCleanupQueue, serverGreedyMeshingQueue, serverActiveGMWorkerGridSize, serialPuppets, serverPuppetsNextIndex)
|
|
1672
|
+
for DirtyGroupID, serializedPart in pairs(serialDirtyGroupData) do
|
|
1673
|
+
self.DirtyGroupData[DirtyGroupID] = {
|
|
1674
|
+
OriginalPart = DeserializedPartInstance(serializedPart),
|
|
1675
|
+
OriginalParent = self.mapContainer,
|
|
1676
|
+
GridSize = serializedPart.Attributes.GridSize
|
|
1677
|
+
}
|
|
1678
|
+
end
|
|
1679
|
+
|
|
1680
|
+
for DirtyGroupID, serialGroupMembers in pairs(serialDirtyGroupMembers) do
|
|
1681
|
+
local groupMembers = {}
|
|
1682
|
+
self.DirtyGroupMembers[DirtyGroupID] = groupMembers
|
|
1683
|
+
|
|
1684
|
+
if destroyedDirtyGroups[DirtyGroupID] then continue end
|
|
1685
|
+
|
|
1686
|
+
if #serialGroupMembers == 0 then
|
|
1687
|
+
self.DirtyGroupData[DirtyGroupID].OriginalPart.Parent = self.mapContainer
|
|
1688
|
+
continue
|
|
1689
|
+
end
|
|
1690
|
+
|
|
1691
|
+
for _, serialMember in ipairs(serialGroupMembers) do
|
|
1692
|
+
local part = DeserializedPartInstance(serialMember, self.CachedParts)
|
|
1693
|
+
if not part.Parent then part.Parent = self.mapContainer end
|
|
1694
|
+
groupMembers[part] = true
|
|
1695
|
+
end
|
|
1696
|
+
end
|
|
1697
|
+
|
|
1698
|
+
for _, serialDestructionParams in ipairs(serialShatterQueue) do
|
|
1699
|
+
table.insert(self.ShatterQueue, self:DeserializedDestructionParams(serialDestructionParams))
|
|
1700
|
+
end
|
|
1701
|
+
self.CountOperationsProcessing = serverCountOperationsProcessing
|
|
1702
|
+
self.OperationAffectedDirtyGroups = serverOperationAffectedDirtyGroups
|
|
1703
|
+
|
|
1704
|
+
self.SmoothCleanupQueue = serverSmoothCleanupQueue
|
|
1705
|
+
|
|
1706
|
+
self.GreedyMeshingQueue = serverGreedyMeshingQueue
|
|
1707
|
+
for DirtyGroupID, GridSize in pairs(serverActiveGMWorkerGridSize) do
|
|
1708
|
+
local thr = coroutine.create(function()
|
|
1709
|
+
self:GreedyMeshWorker(DirtyGroupID, GridSize)
|
|
1710
|
+
end)
|
|
1711
|
+
|
|
1712
|
+
self.GreedyWorkers[DirtyGroupID] = thr
|
|
1713
|
+
self.ActiveGMWorkerCount += 1
|
|
1714
|
+
self.ActiveGMWorkerGridSize[DirtyGroupID] = GridSize
|
|
1715
|
+
|
|
1716
|
+
coroutine.resume(thr)
|
|
1717
|
+
end
|
|
1718
|
+
|
|
1719
|
+
self.PuppetNextIndex = serverPuppetsNextIndex
|
|
1720
|
+
for puppetID, serialPuppet in pairs(serialPuppets) do
|
|
1721
|
+
self.Puppets[puppetID] = {self:DeserializedPuppet(serialPuppet)}
|
|
1722
|
+
end
|
|
1723
|
+
|
|
1724
|
+
self.clientInitComplete = true
|
|
1725
|
+
end
|
|
1726
|
+
|
|
1727
|
+
--[[ HEARTBEAT WORKER ]]--
|
|
1728
|
+
|
|
1729
|
+
function Shatterbox:HeartbeatWorker(deltaTime)
|
|
1730
|
+
if self:UseSetting('UseSmoothCleanup') then
|
|
1731
|
+
coroutine.wrap(function()
|
|
1732
|
+
self:SmoothCleanupBehavior(deltaTime)
|
|
1733
|
+
end)()
|
|
1734
|
+
end
|
|
1735
|
+
|
|
1736
|
+
self.frameDivs = 0
|
|
1737
|
+
local ops = 0
|
|
1738
|
+
|
|
1739
|
+
while #self.ShatterQueue > 0 and self.frameDivs < self:UseSetting('MaxDivisionsPerFrame') and ops < self:UseSetting('MaxOpsPerFrame') do
|
|
1740
|
+
self:ShatterWorker()
|
|
1741
|
+
ops += 1
|
|
1742
|
+
end
|
|
1743
|
+
|
|
1744
|
+
if self:UseSetting('UseGreedyMeshing') then
|
|
1745
|
+
self:GreedyMeshingBehavior()
|
|
1746
|
+
end
|
|
1747
|
+
|
|
1748
|
+
if isServer then
|
|
1749
|
+
self:PuppeteerBehavior(deltaTime)
|
|
1750
|
+
end
|
|
1751
|
+
end
|
|
1752
|
+
|
|
1753
|
+
--[[ PUBLIC API METHODS ]]--
|
|
1754
|
+
|
|
1755
|
+
function Shatterbox:Puppeteer(voxel: Part)
|
|
1756
|
+
assert(isServer, "This function can only be called from the server")
|
|
1757
|
+
|
|
1758
|
+
voxel.Anchored = false
|
|
1759
|
+
voxel.Parent = self.mapContainer
|
|
1760
|
+
|
|
1761
|
+
local s, e = pcall(voxel.SetNetworkOwner, voxel, nil)
|
|
1762
|
+
if not s then
|
|
1763
|
+
warn("Failed to set the network owner of the Puppet, is it welded to an anchored part?\n" .. e)
|
|
1764
|
+
end
|
|
1765
|
+
|
|
1766
|
+
local noSleepCon
|
|
1767
|
+
noSleepCon = RunService.Heartbeat:Connect(function(deltaTime)
|
|
1768
|
+
voxel:ApplyImpulse(Vector3.yAxis * -0.00001)
|
|
1769
|
+
end)
|
|
1770
|
+
|
|
1771
|
+
local OldPuppetData = self.Puppets[self.PuppetNextIndex]
|
|
1772
|
+
|
|
1773
|
+
if OldPuppetData then
|
|
1774
|
+
OldPuppetData[3]:Disconnect()
|
|
1775
|
+
OldPuppetData[2](true)
|
|
1776
|
+
self:DestroyPart(OldPuppetData[1])
|
|
1777
|
+
end
|
|
1778
|
+
|
|
1779
|
+
table.insert(self.CreatePuppetQueue, voxel)
|
|
1780
|
+
local QueueInsertedIndex = #self.CreatePuppetQueue
|
|
1781
|
+
local PuppetInsertedIndex = self.PuppetNextIndex
|
|
1782
|
+
|
|
1783
|
+
local PuppetData = {
|
|
1784
|
+
voxel,
|
|
1785
|
+
function(doNotSignal)
|
|
1786
|
+
noSleepCon:Disconnect()
|
|
1787
|
+
|
|
1788
|
+
local found
|
|
1789
|
+
for i = 1, QueueInsertedIndex do
|
|
1790
|
+
if self.CreatePuppetQueue[i] == voxel then
|
|
1791
|
+
found = i
|
|
1792
|
+
break
|
|
1793
|
+
end
|
|
1794
|
+
end
|
|
1795
|
+
if found then table.remove(self.CreatePuppetQueue, found) end
|
|
1796
|
+
|
|
1797
|
+
if doNotSignal then return end
|
|
1798
|
+
|
|
1799
|
+
self.Puppets[PuppetInsertedIndex] = nil
|
|
1800
|
+
self.PuppetsLerpData[PuppetInsertedIndex] = nil
|
|
1801
|
+
self.ServerEvents.DestroyPuppet.FireAll(PuppetInsertedIndex)
|
|
1802
|
+
end
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
PuppetData[3] = voxel.Destroying:Once(PuppetData[2])
|
|
1806
|
+
PuppetData[4] = 0
|
|
1807
|
+
|
|
1808
|
+
self.Puppets[PuppetInsertedIndex] = PuppetData
|
|
1809
|
+
self.PuppetsLerpData[PuppetInsertedIndex] = {}
|
|
1810
|
+
self.PuppetNextIndex += 1
|
|
1811
|
+
if self.PuppetNextIndex > self:UseSetting('PuppetMaxCount') then
|
|
1812
|
+
self.PuppetNextIndex = 1
|
|
1813
|
+
end
|
|
1814
|
+
end
|
|
1815
|
+
|
|
1816
|
+
function Shatterbox:FakeClientPuppets(fakePuppets: {Part})
|
|
1817
|
+
for _, fake in ipairs(fakePuppets) do
|
|
1818
|
+
local OldPuppetData = self.Puppets[self.FakePuppetOverrideIndex]
|
|
1819
|
+
if OldPuppetData then
|
|
1820
|
+
OldPuppetData[1]:Destroy()
|
|
1821
|
+
self.PuppetsLerpData[self.FakePuppetOverrideIndex] = nil
|
|
1822
|
+
end
|
|
1823
|
+
|
|
1824
|
+
self.Puppets[self.FakePuppetOverrideIndex] = {fake}
|
|
1825
|
+
|
|
1826
|
+
self.FakePuppetOverrideIndex += 1
|
|
1827
|
+
if self.FakePuppetOverrideIndex > self:UseSetting('PuppetMaxCount') then
|
|
1828
|
+
self.FakePuppetOverrideIndex = 1
|
|
1829
|
+
end
|
|
1830
|
+
end
|
|
1831
|
+
end
|
|
1832
|
+
|
|
1833
|
+
function Shatterbox:Destroy(intersectingPart, FilterTagged, CleanupDelay, OnVoxelDestruct, GridSize, SkipEncapsulatedVoxels, OnDestructCompleted, UserData, ExcludePlayersReplication, SkipFloors, SkipWalls)
|
|
1834
|
+
coroutine.wrap(function()
|
|
1835
|
+
local did = self:ModelQueueBehavior(Shatterbox.Destroy, intersectingPart, FilterTagged, CleanupDelay, OnVoxelDestruct, GridSize, SkipEncapsulatedVoxels, OnDestructCompleted, UserData, ExcludePlayersReplication, SkipFloors, SkipWalls)
|
|
1836
|
+
if did then return end
|
|
1837
|
+
|
|
1838
|
+
local params = self:ValidateDestructionParams(intersectingPart, FilterTagged, CleanupDelay, OnVoxelDestruct, GridSize, SkipEncapsulatedVoxels, OnDestructCompleted, UserData, ExcludePlayersReplication, SkipFloors, SkipWalls)
|
|
1839
|
+
|
|
1840
|
+
params.DestroyedVoxels = 0
|
|
1841
|
+
|
|
1842
|
+
local destructibleParts, createdDirtyGroups, createdFallingGroups = self:GetDestructibleParts(params)
|
|
1843
|
+
if #destructibleParts == 0 then return end
|
|
1844
|
+
|
|
1845
|
+
local DestructionID = params.ID
|
|
1846
|
+
|
|
1847
|
+
local AffectedDirtyGroups = {}
|
|
1848
|
+
for _, part in ipairs(destructibleParts) do
|
|
1849
|
+
local DirtyGroupID = part:GetAttribute(DirtyGroupIDAttribute)
|
|
1850
|
+
if AffectedDirtyGroups[DirtyGroupID] then continue end
|
|
1851
|
+
AffectedDirtyGroups[DirtyGroupID] = true
|
|
1852
|
+
self.CountOperationsProcessing[DirtyGroupID] = (self.CountOperationsProcessing[DirtyGroupID] or 0) + 1
|
|
1853
|
+
end
|
|
1854
|
+
self.OperationAffectedDirtyGroups[DestructionID] = AffectedDirtyGroups
|
|
1855
|
+
self.OperationBlacklistedVoxels[DestructionID] = {}
|
|
1856
|
+
|
|
1857
|
+
if self.USE_CLIENT_SERVER and isServer then
|
|
1858
|
+
self:ReplicateDestructionToClients(NORMAL, params, createdDirtyGroups)
|
|
1859
|
+
end
|
|
1860
|
+
|
|
1861
|
+
table.insert(self.ShatterQueue, params)
|
|
1862
|
+
end)()
|
|
1863
|
+
end
|
|
1864
|
+
|
|
1865
|
+
function Shatterbox:ImaginaryVoxels(intersectingPart, FilterTagged, CleanupDelay, GridSize, SkipEncapsulatedVoxels, ExcludePlayersReplication, SkipFloors, SkipWalls)
|
|
1866
|
+
local did, voxels, existingDebris = self:ModelQueueBehavior(Shatterbox.ImaginaryVoxels, intersectingPart, FilterTagged, CleanupDelay, nil, GridSize, SkipEncapsulatedVoxels, nil, nil, ExcludePlayersReplication, SkipFloors, SkipWalls)
|
|
1867
|
+
|
|
1868
|
+
if did then return voxels, existingDebris end
|
|
1869
|
+
|
|
1870
|
+
local params = self:ValidateDestructionParams(intersectingPart, FilterTagged, CleanupDelay, nil, GridSize, SkipEncapsulatedVoxels, nil, nil, ExcludePlayersReplication, SkipFloors, SkipWalls)
|
|
1871
|
+
|
|
1872
|
+
local destructibleParts, createdDirtyGroups, createdFallingGroups = self:GetDestructibleParts(params)
|
|
1873
|
+
if #destructibleParts == 0 then return {}, {} end
|
|
1874
|
+
|
|
1875
|
+
if self.USE_CLIENT_SERVER and isServer then
|
|
1876
|
+
self:ReplicateDestructionToClients(IMAGINARY, params, createdDirtyGroups)
|
|
1877
|
+
end
|
|
1878
|
+
|
|
1879
|
+
voxels, existingDebris = self:ImaginaryVoxels_Internal(params, false, destructibleParts)
|
|
1880
|
+
|
|
1881
|
+
self.OperationAffectedFallingGroups[params.ID] = nil
|
|
1882
|
+
|
|
1883
|
+
return voxels, existingDebris
|
|
1884
|
+
end
|
|
1885
|
+
|
|
1886
|
+
function Shatterbox:InstantiateImaginaryVoxel(ImaginaryVoxel, doNotGiveDebrisTag: boolean?)
|
|
1887
|
+
local DirtyGroupID = ImaginaryVoxel.DirtyGroupID
|
|
1888
|
+
|
|
1889
|
+
local voxel = self:InstantiatePart(DirtyGroupID, ImaginaryVoxel.CFrame, ImaginaryVoxel.Size)
|
|
1890
|
+
|
|
1891
|
+
if not doNotGiveDebrisTag then voxel:AddTag(DebrisTag) end
|
|
1892
|
+
|
|
1893
|
+
voxel.Destroying:Once(function()
|
|
1894
|
+
local members = self.DirtyGroupMembers[DirtyGroupID]
|
|
1895
|
+
if not members then return end
|
|
1896
|
+
members[voxel] = nil
|
|
1897
|
+
end)
|
|
1898
|
+
|
|
1899
|
+
voxel.Parent = self.DirtyGroupData[DirtyGroupID].OriginalParent
|
|
1900
|
+
|
|
1901
|
+
return voxel
|
|
1902
|
+
end
|
|
1903
|
+
|
|
1904
|
+
function Shatterbox:ResetArea(area)
|
|
1905
|
+
local ACF, AS = area.CFrame, area.Size
|
|
1906
|
+
if isServer then self.ServerEvents.SignalResetArea.FireAll(ACF, AS) end
|
|
1907
|
+
|
|
1908
|
+
local AreaVerts = VertexMath.GetVerts.Block(ACF, AS)
|
|
1909
|
+
local AreaNormals = VertexMath.GetNormals.Block(ACF)
|
|
1910
|
+
|
|
1911
|
+
local toResetIDs, toTerminate = {}, {}
|
|
1912
|
+
|
|
1913
|
+
for DirtyGroupID, groupData in pairs(self.DirtyGroupData) do
|
|
1914
|
+
local originalPart = groupData.OriginalPart
|
|
1915
|
+
local cpcf, cps, cpsh = originalPart.CFrame, originalPart.Size, originalPart.Shape.Name
|
|
1916
|
+
local iBl, iBa, iC = cpsh == "Block", cpsh == "Ball", cpsh == "Cylinder"
|
|
1917
|
+
|
|
1918
|
+
local intersectingVerts, intersectingNormals
|
|
1919
|
+
if iBl or iC then
|
|
1920
|
+
intersectingVerts = BlockVerts(cpcf, cps)
|
|
1921
|
+
intersectingNormals = BlockNormals(cpcf)
|
|
1922
|
+
elseif not iBa then
|
|
1923
|
+
intersectingVerts = GetVerts[cpsh](cpcf, cps)
|
|
1924
|
+
intersectingNormals = GetNormals[cpsh](cpcf, cps)
|
|
1925
|
+
end
|
|
1926
|
+
|
|
1927
|
+
local intersected = PartContainsAVert(originalPart, AreaVerts)
|
|
1928
|
+
if not intersected then
|
|
1929
|
+
if iBa then
|
|
1930
|
+
intersected = VertexMath.BallIntersectsBlock(cpcf, cps, ACF, AS)
|
|
1931
|
+
elseif iC then
|
|
1932
|
+
intersected = VertexMath.CylinderIntersectsBlock(cpcf, cps, intersectingVerts, intersectingNormals, ACF, AS, AreaVerts, AreaNormals)
|
|
1933
|
+
else
|
|
1934
|
+
intersected = VertexMath.SAT(intersectingVerts, intersectingNormals, AreaVerts, AreaNormals)
|
|
1935
|
+
end
|
|
1936
|
+
end
|
|
1937
|
+
|
|
1938
|
+
if intersected then
|
|
1939
|
+
local countProcessing = self.CountOperationsProcessing[DirtyGroupID]
|
|
1940
|
+
if countProcessing then
|
|
1941
|
+
local toCancel, countFound = {}, 0
|
|
1942
|
+
for DestructionID, AffectedIDs in pairs(self.OperationAffectedDirtyGroups) do
|
|
1943
|
+
if not AffectedIDs[DirtyGroupID] then continue end
|
|
1944
|
+
toCancel[DestructionID] = true
|
|
1945
|
+
countFound += 1
|
|
1946
|
+
if countFound == countProcessing then break end
|
|
1947
|
+
end
|
|
1948
|
+
for DestructionID in pairs(toCancel) do
|
|
1949
|
+
for i, destructionParams in ipairs(self.ShatterQueue) do
|
|
1950
|
+
if destructionParams.ID == DestructionID then
|
|
1951
|
+
table.remove(self.ShatterQueue, i)
|
|
1952
|
+
break
|
|
1953
|
+
end
|
|
1954
|
+
end
|
|
1955
|
+
self.OperationAffectedDirtyGroups[DestructionID] = nil
|
|
1956
|
+
end
|
|
1957
|
+
end
|
|
1958
|
+
|
|
1959
|
+
if self.GreedyWorkers[DirtyGroupID] then
|
|
1960
|
+
toTerminate[DirtyGroupID] = true
|
|
1961
|
+
else
|
|
1962
|
+
toResetIDs[DirtyGroupID] = true
|
|
1963
|
+
end
|
|
1964
|
+
end
|
|
1965
|
+
end
|
|
1966
|
+
|
|
1967
|
+
for DirtyGroupID in pairs(toResetIDs) do
|
|
1968
|
+
self:UndoDirtyGroup(DirtyGroupID)
|
|
1969
|
+
end
|
|
1970
|
+
|
|
1971
|
+
if next(toTerminate) then
|
|
1972
|
+
for DirtyGroupID in pairs(toTerminate) do
|
|
1973
|
+
self.GreedyWorkersTerminationFlags[DirtyGroupID] = true
|
|
1974
|
+
end
|
|
1975
|
+
while RunService.Heartbeat:Wait() do
|
|
1976
|
+
local allDone = true
|
|
1977
|
+
for DirtyGroupID in pairs(toTerminate) do
|
|
1978
|
+
if self.GreedyWorkers[DirtyGroupID] then
|
|
1979
|
+
allDone = false
|
|
1980
|
+
break
|
|
1981
|
+
end
|
|
1982
|
+
end
|
|
1983
|
+
if allDone then break end
|
|
1984
|
+
end
|
|
1985
|
+
end
|
|
1986
|
+
|
|
1987
|
+
for DirtyGroupID in pairs(toTerminate) do
|
|
1988
|
+
self:UndoDirtyGroup(DirtyGroupID)
|
|
1989
|
+
end
|
|
1990
|
+
end
|
|
1991
|
+
|
|
1992
|
+
function Shatterbox:Reset(doNotRevertOwnership: boolean?, replicated: boolean?)
|
|
1993
|
+
self.ShatterQueue = {}
|
|
1994
|
+
self.CountOperationsProcessing = {}
|
|
1995
|
+
self.OperationAffectedDirtyGroups = {}
|
|
1996
|
+
|
|
1997
|
+
for DirtyGroupID in pairs(self.GreedyWorkers) do
|
|
1998
|
+
self.GreedyWorkersTerminationFlags[DirtyGroupID] = true
|
|
1999
|
+
end
|
|
2000
|
+
while self.ActiveGMWorkerCount > 0 do RunService.Heartbeat:Wait() end
|
|
2001
|
+
|
|
2002
|
+
self.SmoothCleanupQueue = {}
|
|
2003
|
+
|
|
2004
|
+
if isServer then
|
|
2005
|
+
for _, puppetData in pairs(self.Puppets) do
|
|
2006
|
+
puppetData[3]:Disconnect()
|
|
2007
|
+
puppetData[2](true)
|
|
2008
|
+
self:DestroyPart(puppetData[1])
|
|
2009
|
+
end
|
|
2010
|
+
|
|
2011
|
+
self.ServerEvents.SignalReset.FireAll(doNotRevertOwnership)
|
|
2012
|
+
else
|
|
2013
|
+
for _, puppetData in pairs(self.Puppets) do
|
|
2014
|
+
puppetData[1]:Destroy()
|
|
2015
|
+
end
|
|
2016
|
+
end
|
|
2017
|
+
|
|
2018
|
+
if doNotRevertOwnership and self.USE_CLIENT_SERVER then
|
|
2019
|
+
for DirtyGroupID in pairs(self.DirtyGroupData) do
|
|
2020
|
+
self:UndoDirtyGroup(DirtyGroupID)
|
|
2021
|
+
end
|
|
2022
|
+
else
|
|
2023
|
+
for DirtyGroupID, groupData in pairs(self.DirtyGroupData) do
|
|
2024
|
+
local OriginalPart = groupData.OriginalPart
|
|
2025
|
+
if replicated then
|
|
2026
|
+
OriginalPart:Destroy()
|
|
2027
|
+
else
|
|
2028
|
+
OriginalPart:SetAttribute(DirtyGroupIDAttribute)
|
|
2029
|
+
OriginalPart.Parent = groupData.OriginalParent
|
|
2030
|
+
end
|
|
2031
|
+
for member in pairs(self.DirtyGroupMembers[DirtyGroupID]) do
|
|
2032
|
+
member:Destroy()
|
|
2033
|
+
end
|
|
2034
|
+
end
|
|
2035
|
+
|
|
2036
|
+
self.DirtyGroupData = {}
|
|
2037
|
+
self.DirtyGroupMembers = {}
|
|
2038
|
+
end
|
|
2039
|
+
|
|
2040
|
+
self.GreedyMeshingQueue = {}
|
|
2041
|
+
self.GreedyWorkers = {}
|
|
2042
|
+
self.GreedyWorkersTerminationFlags = {}
|
|
2043
|
+
self.ActiveGMWorkerCount = 0
|
|
2044
|
+
self.ActiveGMWorkerGridSize = {}
|
|
2045
|
+
|
|
2046
|
+
self.Puppets = {}
|
|
2047
|
+
self.PuppetsLerpData = {}
|
|
2048
|
+
self.PuppetNextIndex = 1
|
|
2049
|
+
self.PuppetNextReplicationIndex = 1
|
|
2050
|
+
self.CreatePuppetQueue = {}
|
|
2051
|
+
end
|
|
2052
|
+
|
|
2053
|
+
function Shatterbox:ClearQueue()
|
|
2054
|
+
self.ShatterQueue = {}
|
|
2055
|
+
|
|
2056
|
+
if isServer then self.ServerEvents.SignalClearQueue.FireAll() end
|
|
2057
|
+
end
|
|
2058
|
+
|
|
2059
|
+
function Shatterbox:CreateHitbox()
|
|
2060
|
+
local hitbox = {}
|
|
2061
|
+
hitbox.CFrame = CFrame.new()
|
|
2062
|
+
hitbox.Size = Vector3.new(1, 1, 1)
|
|
2063
|
+
hitbox.Shape = Enum.PartType.Ball
|
|
2064
|
+
|
|
2065
|
+
hitbox.DestructDelay = nil
|
|
2066
|
+
hitbox.FilterTagged = nil
|
|
2067
|
+
hitbox.OnVoxelDestruct = nil
|
|
2068
|
+
hitbox.UserData = {}
|
|
2069
|
+
hitbox.ExcludePlayersReplication = {}
|
|
2070
|
+
hitbox.OnDestructCompleted = nil
|
|
2071
|
+
hitbox.CleanupDelay = nil
|
|
2072
|
+
hitbox.GridSize = nil
|
|
2073
|
+
hitbox.SkipEncapsulatedVoxels = nil
|
|
2074
|
+
hitbox.SkipWalls = nil
|
|
2075
|
+
hitbox.SkipFloors = nil
|
|
2076
|
+
hitbox.DestructionType = "DEFAULT"
|
|
2077
|
+
hitbox.ImaginaryCallback = nil
|
|
2078
|
+
hitbox.StartConnectionEvent = RunService.Heartbeat
|
|
2079
|
+
hitbox.WeldConnectionEvent = RunService.PostSimulation
|
|
2080
|
+
hitbox.VelocityPrediction = false
|
|
2081
|
+
hitbox.VelocityBias = 1
|
|
2082
|
+
|
|
2083
|
+
local _selfRef = self
|
|
2084
|
+
function hitbox:Destroy()
|
|
2085
|
+
_selfRef:Destroy(hitbox)
|
|
2086
|
+
hitbox.ExcludePlayersReplication = {}
|
|
2087
|
+
end
|
|
2088
|
+
|
|
2089
|
+
function hitbox:ImaginaryVoxels()
|
|
2090
|
+
local voxels, existingDebris = self:ImaginaryVoxels(hitbox)
|
|
2091
|
+
hitbox.ExcludePlayersReplication = {}
|
|
2092
|
+
return voxels, existingDebris
|
|
2093
|
+
end
|
|
2094
|
+
|
|
2095
|
+
local wcon, dcon
|
|
2096
|
+
|
|
2097
|
+
function hitbox:WeldTo(Part: BasePart)
|
|
2098
|
+
if wcon then wcon:Disconnect() end
|
|
2099
|
+
if dcon then dcon:Disconnect() end
|
|
2100
|
+
hitbox.CFrame = Part.CFrame
|
|
2101
|
+
wcon = hitbox.WeldConnectionEvent:Connect(function(deltaTime)
|
|
2102
|
+
if hitbox.VelocityPrediction then
|
|
2103
|
+
hitbox.CFrame = Part.CFrame + Part.AssemblyLinearVelocity * hitbox.VelocityBias * deltaTime
|
|
2104
|
+
else
|
|
2105
|
+
hitbox.CFrame = Part.CFrame
|
|
2106
|
+
end
|
|
2107
|
+
end)
|
|
2108
|
+
dcon = Part.Destroying:Once(hitbox.DestroyHitbox)
|
|
2109
|
+
end
|
|
2110
|
+
|
|
2111
|
+
function hitbox:Unweld()
|
|
2112
|
+
if wcon then wcon:Disconnect() end
|
|
2113
|
+
if dcon then dcon:Disconnect() end
|
|
2114
|
+
wcon, dcon = nil, nil
|
|
2115
|
+
end
|
|
2116
|
+
|
|
2117
|
+
local ccon
|
|
2118
|
+
local lastCut
|
|
2119
|
+
|
|
2120
|
+
local function DestructionStartConnection()
|
|
2121
|
+
if hitbox.DestructDelay then
|
|
2122
|
+
local t = time()
|
|
2123
|
+
if lastCut and t - lastCut < hitbox.DestructDelay then return end
|
|
2124
|
+
lastCut = t
|
|
2125
|
+
end
|
|
2126
|
+
|
|
2127
|
+
if hitbox.DestructionType == "DEFAULT" then
|
|
2128
|
+
hitbox:Destroy()
|
|
2129
|
+
else
|
|
2130
|
+
local voxels, existingDebris = hitbox:ImaginaryVoxels()
|
|
2131
|
+
if hitbox.ImaginaryCallback then
|
|
2132
|
+
hitbox.ImaginaryCallback(voxels, existingDebris)
|
|
2133
|
+
end
|
|
2134
|
+
end
|
|
2135
|
+
end
|
|
2136
|
+
|
|
2137
|
+
function hitbox:Start()
|
|
2138
|
+
if ccon then return end
|
|
2139
|
+
ccon = hitbox.StartConnectionEvent:Connect(DestructionStartConnection)
|
|
2140
|
+
end
|
|
2141
|
+
|
|
2142
|
+
function hitbox:Stop()
|
|
2143
|
+
if ccon then ccon:Disconnect() end
|
|
2144
|
+
ccon = nil
|
|
2145
|
+
end
|
|
2146
|
+
|
|
2147
|
+
function hitbox:DestroyHitbox()
|
|
2148
|
+
hitbox:Unweld()
|
|
2149
|
+
hitbox:Stop()
|
|
2150
|
+
end
|
|
2151
|
+
|
|
2152
|
+
return hitbox
|
|
2153
|
+
end
|
|
2154
|
+
|
|
2155
|
+
function Shatterbox:GetOriginalPart(DirtyGroupID: string)
|
|
2156
|
+
local groupData = self.DirtyGroupData[DirtyGroupID]
|
|
2157
|
+
if not groupData then return nil end
|
|
2158
|
+
return groupData.OriginalPart
|
|
2159
|
+
end
|
|
2160
|
+
|
|
2161
|
+
function Shatterbox:VoxelDistanceVector(voxel, point: Vector3): Vector3
|
|
2162
|
+
return voxel.CFrame:PointToObjectSpace(point):Abs() / voxel.Size
|
|
2163
|
+
end
|
|
2164
|
+
|
|
2165
|
+
function Shatterbox:VoxelCountVector(voxel, boxSize: Vector3): Vector3
|
|
2166
|
+
return Vector3.one:Max(boxSize / voxel.Size)
|
|
2167
|
+
end
|
|
2168
|
+
|
|
2169
|
+
function Shatterbox:PartEncapsulatesBlockPart(part: Part, contains: Part)
|
|
2170
|
+
assert(part:IsA("Part"), "You can only use this function when a Part is the container.")
|
|
2171
|
+
assert(contains:IsA("Part") and contains.Shape == Enum.PartType.Block, "You can only use this function when a Block Part is the contained object.")
|
|
2172
|
+
return PartEncapsulatesBlockPart(part, contains.CFrame, contains.Size)
|
|
2173
|
+
end
|
|
2174
|
+
|
|
2175
|
+
function Shatterbox:RegisterOnVoxelDestruct(name: string, callback)
|
|
2176
|
+
assert(typeof(name) == "string", "You must register the OnVoxelDestruct callback with a string as the name")
|
|
2177
|
+
local id = name:lower()
|
|
2178
|
+
assert(not rawget(self.OnVoxelDestructRegistry, id), `Callback with name {id} already exists in the registry`)
|
|
2179
|
+
self.OnVoxelDestructRegistry[id] = callback
|
|
2180
|
+
end
|
|
2181
|
+
|
|
2182
|
+
function Shatterbox:GetVoxelDestructionEffect(name: string)
|
|
2183
|
+
assert(typeof(name) == "string", "Voxel destruction effect names must be a string.")
|
|
2184
|
+
|
|
2185
|
+
local id = name:lower()
|
|
2186
|
+
assert(rawget(self.OnVoxelDestructRegistry, id), `No registered voxel destruction behavior "{id}".`)
|
|
2187
|
+
|
|
2188
|
+
return self.OnVoxelDestructRegistry[id]
|
|
2189
|
+
end
|
|
2190
|
+
|
|
2191
|
+
|
|
2192
|
+
function Shatterbox:PrintState()
|
|
2193
|
+
if isServer then self.ServerEvents.SignalPrintState.FireAll() end
|
|
2194
|
+
|
|
2195
|
+
local P = "\n|\n| "
|
|
2196
|
+
print(P .. `SHATTERBOX ({isServer and "SERVER" or "CLIENT"})`)
|
|
2197
|
+
print(P .. "ShatterQueue length: " .. #self.ShatterQueue)
|
|
2198
|
+
print(P .. "ActiveGMWorkerCount: " .. self.ActiveGMWorkerCount)
|
|
2199
|
+
end
|
|
2200
|
+
|
|
2201
|
+
--[[ STARTUP ]]--
|
|
2202
|
+
|
|
2203
|
+
function Shatterbox.Start(self: Shatterbox)
|
|
2204
|
+
if self.started then
|
|
2205
|
+
warn('Cannot start shatterbox more than once.')
|
|
2206
|
+
return
|
|
2207
|
+
end
|
|
2208
|
+
|
|
2209
|
+
RunService.Heartbeat:Connect(function(deltaTime)
|
|
2210
|
+
self:HeartbeatWorker(deltaTime)
|
|
2211
|
+
end)
|
|
2212
|
+
|
|
2213
|
+
local effects = require(src.Effects)
|
|
2214
|
+
effects._SetRef(self)
|
|
2215
|
+
effects._SetRef = nil
|
|
2216
|
+
|
|
2217
|
+
if isServer then
|
|
2218
|
+
self.ServerEvents = require(PathToServer).Shatterbox
|
|
2219
|
+
self.mapContainer = workspace.CurrentCamera
|
|
2220
|
+
|
|
2221
|
+
if self.USE_CLIENT_SERVER then
|
|
2222
|
+
self.ServerEvents.ClientInitComplete.On(function(plr)
|
|
2223
|
+
self:ReplicateStateToPlayer(plr)
|
|
2224
|
+
end)
|
|
2225
|
+
|
|
2226
|
+
self.ServerEvents.CreateDirtyGroupsFromClient.On(function(plr, DirtyGroups)
|
|
2227
|
+
local SerialDirtyGroups = {}
|
|
2228
|
+
for DirtyGroupID, part in pairs(DirtyGroups) do
|
|
2229
|
+
self:PopulateDirtyGroup(part, DirtyGroupID)
|
|
2230
|
+
SerialDirtyGroups[DirtyGroupID] = SerializedPartInstance(part)
|
|
2231
|
+
end
|
|
2232
|
+
|
|
2233
|
+
self.ServerEvents.CreateDirtyGroupsFromServer.FireExcept(plr, SerialDirtyGroups)
|
|
2234
|
+
end)
|
|
2235
|
+
end
|
|
2236
|
+
|
|
2237
|
+
elseif isClient then
|
|
2238
|
+
self.ClientEvents = require(PathToClient).Shatterbox
|
|
2239
|
+
|
|
2240
|
+
self.mapContainer = workspace:FindFirstChild("ClientMap")
|
|
2241
|
+
if not self.mapContainer then
|
|
2242
|
+
self.mapContainer = Instance.new("Folder")
|
|
2243
|
+
self.mapContainer.Parent = workspace
|
|
2244
|
+
self.mapContainer.Name = "ClientMap"
|
|
2245
|
+
end
|
|
2246
|
+
|
|
2247
|
+
if self.USE_CLIENT_SERVER then
|
|
2248
|
+
self.ClientEvents.ReplicateState.On(function(...)
|
|
2249
|
+
self:ReplicateStateFromServer(...)
|
|
2250
|
+
end)
|
|
2251
|
+
|
|
2252
|
+
self.ClientEvents.ReplicateDestruction.On(function(...)
|
|
2253
|
+
self:ReplicateDestructionFromServer(...)
|
|
2254
|
+
end)
|
|
2255
|
+
|
|
2256
|
+
self.ClientEvents.CreateDirtyGroupsFromServer.On(function(...)
|
|
2257
|
+
self:CreateSerialDirtyGroups(...)
|
|
2258
|
+
end)
|
|
2259
|
+
|
|
2260
|
+
self.ClientEvents.ClientInitComplete.Fire()
|
|
2261
|
+
end
|
|
2262
|
+
|
|
2263
|
+
self.ClientEvents.SignalClearQueue.On(function()
|
|
2264
|
+
self:ClearQueue()
|
|
2265
|
+
end)
|
|
2266
|
+
|
|
2267
|
+
self.ClientEvents.SignalReset.On(function(doNotRevertOwnership)
|
|
2268
|
+
self:Reset(doNotRevertOwnership, true)
|
|
2269
|
+
end)
|
|
2270
|
+
|
|
2271
|
+
self.ClientEvents.SignalResetArea.On(function(cframe, size)
|
|
2272
|
+
self:ResetArea({CFrame = cframe, Size = size})
|
|
2273
|
+
end)
|
|
2274
|
+
|
|
2275
|
+
self.ClientEvents.CreatePuppets.On(function(puppets)
|
|
2276
|
+
self:CreatePuppets(puppets)
|
|
2277
|
+
end)
|
|
2278
|
+
|
|
2279
|
+
self.ClientEvents.ReplicatePuppetStates.On(function(...)
|
|
2280
|
+
self:ReplicatePuppetStates(...)
|
|
2281
|
+
end)
|
|
2282
|
+
|
|
2283
|
+
self.ClientEvents.DestroyPuppet.On(function(index)
|
|
2284
|
+
self:DestroyPuppet(index)
|
|
2285
|
+
end)
|
|
2286
|
+
|
|
2287
|
+
self.ClientEvents.SignalPrintState.On(function()
|
|
2288
|
+
self:PrintState()
|
|
2289
|
+
end)
|
|
2290
|
+
|
|
2291
|
+
-- Client-side puppet interpolation
|
|
2292
|
+
RunService.PreRender:Connect(function(deltaTime)
|
|
2293
|
+
if not next(self.PuppetsLerpData) then return end
|
|
2294
|
+
|
|
2295
|
+
local P = self.ClientPuppetStepDelta / self:UseSetting('PuppetReplicationFrequency')
|
|
2296
|
+
local finishedLerps = {}
|
|
2297
|
+
|
|
2298
|
+
for index, lerpData in pairs(self.PuppetsLerpData) do
|
|
2299
|
+
local d = lerpData[3] + deltaTime
|
|
2300
|
+
if d >= P then
|
|
2301
|
+
table.insert(finishedLerps, index)
|
|
2302
|
+
self.Puppets[index][1].CFrame = lerpData[2]
|
|
2303
|
+
else
|
|
2304
|
+
self.Puppets[index][1].CFrame = lerpData[1]:Lerp(lerpData[2], d / P)
|
|
2305
|
+
lerpData[3] = d
|
|
2306
|
+
end
|
|
2307
|
+
end
|
|
2308
|
+
|
|
2309
|
+
for _, index in ipairs(finishedLerps) do
|
|
2310
|
+
self.PuppetsLerpData[index] = nil
|
|
2311
|
+
end
|
|
2312
|
+
end)
|
|
2313
|
+
end
|
|
2314
|
+
|
|
2315
|
+
self.started = true
|
|
2316
|
+
end
|
|
2317
|
+
|
|
2318
|
+
function Shatterbox.UseSetting<T>(self: Shatterbox, settingName: string): T
|
|
2319
|
+
return (self.Settings::any)[settingName] == nil and DefaultSettings[settingName] or (self.Settings::any)[settingName]
|
|
2320
|
+
end
|
|
2321
|
+
|
|
2322
|
+
-- [[ Types ]]
|
|
2323
|
+
|
|
2324
|
+
export type WorldInfo = {
|
|
2325
|
+
CFrame : CFrame,
|
|
2326
|
+
Size : Vector3,
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
export type Hitbox = WorldInfo & {
|
|
2330
|
+
Shape : Enum.PartType
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
export type DestroyedVoxelInfo = {
|
|
2334
|
+
DirtyGroupID : string,
|
|
2335
|
+
|
|
2336
|
+
CuttingPart : Hitbox,
|
|
2337
|
+
|
|
2338
|
+
IsEdge : boolean,
|
|
2339
|
+
IsAlreadyDebris : boolean,
|
|
2340
|
+
|
|
2341
|
+
UserData : {any}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
export type OnDestructCompleted = (DestroyedVoxelCount : number, AffectedDirtyGroupIDs : { [string] : boolean }) -> ()
|
|
2345
|
+
|
|
2346
|
+
export type OnVoxelDestruct = (voxel : Part, info : DestroyedVoxelInfo) -> ()
|
|
2347
|
+
|
|
2348
|
+
export type ImaginaryBox = WorldInfo & {
|
|
2349
|
+
|
|
2350
|
+
DirtyGroupID : string, -- the dirty group ID for this box
|
|
2351
|
+
GridSize : number -- the GridSize that was applied to this box
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
export type ImaginaryVoxel = ImaginaryBox & {
|
|
2355
|
+
|
|
2356
|
+
isEdge : boolean -- whether or not this box lies along the edge of the intersection
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
type SharedDestructionParams = {
|
|
2360
|
+
|
|
2361
|
+
FilterTagged : (string | {string})?,
|
|
2362
|
+
GridSize : number?,
|
|
2363
|
+
CleanupDelay : number?,
|
|
2364
|
+
SkipEncapsulatedVoxels : boolean?,
|
|
2365
|
+
SkipWalls : boolean?,
|
|
2366
|
+
SkipFloors : boolean?,
|
|
2367
|
+
ExcludePlayersReplication : {Player}?
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
type UsesOnVoxelDestruct = {
|
|
2371
|
+
OnVoxelDestruct : string?,
|
|
2372
|
+
DestructParameters : { any },
|
|
2373
|
+
|
|
2374
|
+
OnDestructCompleted : OnDestructCompleted?
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
export type InternalImaginaryParams = SharedDestructionParams & { CuttingPart : Hitbox }
|
|
2378
|
+
export type ImaginaryDestructionParams = SharedDestructionParams & {
|
|
2379
|
+
|
|
2380
|
+
CuttingPart : ((Part?) | (Model?) | (Hitbox?))?,
|
|
2381
|
+
|
|
2382
|
+
CFrame : CFrame?,
|
|
2383
|
+
Size : Vector3?,
|
|
2384
|
+
Shape : Enum.PartType?
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
export type InternalDestructionParams = InternalImaginaryParams & UsesOnVoxelDestruct
|
|
2388
|
+
export type DestructionParams = ImaginaryDestructionParams & UsesOnVoxelDestruct
|
|
2389
|
+
|
|
2390
|
+
export type ImaginaryVoxelArguments = {
|
|
2391
|
+
intersectingPart : Part | Model | WorldInfo | ImaginaryDestructionParams, FilterTagged : (string | {string})?, CleanupDelay : number?, GridSize : number?, SkipEncapsulatedVoxels : boolean?, ExcludePlayersReplication : {Player}?, SkipFloors : boolean?, SkipWalls : boolean?
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
export type DestructionTypes = 'IMAGINARY' | 'NORMAL'
|
|
2395
|
+
|
|
2396
|
+
export type Settings = {
|
|
2397
|
+
--[[
|
|
2398
|
+
DESTRUCTION SETTINGS
|
|
2399
|
+
|
|
2400
|
+
Configure the basic behavior of voxel destruction
|
|
2401
|
+
]]
|
|
2402
|
+
|
|
2403
|
+
-- Default size of each voxel in studs
|
|
2404
|
+
-- Smaller values create more detailed destruction but increase performance cost
|
|
2405
|
+
-- Default: 4
|
|
2406
|
+
DefaultGridSize: number?,
|
|
2407
|
+
|
|
2408
|
+
-- Default time in seconds before destroyed parts are restored
|
|
2409
|
+
-- Set to 0 to never restore destroyed parts
|
|
2410
|
+
-- Default: 0
|
|
2411
|
+
DefaultSmoothCleanupDelay: number?,
|
|
2412
|
+
|
|
2413
|
+
|
|
2414
|
+
--[[
|
|
2415
|
+
GREEDY MESHING SETTINGS
|
|
2416
|
+
|
|
2417
|
+
Greedy meshing combines adjacent voxels into larger parts to reduce part count
|
|
2418
|
+
This significantly improves performance for large destructions
|
|
2419
|
+
]]
|
|
2420
|
+
|
|
2421
|
+
-- Enable greedy meshing algorithm to combine adjacent voxels
|
|
2422
|
+
-- When enabled, adjacent voxels of the same material are merged into larger parts
|
|
2423
|
+
-- Default: true
|
|
2424
|
+
UseGreedyMeshing: boolean?,
|
|
2425
|
+
|
|
2426
|
+
-- Maximum number of concurrent greedy meshing worker threads
|
|
2427
|
+
-- Higher values = faster meshing but more CPU usage per frame
|
|
2428
|
+
-- Default: 2
|
|
2429
|
+
GMWorkerCount: number?,
|
|
2430
|
+
|
|
2431
|
+
-- Number of voxel grid traversals to perform per frame in greedy meshing
|
|
2432
|
+
-- Higher values = faster meshing but more frame time used
|
|
2433
|
+
-- Default: 10000
|
|
2434
|
+
GMTraversalsPerFrame: number?,
|
|
2435
|
+
|
|
2436
|
+
-- Number of parts to create per frame during greedy meshing
|
|
2437
|
+
-- Controls how smoothly the meshing displays results
|
|
2438
|
+
-- Default: 50
|
|
2439
|
+
GMPartCreationsPerFrame: number?,
|
|
2440
|
+
|
|
2441
|
+
|
|
2442
|
+
--[[
|
|
2443
|
+
PERFORMANCE SETTINGS
|
|
2444
|
+
|
|
2445
|
+
Control frame budget and processing limits
|
|
2446
|
+
]]
|
|
2447
|
+
|
|
2448
|
+
-- Maximum number of voxel subdivisions to process per frame
|
|
2449
|
+
-- Limits CPU time spent on destruction per frame
|
|
2450
|
+
-- Default: 500
|
|
2451
|
+
MaxDivisionsPerFrame: number?,
|
|
2452
|
+
|
|
2453
|
+
-- Maximum number of destruction operations to process per frame
|
|
2454
|
+
-- Each operation can involve multiple subdivisions
|
|
2455
|
+
-- Default: 5
|
|
2456
|
+
MaxOpsPerFrame: number?,
|
|
2457
|
+
|
|
2458
|
+
-- Enable priority queue for destruction operations
|
|
2459
|
+
-- When enabled, recent operations are prioritized
|
|
2460
|
+
-- Default: true
|
|
2461
|
+
UsePriorityQueue: boolean?,
|
|
2462
|
+
|
|
2463
|
+
-- Number of recent operations to prioritize when priority queue is enabled
|
|
2464
|
+
-- Higher values = more responsive to recent destructions
|
|
2465
|
+
-- Default: 5
|
|
2466
|
+
PrioritizeRecentN: number?,
|
|
2467
|
+
|
|
2468
|
+
|
|
2469
|
+
--[[
|
|
2470
|
+
SMOOTH CLEANUP SETTINGS
|
|
2471
|
+
|
|
2472
|
+
Configure automatic restoration of destroyed parts
|
|
2473
|
+
]]
|
|
2474
|
+
|
|
2475
|
+
-- Enable smooth cleanup system
|
|
2476
|
+
-- When enabled, destroyed parts are gradually restored over time
|
|
2477
|
+
-- Default: true
|
|
2478
|
+
UseSmoothCleanup: boolean?,
|
|
2479
|
+
|
|
2480
|
+
|
|
2481
|
+
--[[
|
|
2482
|
+
NON-DIVISIBLE PART SETTINGS
|
|
2483
|
+
|
|
2484
|
+
Control behavior for parts that cannot be subdivided
|
|
2485
|
+
(e.g., Balls, Cylinders, Wedges, or parts below minimum voxel size)
|
|
2486
|
+
]]
|
|
2487
|
+
|
|
2488
|
+
-- How to handle non-divisible parts when destroyed
|
|
2489
|
+
-- "NONE": Do nothing, leave the part as-is
|
|
2490
|
+
-- "FALL": Make the part fall with physics
|
|
2491
|
+
-- "REMOVE": Remove the part entirely
|
|
2492
|
+
-- Default: "NONE"
|
|
2493
|
+
NonDivisibleInteraction: ("NONE" | "FALL" | "REMOVE")?,
|
|
2494
|
+
|
|
2495
|
+
|
|
2496
|
+
--[[
|
|
2497
|
+
NETWORK SETTINGS
|
|
2498
|
+
|
|
2499
|
+
Configure client-server replication behavior
|
|
2500
|
+
]]
|
|
2501
|
+
|
|
2502
|
+
-- Enable client-server replication
|
|
2503
|
+
-- When enabled, destructions are processed on both server and client
|
|
2504
|
+
-- This provides lower latency and better visual feedback
|
|
2505
|
+
-- Default: true
|
|
2506
|
+
USE_CLIENT_SERVER: boolean?,
|
|
2507
|
+
|
|
2508
|
+
|
|
2509
|
+
--[[
|
|
2510
|
+
PUPPET SYSTEM SETTINGS
|
|
2511
|
+
|
|
2512
|
+
Puppets are falling voxels that are efficiently replicated to clients
|
|
2513
|
+
The system uses compressed state updates and client-side interpolation
|
|
2514
|
+
]]
|
|
2515
|
+
|
|
2516
|
+
-- Maximum number of concurrent puppet voxels
|
|
2517
|
+
-- When exceeded, oldest puppets are destroyed to make room for new ones
|
|
2518
|
+
-- Default: 500
|
|
2519
|
+
PuppetMaxCount: number?,
|
|
2520
|
+
|
|
2521
|
+
-- Frequency of puppet state updates in Hz
|
|
2522
|
+
-- Higher values = smoother motion but more network traffic
|
|
2523
|
+
-- Default: 20
|
|
2524
|
+
PuppetReplicationFrequency: number?,
|
|
2525
|
+
|
|
2526
|
+
-- Velocity threshold below which puppets are considered "sleeping"
|
|
2527
|
+
-- Sleeping puppets are automatically anchored after PuppetAnchorTimeout
|
|
2528
|
+
-- Default: 0.5
|
|
2529
|
+
PuppetSleepVelocity: number?,
|
|
2530
|
+
|
|
2531
|
+
-- Time in seconds before sleeping puppets are anchored
|
|
2532
|
+
-- Prevents tiny movements from keeping puppets active
|
|
2533
|
+
-- Default: 2
|
|
2534
|
+
PuppetAnchorTimeout: number?,
|
|
2535
|
+
|
|
2536
|
+
-- Enable client-side interpolation of puppet movement
|
|
2537
|
+
-- When enabled, puppets smoothly interpolate between server updates
|
|
2538
|
+
-- Default: true
|
|
2539
|
+
ClientTweenPuppets: boolean?,
|
|
2540
|
+
|
|
2541
|
+
-- Maximum distance from camera to use interpolation (in studs)
|
|
2542
|
+
-- Puppets beyond this distance are teleported instead of interpolated
|
|
2543
|
+
-- This saves performance on distant puppets
|
|
2544
|
+
-- Default: 200
|
|
2545
|
+
ClientTweenDistanceLimit: number?,
|
|
2546
|
+
|
|
2547
|
+
|
|
2548
|
+
--[[
|
|
2549
|
+
CUSTOM FILTER SETTINGS
|
|
2550
|
+
|
|
2551
|
+
Provide custom logic for skipping certain instances
|
|
2552
|
+
]]
|
|
2553
|
+
|
|
2554
|
+
-- Custom function to determine if an instance should be skipped
|
|
2555
|
+
-- Return true to skip the instance (don't allow destruction)
|
|
2556
|
+
-- Return false to allow the instance to be destroyed
|
|
2557
|
+
--
|
|
2558
|
+
-- Default behavior (if not provided):
|
|
2559
|
+
-- - Skip anything with "Indestructible" tag
|
|
2560
|
+
-- - Skip anything with a Humanoid ancestor
|
|
2561
|
+
-- - Skip non-Block parts on client
|
|
2562
|
+
--
|
|
2563
|
+
-- Example:
|
|
2564
|
+
-- SkipInstanceCheck = function(instance)
|
|
2565
|
+
-- -- Skip parts named "Important"
|
|
2566
|
+
-- if instance.Name == "Important" then
|
|
2567
|
+
-- return true
|
|
2568
|
+
-- end
|
|
2569
|
+
-- -- Skip parts in the "SafeZone" folder
|
|
2570
|
+
-- if instance:IsDescendantOf(workspace.SafeZone) then
|
|
2571
|
+
-- return true
|
|
2572
|
+
-- end
|
|
2573
|
+
-- return false
|
|
2574
|
+
-- end
|
|
2575
|
+
SkipInstanceCheck: ((instance: Instance) -> boolean)?
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
export type Shatterbox = { [string]: any } & typeof(Shatterbox)
|
|
2579
|
+
|
|
2580
|
+
return Shatterbox
|