@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/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