+1
-1
default.project.json
+1
-1
default.project.json
+45
-10
src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/init.lua
+45
-10
src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/init.lua
···
3
3
4
4
local TerrainGen = {}
5
5
6
-
local deflate = require("./TerrainGen/Deflate")
7
-
8
-
local DSS = game:GetService("DataStoreService")
9
-
local WORLDNAME = "DEFAULT"
10
-
local WORLDID = "b73bb5a6-297d-4352-b637-daec7e8c8f3e"
11
-
local Store = DSS:GetDataStore("BlockscraftWorldV1", WORLDID)
12
-
13
6
local ChunkManager = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared").ChunkManager)
14
7
local Chunk = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared").ChunkManager.Chunk)
15
8
16
9
TerrainGen.ServerChunkCache = {} :: {[typeof("")]: typeof(Chunk.new(0,0,0))}
17
10
11
+
local function chunkKeyFromCoords(x: number, y: number, z: number): string
12
+
return `{x},{y},{z}`
13
+
end
14
+
15
+
function TerrainGen:UnloadAllChunks(): number
16
+
local count = 0
17
+
for key in pairs(TerrainGen.ServerChunkCache) do
18
+
TerrainGen.ServerChunkCache[key] = nil
19
+
count += 1
20
+
end
21
+
return count
22
+
end
23
+
24
+
local function worldToChunkCoord(v: number): number
25
+
return math.floor((v + 16) / 32)
26
+
end
27
+
28
+
function TerrainGen:PreloadNearPlayers(radius: number, yRadius: number?): number
29
+
local Players = game:GetService("Players")
30
+
local r = radius or 5
31
+
local ry = yRadius or 1
32
+
local loaded = 0
33
+
for _, player in ipairs(Players:GetPlayers()) do
34
+
local character = player.Character
35
+
local root = character and character:FindFirstChild("HumanoidRootPart")
36
+
if root then
37
+
local pos = root.Position
38
+
local cx = worldToChunkCoord(pos.X)
39
+
local cy = worldToChunkCoord(pos.Y)
40
+
local cz = worldToChunkCoord(pos.Z)
41
+
for y = -ry, ry do
42
+
for x = -r, r do
43
+
for z = -r, r do
44
+
TerrainGen:GetChunk(cx + x, cy + y, cz + z)
45
+
loaded += 1
46
+
end
47
+
end
48
+
end
49
+
end
50
+
end
51
+
return loaded
52
+
end
53
+
18
54
-- Load a chunk from the DataStore or generate it if not found
19
55
function TerrainGen:GetChunk(x, y, z)
20
-
local key = `{x},{y},{z}`
56
+
local key = chunkKeyFromCoords(x, y, z)
21
57
if TerrainGen.ServerChunkCache[key] then
22
58
return TerrainGen.ServerChunkCache[key]
23
59
end
24
-
60
+
25
61
-- Generate a new chunk if it doesn't exist
26
62
local chunk = Chunk.new(x, y, z)
27
63
if y == 1 then
···
66
102
67
103
return chunk
68
104
end
69
-
70
105
71
106
TerrainGen.CM = ChunkManager
72
107
+17
-10
src/ServerScriptService/Actor/ServerChunkManager/init.server.lua
+17
-10
src/ServerScriptService/Actor/ServerChunkManager/init.server.lua
···
138
138
return false
139
139
end
140
140
141
-
local DEBUG_PLACEMENT = true
141
+
local DEBUG_PLACEMENT = false
142
+
local function debugPlacementLog(...: any)
143
+
if DEBUG_PLACEMENT then
144
+
Util.StudioLog(...)
145
+
end
146
+
end
147
+
148
+
local function debugPlacementWarn(...: any)
149
+
if DEBUG_PLACEMENT then
150
+
Util.StudioWarn(...)
151
+
end
152
+
end
142
153
143
154
placeRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z, blockId)
144
155
local function reject(reason: string)
145
-
if DEBUG_PLACEMENT then
146
-
warn("[PLACE][REJECT]", player.Name, reason, "chunk", cx, cy, cz, "block", x, y, z, "id", blockId)
147
-
end
156
+
debugPlacementWarn("[PLACE][REJECT]", player.Name, reason, "chunk", cx, cy, cz, "block", x, y, z, "id", blockId)
148
157
return
149
158
end
150
159
···
178
187
if existing and existing.id and existing.id ~= 0 then
179
188
if existing.id == resolvedId then
180
189
-- same block already there; treat as success without changes
181
-
if DEBUG_PLACEMENT then
182
-
print("[PLACE][OK][NOOP]", player.Name, "chunk", cx, cy, cz, "block", x, y, z, "id", resolvedId)
183
-
end
190
+
debugPlacementLog("[PLACE][OK][NOOP]", player.Name, "chunk", cx, cy, cz, "block", x, y, z, "id", resolvedId)
184
191
return
185
192
end
186
193
-- allow replacement when different id: remove then place
···
192
199
}
193
200
chunk:CreateBlock(x, y, z, data)
194
201
propogate("B_C", cx, cy, cz, x, y, z, data)
195
-
if DEBUG_PLACEMENT then
196
-
print("[PLACE][OK]", player.Name, "chunk", cx, cy, cz, "block", x, y, z, "id", resolvedId)
197
-
end
202
+
debugPlacementLog("[PLACE][OK]", player.Name, "chunk", cx, cy, cz, "block", x, y, z, "id", resolvedId)
198
203
end)
199
204
200
205
breakRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z)
···
219
224
task.synchronize()
220
225
tickRemote:FireClient(player, "C_R", cx, cy, cz, 0, 0, 0, 0)
221
226
task.desynchronize()
227
+
debugPlacementLog("[BREAK][NOOP]", player.Name, "chunk", cx, cy, cz, "block", x, y, z)
222
228
return
223
229
end
224
230
chunk:RemoveBlock(x, y, z)
225
231
propogate("B_D", cx, cy, cz, x, y, z, 0)
232
+
debugPlacementLog("[BREAK][OK]", player.Name, "chunk", cx, cy, cz, "block", x, y, z)
226
233
end)
227
234
228
235
task.desynchronize()
+79
src/ServerScriptService/CmdrCommands/ChunkCullWorld.lua
+79
src/ServerScriptService/CmdrCommands/ChunkCullWorld.lua
···
1
+
return {
2
+
Name = "chunkcull",
3
+
Aliases = {"cullchunks", "resetchunks"},
4
+
Description = "Unload all server chunk cache instantly, then preload only chunks near players (and force clients to unload/resync).",
5
+
Group = "Admin",
6
+
Args = {
7
+
{
8
+
Type = "integer",
9
+
Name = "radius",
10
+
Description = "Horizontal chunk radius around each player to preload",
11
+
Optional = true,
12
+
Default = 5,
13
+
},
14
+
{
15
+
Type = "integer",
16
+
Name = "yRadius",
17
+
Description = "Vertical chunk radius around each player to preload",
18
+
Optional = true,
19
+
Default = 1,
20
+
},
21
+
},
22
+
Run = function(context, radius, yRadius)
23
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")
24
+
local Players = game:GetService("Players")
25
+
26
+
local terrainGen = require(
27
+
game:GetService("ServerScriptService")
28
+
:WaitForChild("Actor")
29
+
:WaitForChild("ServerChunkManager")
30
+
:WaitForChild("TerrainGen")
31
+
)
32
+
33
+
local tickRemote = ReplicatedStorage:WaitForChild("Tick")
34
+
35
+
local r = radius or 5
36
+
local ry = yRadius or 1
37
+
38
+
local unloaded = 0
39
+
pcall(function()
40
+
unloaded = terrainGen:UnloadAllChunks()
41
+
end)
42
+
43
+
-- Tell all clients to immediately drop their local chunk instances
44
+
pcall(function()
45
+
tickRemote:FireAllClients("U_ALL", 0, 0, 0, 0, 0, 0, 0)
46
+
end)
47
+
48
+
-- Preload server chunks around players (reduces initial lag spikes after cull)
49
+
local preloaded = 0
50
+
pcall(function()
51
+
preloaded = terrainGen:PreloadNearPlayers(r, ry)
52
+
end)
53
+
54
+
-- Force clients to resync around themselves
55
+
local resyncCount = 0
56
+
for _, player in ipairs(Players:GetPlayers()) do
57
+
local character = player.Character
58
+
local root = character and character:FindFirstChild("HumanoidRootPart")
59
+
if root then
60
+
local pos = root.Position
61
+
local cx = math.floor((pos.X + 16) / 32)
62
+
local cy = math.floor((pos.Y + 16) / 32)
63
+
local cz = math.floor((pos.Z + 16) / 32)
64
+
for y = -ry, ry do
65
+
for x = -r, r do
66
+
for z = -r, r do
67
+
tickRemote:FireClient(player, "C_R", cx + x, cy + y, cz + z, 0, 0, 0, 0)
68
+
resyncCount += 1
69
+
end
70
+
end
71
+
end
72
+
end
73
+
end
74
+
75
+
return (
76
+
"chunkcull done | unloaded=%d | preloaded=%d | resyncPackets=%d | radius=%d yRadius=%d"
77
+
):format(unloaded, preloaded, resyncCount, r, ry)
78
+
end,
79
+
}
+35
-5
src/StarterGui/Hotbar/LocalScript.client.lua
+35
-5
src/StarterGui/Hotbar/LocalScript.client.lua
···
15
15
local PM = require(ReplicatedStorage.Shared.PlacementManager)
16
16
local BlockManager = require(ReplicatedStorage.Shared.ChunkManager.BlockManager)
17
17
local PlacementState = require(ReplicatedStorage.Shared.PlacementState)
18
+
local Util = require(ReplicatedStorage.Shared.Util)
18
19
19
20
local blocksFolder = ReplicatedStorage:WaitForChild("Blocks")
20
21
···
56
57
for _, block in ipairs(blocksFolder:GetChildren()) do
57
58
local id = block:GetAttribute("n")
58
59
if id ~= nil then
59
-
local idStr = tostring(id)
60
-
table.insert(ids, idStr)
61
-
names[idStr] = block:GetAttribute("displayName") or block:GetAttribute("dn") or block.Name
60
+
local n = tonumber(id)
61
+
if n and n > 0 then
62
+
local idStr = tostring(n)
63
+
table.insert(ids, idStr)
64
+
names[idStr] = block:GetAttribute("displayName") or block:GetAttribute("dn") or block.Name
65
+
end
62
66
end
63
67
end
64
68
table.sort(ids, function(a, b)
···
133
137
local slots, names = buildHotbarIds()
134
138
self.state.slots = slots
135
139
self.state.names = names
140
+
local initialId = slots and slots[1] or ""
141
+
if initialId and initialId ~= "" then
142
+
local initialName = names and (names[initialId] or initialId) or initialId
143
+
PlacementState:SetSelected(initialId, initialName)
144
+
end
136
145
137
146
self._updateSlots = function()
138
147
local nextSlots, nextNames = buildHotbarIds()
···
154
163
if id ~= "" and self.state.names then
155
164
name = self.state.names[id] or id
156
165
end
166
+
Util.StudioLog("[PLACE][CLIENT][SELECT]", "slot", slot, "id", id, "name", name)
157
167
PlacementState:SetSelected(id, name)
158
168
end
159
169
160
170
self._handleInput = function(input: InputObject, gameProcessedEvent: boolean)
161
-
if gameProcessedEvent or isTextInputFocused() then
171
+
if isTextInputFocused() then
162
172
return
163
173
end
164
174
165
175
local slot = keyToSlot[input.KeyCode]
166
176
if slot then
177
+
if gameProcessedEvent then
178
+
return
179
+
end
167
180
self._setSelected(slot)
168
181
return
169
182
end
170
183
171
184
if input.UserInputType == Enum.UserInputType.MouseButton1 then
185
+
Util.StudioLog("[INPUT][CLIENT]", "MouseButton1", "processed", gameProcessedEvent)
186
+
-- Allow click even if gameProcessedEvent (UI can set this), but only if we're actually pointing at a block
187
+
if not PM:GetBlockAtMouse() then
188
+
return
189
+
end
172
190
local mouseBlock = PM:GetBlockAtMouse()
173
191
if not mouseBlock then
174
192
return
···
182
200
mouseBlock.block.Z
183
201
)
184
202
elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
185
-
local mouseBlock = PM:GetPlacementAtMouse()
203
+
Util.StudioLog("[INPUT][CLIENT]", "MouseButton2", "processed", gameProcessedEvent)
204
+
-- Allow click even if gameProcessedEvent (UI can set this), but only if we're actually pointing at a block
205
+
local mouseBlock = PM:DebugGetPlacementOrWarn()
186
206
if not mouseBlock then
187
207
return
188
208
end
189
209
local id = PlacementState:GetSelected()
190
210
if not id or id == "" then
211
+
Util.StudioWarn("[PLACE][CLIENT][REJECT]", "no selected id")
191
212
return
192
213
end
214
+
Util.StudioLog(
215
+
"[PLACE][CLIENT][SEND][CLICK]",
216
+
"chunk",
217
+
mouseBlock.chunk,
218
+
"block",
219
+
mouseBlock.block,
220
+
"id",
221
+
id
222
+
)
193
223
PM:PlaceBlock(
194
224
mouseBlock.chunk.X,
195
225
mouseBlock.chunk.Y,
History
1 round
0 comments
kris.darkworld.download
submitted
#0
expand 0 comments
pull request successfully merged