Minecraft-like Roblox block game rblx.games/135624152691584
roblox roblox-game rojo

chore: init

+4016
+35
default.project.json
··· 1 + { 2 + "name": "project", 3 + "tree": { 4 + "$className": "DataModel", 5 + "ReplicatedStorage": { 6 + "$className": "ReplicatedStorage", 7 + "$ignoreUnknownInstances": true, 8 + "$path": "src/ReplicatedStorage" 9 + }, 10 + "ServerScriptService": { 11 + "$className": "ServerScriptService", 12 + "$ignoreUnknownInstances": true, 13 + "$path": "src/ServerScriptService" 14 + }, 15 + "StarterGui": { 16 + "$className": "StarterGui", 17 + "$ignoreUnknownInstances": true, 18 + "$path": "src/StarterGui" 19 + }, 20 + "StarterPlayer": { 21 + "$className": "StarterPlayer", 22 + "StarterPlayerScripts": { 23 + "$className": "StarterPlayerScripts", 24 + "$ignoreUnknownInstances": true, 25 + "$path": "src/StarterPlayer/StarterPlayerScripts" 26 + }, 27 + "$ignoreUnknownInstances": true 28 + }, 29 + "Workspace": { 30 + "$className": "Workspace", 31 + "$ignoreUnknownInstances": true, 32 + "$path": "src/Workspace" 33 + } 34 + } 35 + }
+86
src/ReplicatedStorage/Shared/ChunkManager/BlockManager.lua
··· 1 + local BlockManager = {} 2 + 3 + BlockManager.BlockIdMappings = {} :: {BasePart} 4 + 5 + for i,v in pairs(game:GetService("ReplicatedStorage"):WaitForChild("Blocks"):GetChildren()) do 6 + BlockManager.BlockIdMappings[v:GetAttribute("n")] = v 7 + end 8 + 9 + BlockManager.UpdateIdMappings = {} :: {BasePart} 10 + 11 + for i,v in pairs(game:GetService("ReplicatedStorage"):WaitForChild("BlockUpdateOperations"):GetChildren()) do 12 + local success, reason = pcall(function() 13 + BlockManager.UpdateIdMappings[v:GetAttribute("n")] = require(v) 14 + end) 15 + if not success then 16 + warn("[BLOCKMANAGER] Invalid update operation",v:GetAttribute("n"),":",reason) 17 + end 18 + end 19 + 20 + local warnedBlockIds = {} 21 + 22 + function BlockManager:GetBlock(blockId: number, attr: {[typeof("")]: any}?) 23 + 24 + task.synchronize() 25 + 26 + if not BlockManager.BlockIdMappings[blockId] then 27 + if not warnedBlockIds[blockId] then 28 + warnedBlockIds[blockId] = true 29 + warn("[BLOCKMANAGER] Invalid block id",blockId) 30 + end 31 + return script.invalid:Clone() 32 + end 33 + 34 + local b = BlockManager.BlockIdMappings[blockId]:Clone() 35 + b.Size = Vector3.new(3.95,3.95,3.95) 36 + 37 + for i,v in pairs(attr or {}) do 38 + b:SetAttribute(i,v) 39 + end 40 + 41 + if BlockManager.UpdateIdMappings[blockId] then 42 + local success, reason = pcall(function() 43 + BlockManager.UpdateIdMappings[blockId](b) 44 + end) 45 + if not success then 46 + warn("[BLOCKMANAGER] Failed update operation",blockId,":",reason) 47 + end 48 + end 49 + 50 + return b 51 + end 52 + 53 + -- ChatGPT Generated Func!!!! 54 + function BlockManager:GetBlockRotated(blockId: number, face: Enum.NormalId, attr: {[typeof("")]: any}?) 55 + -- Returns block with id blockId, rotated so the given face (NormalId) points north (+X). 56 + local block = BlockManager:GetBlock(blockId, attr) 57 + local rot = CFrame.new() 58 + 59 + task.synchronize() 60 + 61 + if face == Enum.NormalId.Front then 62 + rot = CFrame.Angles(0, 0, 0) -- no rot 63 + elseif face == Enum.NormalId.Back then 64 + rot = CFrame.Angles(0, math.rad(180), 0) 65 + elseif face == Enum.NormalId.Left then 66 + rot = CFrame.Angles(0, math.rad(90), 0) 67 + elseif face == Enum.NormalId.Right then 68 + rot = CFrame.Angles(0, math.rad(-90), 0) 69 + elseif face == Enum.NormalId.Top then 70 + rot = CFrame.Angles(math.rad(-90), 0, 0) -- top +x 71 + elseif face == Enum.NormalId.Bottom then 72 + rot = CFrame.Angles(math.rad(90), 0, 0) -- bottom +x 73 + end 74 + 75 + 76 + -- ocbwoy3's fix 77 + block:PivotTo(rot) 78 + block.CFrame = CFrame.new(rot.Position) 79 + 80 + return block 81 + end 82 + 83 + 84 + 85 + 86 + return BlockManager
+3
src/ReplicatedStorage/Shared/ChunkManager/BlockManager.meta.json
··· 1 + { 2 + "ignoreUnknownInstances": true 3 + }
+180
src/ReplicatedStorage/Shared/ChunkManager/Chunk.lua
··· 1 + local Chunk = {} 2 + Chunk.__index = Chunk 3 + 4 + Chunk.UpdateBlockBindable = Instance.new("BindableEvent") :: BindableEvent 5 + 6 + Chunk.AllChunks = {} :: {[typeof("")]: typeof(Chunk.new(0,0,0))} 7 + 8 + local RunService = game:GetService("RunService") 9 + 10 + local function Swait(l) 11 + for i = 1,l do 12 + RunService.Stepped:Wait() 13 + end 14 + end 15 + 16 + 17 + export type BlockData = { 18 + id: number, 19 + state: { 20 + [typeof("")]: string | boolean | number 21 + } 22 + } 23 + 24 + function Chunk.new(x,y,z) 25 + local self = setmetatable({}, Chunk) 26 + self.pos = Vector3.new(x,y,z) 27 + 28 + -- Tick ONLY in a 5 chunk distance of LP's char 29 + self.inhabitedTime = tick() 30 + self.instance = Instance.new("Folder") 31 + self.unloadChunkHook = function() end 32 + self.UpdateBlockBindableL = Instance.new("BindableEvent") :: BindableEvent 33 + 34 + self.loaded = false 35 + self.loading = false 36 + 37 + self.data = {} :: {[typeof("")]: BlockData} -- "X,Y,Z": BlockData ("-1,-1,1": BlockData) 38 + return self 39 + end 40 + 41 + function Chunk.from(x,y,z,data) 42 + local self = setmetatable({}, Chunk) 43 + self.pos = Vector3.new(x,y,z) 44 + 45 + -- Tick ONLY in a 5 chunk distance of LP's char 46 + self.inhabitedTime = tick() 47 + self.instance = Instance.new("Folder") 48 + self.unloadChunkHook = function() end 49 + self.UpdateBlockBindableL = Instance.new("BindableEvent") :: BindableEvent 50 + 51 + self.data = data :: {[typeof("")]: BlockData} -- "X,Y,Z": BlockData ("-1,-1,1": BlockData) 52 + return self 53 + end 54 + 55 + function Chunk:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, offsetX, offsetY, offsetZ) 56 + task.desynchronize() 57 + -- Calculate the local position of the neighboring block 58 + local neighborRX, neighborRY, neighborRZ = rx + offsetX, ry + offsetY, rz + offsetZ 59 + local neighborGX, neighborGY, neighborGZ = gx, gy, gz 60 + 61 + -- Adjust for chunk boundaries 62 + if neighborRX < 1 then 63 + neighborRX = 8 64 + neighborGX = gx - 1 65 + elseif neighborRX > 8 then 66 + neighborRX = 1 67 + neighborGX = gx + 1 68 + end 69 + 70 + if neighborRY < 1 then 71 + neighborRY = 8 72 + neighborGY = gy - 1 73 + elseif neighborRY > 8 then 74 + neighborRY = 1 75 + neighborGY = gy + 1 76 + end 77 + 78 + if neighborRZ < 1 then 79 + neighborRZ = 8 80 + neighborGZ = gz - 1 81 + elseif neighborRZ > 8 then 82 + neighborRZ = 1 83 + neighborGZ = gz + 1 84 + end 85 + 86 + if neighborGY < 0 then 87 + return true 88 + end 89 + 90 + -- Get the neighboring chunk 91 + local neighborChunk = Chunk.AllChunks[`{neighborGX},{neighborGY},{neighborGZ}`] 92 + if not neighborChunk then 93 + return false -- Treat unloaded chunks as empty so edges render 94 + end 95 + 96 + -- Check if the block exists in the neighboring chunk 97 + return neighborChunk:GetBlockAt(neighborRX, neighborRY, neighborRZ) ~= nil 98 + end 99 + 100 + function Chunk:IsBlockRenderable(rx, ry, rz) 101 + task.desynchronize() 102 + local gx, gy, gz = self.pos.X, self.pos.Y, self.pos.Z 103 + -- Check all six neighboring blocks 104 + local d = not ( 105 + self:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, 1, 0, 0) and 106 + self:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, -1, 0, 0) and 107 + self:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, 0, 1, 0) and 108 + self:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, 0, -1, 0) and 109 + self:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, 0, 0, 1) and 110 + self:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, 0, 0, -1) 111 + ) 112 + return d 113 + end 114 + 115 + 116 + function Chunk:Tick() 117 + self.inhabitedTime = tick() 118 + end 119 + 120 + function Chunk:PropogateChanges(x: number,y: number,z: number,d:BlockData) 121 + self.UpdateBlockBindableL:Fire(x,y,z,d) 122 + end 123 + 124 + function Chunk:GetBlockAt(x,y,z) 125 + task.desynchronize() 126 + if not self.data[`{tostring(x)},{tostring(y)},{tostring(z)}`] then 127 + return nil 128 + end 129 + return self.data[`{tostring(x)},{tostring(y)},{tostring(z)}`] 130 + end 131 + 132 + function Chunk:CreateBlock(x: number,y: number,z: number,d:BlockData) 133 + self.data[`{tostring(x)},{tostring(y)},{tostring(z)}`] = d 134 + self:PropogateChanges(x,y,z,d) 135 + return self:GetBlockAt(x,y,z) 136 + end 137 + 138 + function Chunk:RemoveBlock(x, y, z) 139 + self.data[x .. "," .. y .. "," .. z] = nil 140 + self:PropogateChanges(x,y,z,0) 141 + end 142 + 143 + -- unsure why this exists 144 + function Chunk:Load() 145 + end 146 + 147 + function Chunk:Unload() 148 + 149 + task.synchronize() 150 + self.loaded = false 151 + 152 + -- SLOWCLEAR 153 + 154 + task.defer(function() 155 + local g = 0 156 + for _,v in pairs(self.instance:GetChildren()) do 157 + pcall(function() 158 + v:Destroy() 159 + end) 160 + g += 1 161 + if g == 30 then 162 + g = 0 163 + Swait(2) 164 + end 165 + end 166 + 167 + task.synchronize() 168 + self.instance.Parent = nil 169 + self.instance:Destroy() 170 + self.unloadChunkHook() 171 + task.desynchronize() 172 + end) 173 + end 174 + 175 + -- DO NOT INTERACT WITH CHUNK AFTER CALLING THIS 176 + function Chunk:Destroy() 177 + self.data = {} 178 + end 179 + 180 + return Chunk :: typeof(Chunk)
+257
src/ReplicatedStorage/Shared/ChunkManager/ChunkBuilder.lua
··· 1 + local ChunkBuilder = {} 2 + 3 + local Chunk = require("./Chunk") 4 + local BlockManager = require("./BlockManager") 5 + local util = require("../Util") 6 + 7 + local objects = script.Parent.Parent.Parent.Objects 8 + 9 + local RunService = game:GetService("RunService") 10 + 11 + local ChunkBorderFolder = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") 12 + 13 + local NEIGHBOR_OFFSETS = { 14 + {-1, 0, 0}, {1, 0, 0}, 15 + {0, -1, 0}, {0, 1, 0}, 16 + {0, 0, -1}, {0, 0, 1} 17 + } 18 + 19 + -- TODO: Move to Chunk 20 + type BlockData = { 21 + id: number, 22 + state: { 23 + [typeof("")]: string | boolean | number 24 + } 25 + } 26 + 27 + local function placeBorder(a,b,c) 28 + local pos = util.ChunkPosToCFrame(Vector3.new(a,b,c),Vector3.new(1,1,1)).Position - Vector3.new(2,2,2) 29 + local d = objects.ChunkLoading:Clone() 30 + d:PivotTo(CFrame.new(pos)) 31 + d.Parent = ChunkBorderFolder 32 + return d 33 + end 34 + 35 + local function Swait(l) 36 + for i = 1,l do 37 + RunService.Stepped:Wait() 38 + end 39 + end 40 + 41 + local function propogateNeighboringBlockChanges(cx, cy, cz, x, y, z) 42 + --warn("propogateNeighboringBlockChanges",cx,cy,cz,x,y,z) 43 + -- updates block in another chunk 44 + local c = Chunk.AllChunks[`{cx},{cy},{cz}`] 45 + if not c then return end 46 + 47 + local d = c.data[`{x},{y},{z}`] 48 + if not d then return end 49 + 50 + if c:IsBlockRenderable(x, y, z) then 51 + if c.instance:FindFirstChild(`{x},{y},{z}`) then return end 52 + task.synchronize() 53 + local block = BlockManager:GetBlockRotated(d.id, util.RotationStringToNormalId(d.state["r"] or "f"), d.state) 54 + block.Name = `{x},{y},{z}` 55 + block:PivotTo(util.ChunkPosToCFrame(c.pos, Vector3.new(x, y, z))) 56 + block.Parent = c.instance 57 + task.desynchronize() 58 + else 59 + local existing = c.instance:FindFirstChild(`{x},{y},{z}`) 60 + if existing then 61 + task.synchronize() 62 + existing:Destroy() 63 + task.desynchronize() 64 + end 65 + end 66 + end 67 + 68 + function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?) 69 + 70 + if c.loaded then 71 + return c.instance 72 + end 73 + 74 + local blocks = c.data 75 + local newcache = {} :: {[typeof("")]: BlockData} 76 + 77 + local finished = false 78 + 79 + 80 + local ch = Instance.new("Folder") 81 + ch.Parent = parent 82 + ch.Name = `{c.pos.X},{c.pos.Y},{c.pos.Z}` 83 + 84 + local conn = c.UpdateBlockBindableL.Event:Connect(function(x: number, y: number, z: number, d: BlockData) 85 + task.desynchronize() 86 + if finished == false then 87 + newcache[`{x},{y},{z}`] = d 88 + return 89 + end 90 + task.synchronize() 91 + for _, o in pairs(NEIGHBOR_OFFSETS) do 92 + --warn("propogate",o[1],o[2],o[3]) 93 + -- Adjust for chunk boundaries 94 + local b = {x = x + o[1], y = y + o[2], z = z + o[3]} 95 + local ch = {x = c.pos.X, y = c.pos.Y, z = c.pos.Z} 96 + 97 + if b.x < 1 then ch.x = c.pos.X - 1 b.x = 8 end 98 + if b.x > 8 then ch.x = c.pos.X + 1 b.x = 1 end 99 + if b.y < 1 then ch.y = c.pos.Y - 1 b.y = 8 end 100 + if b.y > 8 then ch.y = c.pos.Y + 1 b.y = 1 end 101 + if b.z < 1 then ch.z = c.pos.Z - 1 b.z = 8 end 102 + if b.z > 8 then ch.z = c.pos.Z + 1 b.z = 1 end 103 + 104 + propogateNeighboringBlockChanges(ch.x, ch.y, ch.z, b.x, b.y, b.z) 105 + --BlockManager:GetBlock(ch.x) 106 + end 107 + 108 + local blockName = `{x},{y},{z}` 109 + local existing = ch:FindFirstChild(blockName) 110 + if d == 0 then 111 + if existing then 112 + task.synchronize() 113 + existing:Destroy() 114 + task.desynchronize() 115 + end 116 + return 117 + end 118 + if not c:IsBlockRenderable(x, y, z) then 119 + if existing then 120 + task.synchronize() 121 + existing:Destroy() 122 + task.desynchronize() 123 + end 124 + return 125 + end 126 + if existing then 127 + task.synchronize() 128 + existing:Destroy() 129 + task.desynchronize() 130 + end 131 + if not d then return end 132 + if d.id == 0 then return end 133 + local N = util.RotationStringToNormalId(d.state["r"] or "f") 134 + task.synchronize() 135 + local block = BlockManager:GetBlockRotated(d.id, N, d.state) 136 + block.Name = blockName 137 + block:PivotTo(util.ChunkPosToCFrame(c.pos, Vector3.new(x, y, z))) 138 + block.Parent = ch 139 + task.desynchronize() 140 + end) 141 + 142 + c.unloadChunkHook = function() 143 + conn:Disconnect() 144 + blocks = nil 145 + c = nil 146 + end 147 + 148 + task.defer(function() 149 + 150 + local p = 0 151 + 152 + task.synchronize() 153 + 154 + local hb = false 155 + 156 + for a,b in pairs(blocks) do 157 + hb = true 158 + end 159 + 160 + local border = Instance.new("Part") 161 + if hb == true then 162 + border:Destroy() 163 + border = placeBorder(c.pos.X, c.pos.Y, c.pos.Z) 164 + end 165 + 166 + for a,b in pairs(blocks) do 167 + task.desynchronize() 168 + local coords = util.BlockPosStringToCoords(a) 169 + if not c:IsBlockRenderable(coords.X, coords.Y, coords.Z) then 170 + if ch:FindFirstChild(a) then 171 + task.synchronize() 172 + ch:FindFirstChild(a):Destroy() 173 + task.desynchronize() 174 + end 175 + continue 176 + end 177 + task.desynchronize() 178 + local N = util.RotationStringToNormalId(b.state["r"] or "f") 179 + task.synchronize() 180 + local block = BlockManager:GetBlockRotated(b.id, N, b.state) 181 + if ch:FindFirstChild(a) then 182 + ch:FindFirstChild(a):Destroy() 183 + end 184 + block.Name = a 185 + block:PivotTo(util.ChunkPosToCFrame(c.pos, coords)) 186 + block.Parent = ch 187 + p += 1 188 + if p == 15 then 189 + p = 0 190 + Swait(1) 191 + end 192 + end 193 + 194 + finished = true 195 + 196 + task.synchronize() 197 + border:Destroy() 198 + task.desynchronize() 199 + 200 + task.defer(function() 201 + task.synchronize() 202 + for key, data in pairs(newcache) do 203 + local coords = util.BlockPosStringToCoords(key) 204 + for _, o in pairs(NEIGHBOR_OFFSETS) do 205 + -- chunks are 8x8x8 206 + local nb = {x = coords.X + o[1], y = coords.Y + o[2], z = coords.Z + o[3]} 207 + local chCoords = {x = c.pos.X, y = c.pos.Y, z = c.pos.Z} 208 + if nb.x == 0 then chCoords.x = c.pos.X - 1 nb.x = 8 end 209 + if nb.x == 9 then chCoords.x = c.pos.X + 1 nb.x = 1 end 210 + 211 + if nb.y == 0 then chCoords.y = c.pos.Y - 1 nb.y = 8 end 212 + if nb.y == 9 then chCoords.y = c.pos.Y + 1 nb.y = 1 end 213 + 214 + if nb.z == 0 then chCoords.z = c.pos.Z - 1 nb.z = 8 end 215 + if nb.z == 9 then chCoords.z = c.pos.Z + 1 nb.z = 1 end 216 + 217 + propogateNeighboringBlockChanges(chCoords.x, chCoords.y, chCoords.z, nb.x, nb.y, nb.z) 218 + end 219 + 220 + local existing = ch:FindFirstChild(key) 221 + if data == 0 or (data and data.id == 0) then 222 + if existing then 223 + existing:Destroy() 224 + end 225 + continue 226 + end 227 + if not c:IsBlockRenderable(coords.X, coords.Y, coords.Z) then 228 + if existing then 229 + existing:Destroy() 230 + end 231 + continue 232 + end 233 + if existing then 234 + existing:Destroy() 235 + end 236 + if not data then 237 + continue 238 + end 239 + local N = util.RotationStringToNormalId(data.state["r"] or "f") 240 + local block = BlockManager:GetBlockRotated(data.id, N, data.state) 241 + block.Name = key 242 + block:PivotTo(util.ChunkPosToCFrame(c.pos, coords)) 243 + block.Parent = ch 244 + end 245 + newcache = nil 246 + blocks = nil 247 + end) 248 + task.desynchronize() 249 + end) 250 + 251 + c.loaded = true 252 + 253 + return ch 254 + 255 + end 256 + 257 + return ChunkBuilder
+232
src/ReplicatedStorage/Shared/ChunkManager/init.lua
··· 1 + local ChunkManager = {} 2 + 3 + local RunService = game:GetService("RunService") 4 + 5 + local Chunk = require("./ChunkManager/Chunk") 6 + local BlockManager = require("./ChunkManager/BlockManager") 7 + local ChunkBuilder = require("./ChunkManager/ChunkBuilder") 8 + 9 + local remote = game:GetService("ReplicatedStorage"):WaitForChild("RecieveChunkPacket") 10 + local tickremote = game:GetService("ReplicatedStorage"):WaitForChild("Tick") 11 + 12 + local ChunkFolder = Instance.new("Folder") 13 + ChunkFolder.Name = "$blockscraft_client" 14 + 15 + ChunkManager.ChunkFolder = ChunkFolder 16 + 17 + local CHUNK_RADIUS = 5 18 + local LOAD_BATCH = 8 19 + local FORCELOAD_CHUNKS = { 20 + {0, 1, 0} 21 + } 22 + 23 + local unloadingChunks = {} 24 + local pendingChunkRequests = {} 25 + 26 + local CHUNK_OFFSETS = {} 27 + do 28 + for y = -CHUNK_RADIUS, CHUNK_RADIUS do 29 + for x = -CHUNK_RADIUS, CHUNK_RADIUS do 30 + for z = -CHUNK_RADIUS, CHUNK_RADIUS do 31 + table.insert(CHUNK_OFFSETS, {x, y, z, (x * x) + (y * y) + (z * z)}) 32 + end 33 + end 34 + end 35 + table.sort(CHUNK_OFFSETS, function(a, b) 36 + return a[4] < b[4] 37 + end) 38 + end 39 + 40 + local function Swait(l) 41 + task.synchronize() 42 + for _ = 1, l do 43 + RunService.Stepped:Wait() 44 + end 45 + end 46 + 47 + function ChunkManager:GetChunk(x, y, z) 48 + local key = `{x},{y},{z}` 49 + if Chunk.AllChunks[key] then 50 + return Chunk.AllChunks[key] 51 + end 52 + 53 + if pendingChunkRequests[key] then 54 + task.synchronize() 55 + while pendingChunkRequests[key] do 56 + task.wait() 57 + end 58 + return Chunk.AllChunks[key] 59 + end 60 + 61 + task.synchronize() 62 + pendingChunkRequests[key] = true 63 + local ok, data = pcall(function() 64 + return remote:InvokeServer(x, y, z) 65 + end) 66 + if not ok then 67 + data = {} 68 + end 69 + task.synchronize() 70 + local ch = Chunk.from(x, y, z, data) 71 + Chunk.AllChunks[key] = ch 72 + pendingChunkRequests[key] = nil 73 + return ch 74 + end 75 + 76 + local function ensureNeighboringChunksLoaded(x, y, z) 77 + local offsets = { 78 + {1, 0, 0}, {-1, 0, 0}, 79 + {0, 1, 0}, {0, -1, 0}, 80 + {0, 0, 1}, {0, 0, -1} 81 + } 82 + 83 + for _, offset in ipairs(offsets) do 84 + local nx, ny, nz = x + offset[1], y + offset[2], z + offset[3] 85 + ChunkManager:GetChunk(nx, ny, nz):Tick() 86 + end 87 + end 88 + 89 + function ChunkManager:LoadChunk(x, y, z) 90 + local key = `{x},{y},{z}` 91 + if unloadingChunks[key] or not Chunk.AllChunks[key] or Chunk.AllChunks[key].loaded then 92 + return 93 + end 94 + 95 + unloadingChunks[key] = true 96 + task.defer(function() 97 + task.desynchronize() 98 + ensureNeighboringChunksLoaded(x, y, z) 99 + 100 + local chunk = Chunk.AllChunks[key] 101 + if not chunk then 102 + chunk = ChunkManager:GetChunk(x, y, z) 103 + Chunk.AllChunks[key] = chunk 104 + end 105 + 106 + task.synchronize() 107 + local instance = ChunkBuilder:BuildChunk(chunk, ChunkFolder) 108 + chunk.instance = instance 109 + chunk.loaded = true 110 + unloadingChunks[key] = nil 111 + end) 112 + end 113 + 114 + function ChunkManager:ForceTick() 115 + for _, coords in ipairs(FORCELOAD_CHUNKS) do 116 + local key = `{coords[1]},{coords[2]},{coords[3]}` 117 + local chunk = Chunk.AllChunks[key] 118 + if not chunk then 119 + ChunkManager:LoadChunk(coords[1], coords[2], coords[3]) 120 + else 121 + chunk:Tick() 122 + end 123 + end 124 + end 125 + 126 + function ChunkManager:TickI() 127 + for key, chunk in pairs(Chunk.AllChunks) do 128 + if tick() - chunk.inhabitedTime <= 5 then 129 + tickremote:FireServer(key) 130 + end 131 + end 132 + end 133 + 134 + function ChunkManager:Tick() 135 + ChunkManager:ForceTick() 136 + local player = game:GetService("Players").LocalPlayer 137 + if not player.Character then 138 + return 139 + end 140 + 141 + local pos = player.Character:GetPivot().Position 142 + local chunkPos = { 143 + x = math.round(pos.X / 32), 144 + y = math.round(pos.Y / 32), 145 + z = math.round(pos.Z / 32) 146 + } 147 + 148 + task.defer(function() 149 + local processed = 0 150 + for _, offset in ipairs(CHUNK_OFFSETS) do 151 + local cx, cy, cz = chunkPos.x + offset[1], chunkPos.y + offset[2], chunkPos.z + offset[3] 152 + local chunk = ChunkManager:GetChunk(cx, cy, cz) 153 + chunk.inhabitedTime = tick() 154 + if not chunk.loaded then 155 + ChunkManager:LoadChunk(cx, cy, cz) 156 + processed += 1 157 + if processed % LOAD_BATCH == 0 then 158 + Swait(1) 159 + end 160 + end 161 + end 162 + end) 163 + 164 + --[[ 165 + task.defer(function() 166 + for y = 0, 2 do 167 + task.defer(function() 168 + for x = -CHUNK_RADIUS, CHUNK_RADIUS do 169 + task.desynchronize() 170 + for z = -CHUNK_RADIUS, CHUNK_RADIUS do 171 + local cx, cy, cz = chunkPos.x + x, y, chunkPos.z + z 172 + local key = `{cx},{cy},{cz}` 173 + local chunk = ChunkManager:GetChunk(cx, cy, cz) 174 + chunk.inhabitedTime = tick() 175 + if not chunk.loaded then 176 + ChunkManager:LoadChunk(cx, cy, cz) 177 + Swait(2) 178 + end 179 + end 180 + task.synchronize() 181 + end 182 + end) 183 + Swait(10) 184 + end 185 + end) 186 + --]] 187 + 188 + for key, loadedChunk in pairs(Chunk.AllChunks) do 189 + if tick() - loadedChunk.inhabitedTime > 15 and not unloadingChunks[key] then 190 + unloadingChunks[key] = true 191 + task.defer(function() 192 + task.synchronize() 193 + loadedChunk:Unload() 194 + loadedChunk:Destroy() 195 + Chunk.AllChunks[key] = nil 196 + unloadingChunks[key] = nil 197 + end) 198 + end 199 + end 200 + end 201 + 202 + function ChunkManager:Init() 203 + if not RunService:IsClient() then 204 + error("ChunkManager:Init can only be called on the client") 205 + end 206 + 207 + ChunkFolder.Parent = game:GetService("Workspace") 208 + ChunkManager:ForceTick() 209 + 210 + task.defer(function() 211 + while true do 212 + wait(2) 213 + ChunkManager:TickI() 214 + end 215 + end) 216 + 217 + task.defer(function() 218 + while true do 219 + task.defer(function() 220 + local success, err = pcall(function() 221 + ChunkManager:Tick() 222 + end) 223 + if not success then 224 + warn("[CHUNKMANAGER]", err) 225 + end 226 + end) 227 + Swait(20) 228 + end 229 + end) 230 + end 231 + 232 + return ChunkManager
+47
src/ReplicatedStorage/Shared/ModLoader.lua
··· 1 + local ML = {} 2 + 3 + type modContext = { 4 + name: string, 5 + description: string, 6 + ns: string, 7 + author: {string}, 8 + init: typeof(function()end) 9 + } 10 + 11 + local ReplicatedStorage = game:GetService("ReplicatedStorage") 12 + local Shared = ReplicatedStorage:WaitForChild("Shared") 13 + local ModsFolder = ReplicatedStorage:WaitForChild("Mods") 14 + 15 + function ML.loadModsS() 16 + print("[SSModLoader] Loading Mods") 17 + 18 + for _, m in pairs(ModsFolder:GetChildren()) do 19 + local success, reason = pcall(function() 20 + -- ignore type err 21 + local mod: modContext = require(m) 22 + mod.init() 23 + print(`[SSModLoader] Loaded {mod.name} ({mod.ns}) by {table.concat(mod.author,", ")}`) 24 + end) 25 + if not success then 26 + warn(`[CSModLoader] Error loading {m.Name}: {reason}`) 27 + end 28 + end 29 + end 30 + 31 + function ML.loadModsC() 32 + print("[CSModLoader] Loading Mods") 33 + 34 + for _, m in pairs(ModsFolder:GetChildren()) do 35 + local success, reason = pcall(function() 36 + -- ignore type err 37 + local mod: modContext = require(m) 38 + mod.init() 39 + print(`[CSModLoader] Loaded {mod.name} ({mod.ns}) by {table.concat(mod.author,", ")}`) 40 + end) 41 + if not success then 42 + warn(`[CSModLoader] Error loading {m.Name}: {reason}`) 43 + end 44 + end 45 + end 46 + 47 + return ML
+221
src/ReplicatedStorage/Shared/PlacementManager.lua
··· 1 + local PlacementManager = {} 2 + 3 + local ChunkManager = require("./ChunkManager") 4 + local Util = require("./Util") 5 + 6 + PlacementManager.ChunkFolder = ChunkManager.ChunkFolder 7 + 8 + local raycastParams = RaycastParams.new() 9 + raycastParams.FilterDescendantsInstances = {PlacementManager.ChunkFolder} 10 + raycastParams.FilterType = Enum.RaycastFilterType.Include 11 + raycastParams.IgnoreWater = true 12 + 13 + if _G.SB then return nil end 14 + _G.SB = true 15 + 16 + PlacementManager.SelectionBox = script.SelectionBox:Clone() 17 + PlacementManager.SelectionBox.Name = "$SelectionBox"..(game:GetService("RunService"):IsServer() and "_SERVER" or "") 18 + PlacementManager.SelectionBox.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") 19 + 20 + -- Trash method TODO: Fix this 21 + local function findParent(i: Instance): Instance 22 + local f = i:FindFirstAncestorOfClass("Folder") 23 + local d = i 24 + repeat 25 + d = d.Parent 26 + until d.Parent == f 27 + return d 28 + end 29 + 30 + local Mouse: Mouse = nil 31 + local lastNormalId: Enum.NormalId? = nil 32 + 33 + local function normalIdToOffset(normal: Enum.NormalId): Vector3 34 + if normal == Enum.NormalId.Top then 35 + return Vector3.new(0, 1, 0) 36 + elseif normal == Enum.NormalId.Bottom then 37 + return Vector3.new(0, -1, 0) 38 + elseif normal == Enum.NormalId.Left then 39 + return Vector3.new(-1, 0, 0) 40 + elseif normal == Enum.NormalId.Right then 41 + return Vector3.new(1, 0, 0) 42 + elseif normal == Enum.NormalId.Back then 43 + return Vector3.new(0, 0, 1) 44 + elseif normal == Enum.NormalId.Front then 45 + return Vector3.new(0, 0, -1) 46 + end 47 + return Vector3.new(0, 0, 0) 48 + end 49 + 50 + local function offsetChunkBlock(chunk: Vector3, block: Vector3, offset: Vector3) 51 + local cx, cy, cz = chunk.X, chunk.Y, chunk.Z 52 + local bx, by, bz = block.X + offset.X, block.Y + offset.Y, block.Z + offset.Z 53 + 54 + if bx < 1 then 55 + bx = 8 56 + cx -= 1 57 + elseif bx > 8 then 58 + bx = 1 59 + cx += 1 60 + end 61 + 62 + if by < 1 then 63 + by = 8 64 + cy -= 1 65 + elseif by > 8 then 66 + by = 1 67 + cy += 1 68 + end 69 + 70 + if bz < 1 then 71 + bz = 8 72 + cz -= 1 73 + elseif bz > 8 then 74 + bz = 1 75 + cz += 1 76 + end 77 + 78 + return Vector3.new(cx, cy, cz), Vector3.new(bx, by, bz) 79 + end 80 + 81 + -- Gets the block and normalid of the block (and surface) the player is looking at 82 + function PlacementManager:Raycast() 83 + if not Mouse then 84 + Mouse = game:GetService("Players").LocalPlayer:GetMouse() 85 + end 86 + task.synchronize() 87 + local objLookingAt = Mouse.Target 88 + local dir = Mouse.TargetSurface 89 + if not objLookingAt then 90 + PlacementManager.SelectionBox.Adornee = nil 91 + script.RaycastResult.Value = nil 92 + lastNormalId = nil 93 + return 94 + end 95 + 96 + --if not objLookingAt:IsDescendantOf(ChunkManager.ChunkFolder) then return end 97 + local parent = findParent(objLookingAt) 98 + if parent:GetAttribute("ns") == true then 99 + PlacementManager.SelectionBox.Adornee = nil 100 + script.RaycastResult.Value = nil 101 + lastNormalId = nil 102 + return 103 + end 104 + PlacementManager.SelectionBox.Adornee = parent 105 + script.RaycastResult.Value = parent 106 + lastNormalId = dir 107 + return parent, dir 108 + end 109 + 110 + function PlacementManager:RaycastGetResult() 111 + return script.RaycastResult.Value 112 + end 113 + 114 + local remotes = game:GetService("ReplicatedStorage"):WaitForChild("Remotes") 115 + local placeRemote = remotes:WaitForChild("PlaceBlock") 116 + local breakRemote = remotes:WaitForChild("BreakBlock") 117 + local tickRemote = game:GetService("ReplicatedStorage").Tick 118 + 119 + -- FIRES REMOTE 120 + function PlacementManager:PlaceBlock(cx, cy, cz, x, y, z, blockId: string) 121 + --print("placeblock") 122 + --local chunk = ChunkManager:GetChunk(cx, cy, cz) 123 + --chunk:CreateBlock(x, y, z, blockData) 124 + placeRemote:FireServer(cx, cy, cz, x, y, z, blockId) 125 + end 126 + 127 + -- FIRES REMOTE 128 + function PlacementManager:BreakBlock(cx, cy, cz, x, y, z) 129 + --print("breakblock") 130 + --local chunk = ChunkManager:GetChunk(cx, cy, cz) 131 + --chunk:RemoveBlock(x, y, z) 132 + breakRemote:FireServer(cx, cy, cz, x, y, z) 133 + end 134 + 135 + -- CLIENTSIDED 136 + function PlacementManager:PlaceBlockLocal(cx, cy, cz, x, y, z, blockData) 137 + local chunk = ChunkManager:GetChunk(cx, cy, cz) 138 + chunk:CreateBlock(x, y, z, blockData) 139 + end 140 + 141 + -- CLIENTSIDED 142 + function PlacementManager:BreakBlockLocal(cx, cy, cz, x, y, z) 143 + local chunk = ChunkManager:GetChunk(cx, cy, cz) 144 + chunk:RemoveBlock(x, y, z) 145 + end 146 + 147 + function PlacementManager:GetBlockAtMouse(): nil | {chunk:Vector3, block: Vector3} 148 + local selectedPart = PlacementManager:RaycastGetResult() 149 + --print(selectedPart and selectedPart:GetFullName() or nil) 150 + if selectedPart == nil then 151 + PlacementManager.SelectionBox.Adornee = nil 152 + script.RaycastResult.Value = nil 153 + lastNormalId = nil 154 + return nil 155 + end 156 + if not selectedPart.Parent then 157 + PlacementManager.SelectionBox.Adornee = nil 158 + script.RaycastResult.Value = nil 159 + lastNormalId = nil 160 + return nil 161 + end 162 + local chunkCoords = Util.BlockPosStringToCoords(selectedPart.Parent.Name) 163 + local blockCoords = Util.BlockPosStringToCoords(selectedPart.Name) 164 + 165 + return { 166 + chunk = chunkCoords, 167 + block = blockCoords 168 + } 169 + 170 + end 171 + 172 + function PlacementManager:GetTargetAtMouse(): nil | {chunk:Vector3, block: Vector3, normal: Enum.NormalId} 173 + local hit = PlacementManager:GetBlockAtMouse() 174 + if not hit or not lastNormalId then 175 + return nil 176 + end 177 + 178 + return { 179 + chunk = hit.chunk, 180 + block = hit.block, 181 + normal = lastNormalId 182 + } 183 + end 184 + 185 + function PlacementManager:GetPlacementAtMouse(): nil | {chunk:Vector3, block: Vector3} 186 + local hit = PlacementManager:GetTargetAtMouse() 187 + if not hit then 188 + return nil 189 + end 190 + local offset = normalIdToOffset(hit.normal) 191 + local placeChunk, placeBlock = offsetChunkBlock(hit.chunk, hit.block, offset) 192 + return { 193 + chunk = placeChunk, 194 + block = placeBlock 195 + } 196 + end 197 + 198 + function PlacementManager:Init() 199 + game:GetService("RunService").Heartbeat:Connect(function() 200 + local a,b = pcall(function() 201 + PlacementManager:Raycast() 202 + end) 203 + if not a then 204 + task.synchronize() 205 + PlacementManager.SelectionBox.Adornee = nil 206 + script.RaycastResult.Value = nil 207 + task.desynchronize() 208 + end 209 + end) 210 + tickRemote.OnClientEvent:Connect(function(m, cx, cy, cz, x, y, z, d) 211 + --warn("PROPOGATED TICK", m, cx, cy, cz, x, y, z, d) 212 + if m == "B_C" then 213 + PlacementManager:PlaceBlockLocal(cx, cy, cz, x, y ,z, d) 214 + end 215 + if m == "B_D" then 216 + PlacementManager:BreakBlockLocal(cx, cy, cz, x, y ,z) 217 + end 218 + end) 219 + end 220 + 221 + return PlacementManager
+3
src/ReplicatedStorage/Shared/PlacementManager.meta.json
··· 1 + { 2 + "ignoreUnknownInstances": true 3 + }
+48
src/ReplicatedStorage/Shared/Util.lua
··· 1 + local module = {} 2 + 3 + function module.BlockPosStringToCoords(s: string): Vector3 4 + -- a,b,c 5 + local split = string.split(s,",") 6 + return Vector3.new(tonumber(split[1]), tonumber(split[2]), tonumber(split[3])) 7 + end 8 + 9 + function module.RotationStringToNormalId(s: string): Enum.NormalId 10 + if not s then return Enum.NormalId.Front end 11 + if s == "f" then 12 + return Enum.NormalId.Front 13 + end 14 + if s == "b" then 15 + return Enum.NormalId.Back 16 + end 17 + if s == "l" then 18 + return Enum.NormalId.Left 19 + end 20 + if s == "r" then 21 + return Enum.NormalId.Right 22 + end 23 + if s == "t" then 24 + return Enum.NormalId.Top 25 + end 26 + if s == "b" then 27 + return Enum.NormalId.Bottom 28 + end 29 + warn("Could not convert",s,"to Enum.NormalId") 30 + return Enum.NormalId.Front 31 + end 32 + 33 + -- chunk size 8x8x8 relative block indexs 1-8 34 + function module.GlobalBlockPosToRelative(x: number,y:number,z:number) 35 + return x%8,y%8,z%8 36 + end 37 + 38 + function module.ChunkPosToCFrame(chunk: Vector3, block: Vector3): CFrame 39 + return CFrame.new( 40 + CFrame.new( 41 + (32*chunk.X)+(block.X*4)-18, 42 + (32*chunk.Y)+(block.Y*4)-18, 43 + (32*chunk.Z)+(block.Z*4)-18 44 + ).Position 45 + ) 46 + end 47 + 48 + return module
+3
src/ReplicatedStorage/Shared/init.meta.json
··· 1 + { 2 + "ignoreUnknownInstances": true 3 + }
+1816
src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/BitBuffer.lua
··· 1 + local CHAR_SET = [[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/]] 2 + 3 + -- Tradition is to use chars for the lookup table instead of codepoints. 4 + -- But due to how we're running the encode function, it's faster to use codepoints. 5 + local encode_char_set = {} 6 + local decode_char_set = {} 7 + for i = 1, 64 do 8 + encode_char_set[i - 1] = string.byte(CHAR_SET, i, i) 9 + decode_char_set[string.byte(CHAR_SET, i, i)] = i - 1 10 + end 11 + 12 + -- stylua: ignore 13 + local HEX_TO_BIN = { 14 + ["0"] = "0000", ["1"] = "0001", ["2"] = "0010", ["3"] = "0011", 15 + ["4"] = "0100", ["5"] = "0101", ["6"] = "0110", ["7"] = "0111", 16 + ["8"] = "1000", ["9"] = "1001", ["a"] = "1010", ["b"] = "1011", 17 + ["c"] = "1100", ["d"] = "1101", ["e"] = "1110", ["f"] = "1111" 18 + } 19 + 20 + -- stylua: ignore 21 + local NORMAL_ID_VECTORS = { -- [Enum.Value] = Vector3.fromNormalId(Enum) 22 + [0] = Vector3.new(1, 0, 0), -- Enum.NormalId.Right 23 + [1] = Vector3.new(0, 1, 0), -- Enum.NormalId.Top 24 + [2] = Vector3.new(0, 0, 1), -- Enum.NormalId.Back 25 + [3] = Vector3.new(-1, 0, 0), -- Enum.NormalId.Left 26 + [4] = Vector3.new(0, -1, 0), -- Enum.NormalId.Bottom 27 + [5] = Vector3.new(0, 0, -1) -- Enum.NormalId.Front 28 + } 29 + 30 + local ONES_VECTOR = Vector3.new(1, 1, 1) 31 + 32 + local BOOL_TO_BIT = { [true] = 1, [false] = 0 } 33 + 34 + local CRC32_POLYNOMIAL = 0xedb88320 35 + 36 + local crc32_poly_lookup = {} 37 + for i = 0, 255 do 38 + local crc = i 39 + for _ = 1, 8 do 40 + local mask = -bit32.band(crc, 1) 41 + crc = bit32.bxor(bit32.rshift(crc, 1), bit32.band(CRC32_POLYNOMIAL, mask)) 42 + end 43 + crc32_poly_lookup[i] = crc 44 + end 45 + 46 + local powers_of_2 = {} 47 + for i = 0, 64 do 48 + powers_of_2[i] = 2 ^ i 49 + end 50 + 51 + local byte_to_hex = {} 52 + for i = 0, 255 do 53 + byte_to_hex[i] = string.format("%02x", i) 54 + end 55 + 56 + local function bitBuffer(stream) 57 + if stream ~= nil then 58 + assert(type(stream) == "string", "argument to BitBuffer constructor must be either nil or a string") 59 + end 60 + 61 + -- The bit buffer works by keeping an array of bytes, a 'final' byte, and how many bits are currently in that last byte 62 + -- Bits are not kept track of on their own, and are instead combined to form a byte, which is stored in the last space in the array. 63 + -- This byte is also stored seperately, so that table operations aren't needed to read or modify its value. 64 + -- The byte array is called `bytes`. The last byte is stored in `lastByte`. The bit counter is stored in `bits`. 65 + 66 + local bits = 0 -- How many free floating bits there are. 67 + local bytes = {} --! -- Array of bytes currently in the buffer 68 + local lastByte = 0 -- The most recent byte in the buffer, made up of free floating bits 69 + 70 + local byteCount = 0 -- This variable keeps track of how many bytes there are total in the bit buffer. 71 + local bitCount = 0 -- This variable keeps track of how many bits there are total in the bit buffer 72 + 73 + local pointer = 0 -- This variable keeps track of what bit the read functions start at 74 + local pointerByte = 1 -- This variable keeps track of what byte the pointer is at. It starts at 1 since the byte array starts at 1. 75 + 76 + if stream then 77 + byteCount = #stream 78 + bitCount = byteCount * 8 79 + 80 + bytes = table.create(#stream) 81 + 82 + for i = 1, byteCount do 83 + bytes[i] = string.byte(stream, i, i) 84 + end 85 + end 86 + 87 + local function dumpBinary() 88 + -- This function is for debugging or analysis purposes. 89 + -- It dumps the contents of the byte array and the remaining bits into a string of binary digits. 90 + -- Thus, bytes [97, 101] with bits [1, 1, 0] would output "01100001 01100101 110" 91 + local output = table.create(byteCount) --! 92 + for i, v in ipairs(bytes) do 93 + output[i] = string.gsub(byte_to_hex[v], "%x", HEX_TO_BIN) 94 + end 95 + if bits ~= 0 then 96 + -- Because the last byte (where the free floating bits are stored) is in the byte array, it has to be overwritten. 97 + output[byteCount] = string.sub(output[byteCount], 1, bits) 98 + end 99 + 100 + return table.concat(output, " ") 101 + end 102 + 103 + local function dumpStringOld() 104 + -- This function is for accessing the total contents of the bitbuffer. 105 + -- This function combines all the bytes, including the last byte, into a string of binary data. 106 + -- Thus, bytes [97, 101] and bits [1, 1, 0] would become (in hex) "0x61 0x65 0x06" 107 + 108 + -- It's substantially faster to create several smaller strings before using table.concat. (well maybe it was, but it isn't now post 2022) 109 + local output = table.create(math.ceil(byteCount / 4096)) --! 110 + local c = 1 111 + for i = 1, byteCount, 4096 do -- groups of 4096 bytes is the point at which there are diminishing returns 112 + output[c] = string.char(table.unpack(bytes, i, math.min(byteCount, i + 4095))) 113 + c = c + 1 114 + end 115 + 116 + return table.concat(output, "") 117 + end 118 + 119 + --Let lua be lua 120 + local function dumpString() 121 + return string.char(table.unpack(bytes)) 122 + end 123 + 124 + 125 + local function dumpHex() 126 + -- This function is for getting the hex of the bitbuffer's contents, should that be desired 127 + local output = table.create(byteCount) --! 128 + for i, v in ipairs(bytes) do 129 + output[i] = byte_to_hex[v] 130 + end 131 + 132 + return table.concat(output, "") 133 + end 134 + 135 + local function dumpBase64() 136 + -- Base64 is a safe and easy way to convert binary data to be entirely printable 137 + -- It works on the principle that groups of 3 bytes (24 bits) can evenly be divided into 4 groups of 6 138 + -- And 2^6 is a mere 64, far less than the number of printable characters. 139 + -- If there are any missing bytes, `=` is added to the end as padding. 140 + -- Base64 increases the size of its input by 33%. 141 + local output = table.create(math.ceil(byteCount * 1.333)) --! 142 + 143 + local c = 1 144 + for i = 1, byteCount, 3 do 145 + local b1, b2, b3 = bytes[i], bytes[i + 1], bytes[i + 2] 146 + local packed = bit32.bor(bit32.lshift(b1, 16), bit32.lshift(b2 or 0, 8), b3 or 0) 147 + 148 + -- This can be done with bit32.extract (and/or bit32.lshift, bit32.band, bit32.rshift) 149 + -- But bit masking and shifting is more eloquent in my opinion. 150 + output[c] = encode_char_set[bit32.rshift(bit32.band(packed, 0xfc0000), 0x12)] 151 + output[c + 1] = encode_char_set[bit32.rshift(bit32.band(packed, 0x3f000), 0xc)] 152 + output[c + 2] = b2 and encode_char_set[bit32.rshift(bit32.band(packed, 0xfc0), 0x6)] or 0x3d -- 0x3d == "=" 153 + output[c + 3] = b3 and encode_char_set[bit32.band(packed, 0x3f)] or 0x3d 154 + 155 + c = c + 4 156 + end 157 + c = c - 1 -- c will always be 1 more than the length of `output` 158 + 159 + local realOutput = table.create(math.ceil(c / 0x1000)) --! 160 + local k = 1 161 + for i = 1, c, 0x1000 do 162 + realOutput[k] = string.char(table.unpack(output, i, math.min(c, i + 0xfff))) 163 + k = k + 1 164 + end 165 + 166 + return table.concat(realOutput, "") 167 + end 168 + 169 + local function exportChunk(chunkLength) 170 + assert(type(chunkLength) == "number", "argument #1 to BitBuffer.exportChunk should be a number") 171 + assert(chunkLength > 0, "argument #1 to BitBuffer.exportChunk should be above zero") 172 + assert(chunkLength % 1 == 0, "argument #1 to BitBuffer.exportChunk should be an integer") 173 + 174 + -- Since `i` is being returned, the most eloquent way to handle this is with a coroutine 175 + -- This allows returning the existing value of `i` without having to increment it first. 176 + -- The alternative was starting at `i = -(chunkLength-1)` and incrementing at the start of the iterator function. 177 + return coroutine.wrap(function() 178 + local realChunkLength = chunkLength - 1 179 + -- Since this function only has one 'state', it's perfectly fine to use a for-loop. 180 + for i = 1, byteCount, chunkLength do 181 + local chunk = string.char(table.unpack(bytes, i, math.min(byteCount, i + realChunkLength))) 182 + coroutine.yield(i, chunk) 183 + end 184 + end) 185 + end 186 + 187 + local function exportBase64Chunk(chunkLength) 188 + chunkLength = chunkLength or 76 189 + assert(type(chunkLength) == "number", "argument #1 to BitBuffer.exportBase64Chunk should be a number") 190 + assert(chunkLength > 0, "argument #1 to BitBuffer.exportBase64Chunk should be above zero") 191 + assert(chunkLength % 1 == 0, "argument #1 to BitBuffer.exportBase64Chunk should be an integer") 192 + 193 + local output = table.create(math.ceil(byteCount * 0.333)) --! 194 + 195 + local c = 1 196 + for i = 1, byteCount, 3 do 197 + local b1, b2, b3 = bytes[i], bytes[i + 1], bytes[i + 2] 198 + local packed = bit32.bor(bit32.lshift(b1, 16), bit32.lshift(b2 or 0, 8), b3 or 0) 199 + 200 + output[c] = encode_char_set[bit32.rshift(bit32.band(packed, 0xfc0000), 0x12)] 201 + output[c + 1] = encode_char_set[bit32.rshift(bit32.band(packed, 0x3f000), 0xc)] 202 + output[c + 2] = b2 and encode_char_set[bit32.rshift(bit32.band(packed, 0xfc0), 0x6)] or 0x3d 203 + output[c + 3] = b3 and encode_char_set[bit32.band(packed, 0x3f)] or 0x3d 204 + 205 + c = c + 4 206 + end 207 + c = c - 1 208 + 209 + return coroutine.wrap(function() 210 + local realChunkLength = chunkLength - 1 211 + for i = 1, c, chunkLength do 212 + local chunk = string.char(table.unpack(output, i, math.min(c, i + realChunkLength))) 213 + coroutine.yield(chunk) 214 + end 215 + end) 216 + end 217 + 218 + local function exportHexChunk(chunkLength) 219 + assert(type(chunkLength) == "number", "argument #1 to BitBuffer.exportHexChunk should be a number") 220 + assert(chunkLength > 0, "argument #1 to BitBuffer.exportHexChunk should be above zero") 221 + assert(chunkLength % 1 == 0, "argument #1 to BitBuffer.exportHexChunk should be an integer") 222 + 223 + local halfLength = math.floor(chunkLength / 2) 224 + 225 + if chunkLength % 2 == 0 then 226 + return coroutine.wrap(function() 227 + local output = {} --! 228 + for i = 1, byteCount, halfLength do 229 + for c = 0, halfLength - 1 do 230 + output[c] = byte_to_hex[bytes[i + c]] 231 + end 232 + coroutine.yield(table.concat(output, "", 0)) 233 + end 234 + end) 235 + else 236 + return coroutine.wrap(function() 237 + local output = { [0] = "" } --! 238 + local remainder = "" 239 + 240 + local i = 1 241 + while i <= byteCount do 242 + if remainder == "" then 243 + output[0] = "" 244 + for c = 0, halfLength - 1 do 245 + output[c + 1] = byte_to_hex[bytes[i + c]] 246 + end 247 + local endByte = byte_to_hex[bytes[i + halfLength]] 248 + if endByte then 249 + output[halfLength + 1] = string.sub(endByte, 1, 1) 250 + remainder = string.sub(endByte, 2, 2) 251 + end 252 + i = i + 1 253 + else 254 + output[0] = remainder 255 + for c = 0, halfLength - 1 do 256 + output[c + 1] = byte_to_hex[bytes[i + c]] 257 + end 258 + output[halfLength + 1] = "" 259 + remainder = "" 260 + end 261 + 262 + coroutine.yield(table.concat(output, "", 0)) 263 + i = i + halfLength 264 + end 265 + end) 266 + end 267 + end 268 + 269 + local function crc32() 270 + local crc = 0xffffffff -- 2^32 271 + 272 + for _, v in ipairs(bytes) do 273 + local poly = crc32_poly_lookup[bit32.band(bit32.bxor(crc, v), 255)] 274 + crc = bit32.bxor(bit32.rshift(crc, 8), poly) 275 + end 276 + 277 + return bit32.bnot(crc) % 0xffffffff -- 2^32 278 + end 279 + 280 + local function getLength() 281 + return bitCount 282 + end 283 + 284 + local function getByteLength() 285 + return byteCount 286 + end 287 + 288 + local function getPointer() 289 + -- This function gets the value of the pointer. This is self-explanatory. 290 + return pointer 291 + end 292 + 293 + local function setPointer(n) 294 + assert(type(n) == "number", "argument #1 to BitBuffer.setPointer should be a number") 295 + assert(n >= 0, "argument #1 to BitBuffer.setPointer should be zero or higher") 296 + assert(n % 1 == 0, "argument #1 to BitBuffer.setPointer should be an integer") 297 + assert(n <= bitCount, "argument #1 to BitBuffer.setPointerByte should within range of the buffer") 298 + -- This function sets the value of pointer. This is self-explanatory. 299 + pointer = n 300 + pointerByte = math.floor(n / 8) + 1 301 + end 302 + 303 + local function setPointerFromEnd(n) 304 + assert(type(n) == "number", "argument #1 to BitBuffer.setPointerFromEnd should be a number") 305 + assert(n >= 0, "argument #1 to BitBuffer.setPointerFromEnd should be zero or higher") 306 + assert(n % 1 == 0, "argument #1 to BitBuffer.setPointerFromEnd should be an integer") 307 + assert(n <= bitCount, "argument #1 to BitBuffer.setPointerFromEnd should within range of the buffer") 308 + 309 + pointer = bitCount - n 310 + pointerByte = math.floor(pointer / 8 + 1) 311 + end 312 + 313 + local function getPointerByte() 314 + return pointerByte 315 + end 316 + 317 + local function setPointerByte(n) 318 + assert(type(n) == "number", "argument #1 to BitBuffer.setPointerByte should be a number") 319 + assert(n > 0, "argument #1 to BitBuffer.setPointerByte should be positive") 320 + assert(n % 1 == 0, "argument #1 to BitBuffer.setPointerByte should be an integer") 321 + assert(n <= byteCount, "argument #1 to BitBuffer.setPointerByte should be within range of the buffer") 322 + -- Sets the value of the pointer in bytes instead of bits 323 + pointer = n * 8 324 + pointerByte = n 325 + end 326 + 327 + local function setPointerByteFromEnd(n) 328 + assert(type(n) == "number", "argument #1 to BitBuffer.setPointerByteFromEnd should be a number") 329 + assert(n >= 0, "argument #1 to BitBuffer.setPointerByteFromEnd should be zero or higher") 330 + assert(n % 1 == 0, "argument #1 to BitBuffer.setPointerByteFromEnd should be an integer") 331 + assert(n <= byteCount, "argument #1 to BitBuffer.setPointerByteFromEnd should be within range of the buffer") 332 + 333 + pointerByte = byteCount - n 334 + pointer = pointerByte * 8 335 + end 336 + 337 + local function isFinished() 338 + return pointer == bitCount 339 + end 340 + 341 + local function writeBits(...) 342 + -- The first of two main functions for the actual 'writing' of the bitbuffer. 343 + -- This function takes a vararg of 1s and 0s and writes them to the buffer. 344 + local bitN = select("#", ...) 345 + if bitN == 0 then 346 + return 347 + end -- Throwing here seems unnecessary 348 + bitCount = bitCount + bitN 349 + local packed = table.pack(...) 350 + for _, v in ipairs(packed) do 351 + assert(v == 1 or v == 0, "arguments to BitBuffer.writeBits should be either 1 or 0") 352 + if bits == 0 then -- If the bit count is 0, increment the byteCount 353 + -- This is the case at the beginning of the buffer as well as when the the buffer reaches 7 bits, 354 + -- so it's done at the beginning of the loop. 355 + byteCount = byteCount + 1 356 + end 357 + lastByte = lastByte + (v == 1 and powers_of_2[7 - bits] or 0) -- Add the current bit to lastByte, from right to left 358 + bits = bits + 1 359 + if bits == 8 then -- If the bit count is 8, set it to 0, write lastByte to the byte list, and set lastByte to 0 360 + bits = 0 361 + bytes[byteCount] = lastByte 362 + lastByte = 0 363 + end 364 + end 365 + if bits ~= 0 then -- If there are some bits in lastByte, it has to be put into lastByte 366 + -- If this is done regardless of the bit count, there might be a trailing zero byte 367 + bytes[byteCount] = lastByte 368 + end 369 + end 370 + 371 + local function writeByte(n) 372 + --assert(type(n) == "number", "argument #1 to BitBuffer.writeByte should be a number") 373 + --assert(n >= 0 and n <= 255, "argument #1 to BitBuffer.writeByte should be in the range [0, 255]") 374 + --assert(n % 1 == 0, "argument #1 to BitBuffer.writeByte should be an integer") 375 + 376 + -- The second of two main functions for the actual 'writing' of the bitbuffer. 377 + -- This function takes a byte (an 8-bit integer) and writes it to the buffer. 378 + if bits == 0 then 379 + -- If there aren't any free-floating bits, this is easy. 380 + byteCount = byteCount + 1 381 + bytes[byteCount] = n 382 + else 383 + local nibble = bit32.rshift(n, bits) -- Shift `bits` number of bits out of `n` (they go into the aether) 384 + bytes[byteCount] = lastByte + nibble -- Manually set the most recent byte to the lastByte + the front part of `n` 385 + byteCount = byteCount + 1 386 + lastByte = bit32.band(bit32.lshift(n, 8 - bits), 255) -- Shift `n` forward `8-bits` and get what remains in the first 8 bits 387 + bytes[byteCount] = lastByte 388 + end 389 + bitCount = bitCount + 8 -- Increment the bit counter 390 + end 391 + 392 + local function writeBytesFast(tab) 393 + assert(bits == 0, "writeBytesFast can only work for whole byte streams") 394 + local count = #tab 395 + table.move(tab, 1 , count, byteCount + 1, bytes) 396 + byteCount+= count 397 + bitCount += count * 8 398 + end 399 + 400 + local function writeUnsigned(width, n) 401 + assert(type(width) == "number", "argument #1 to BitBuffer.writeUnsigned should be a number") 402 + assert(width >= 1 and width <= 64, "argument #1 to BitBuffer.writeUnsigned should be in the range [1, 64]") 403 + assert(width % 1 == 0, "argument #1 to BitBuffer.writeUnsigned should be an integer") 404 + 405 + assert(type(n) == "number", "argument #2 to BitBuffer.writeUnsigned should be a number") 406 + assert(n >= 0 and n <= powers_of_2[width] - 1, "argument #2 to BitBuffer.writeUnsigned is out of range") 407 + assert(n % 1 == 0, "argument #2 to BitBuffer.writeUnsigned should be an integer") 408 + -- Writes unsigned integers of arbitrary length to the buffer. 409 + -- This is the first function that uses other functions in the buffer to function. 410 + -- This is done because the space taken up would be rather large for very little performance gain. 411 + 412 + -- Get the number of bytes and number of floating bits in the specified width 413 + local bytesInN, bitsInN = math.floor(width / 8), width % 8 414 + local extractedBits = table.create(bitsInN) --! 415 + 416 + -- If the width is less than or equal to 32-bits, bit32 can be used without any problem. 417 + if width <= 32 then 418 + -- Counting down from the left side, the bytes are written to the buffer 419 + local c = width 420 + for _ = 1, bytesInN do 421 + c = c - 8 422 + writeByte(bit32.extract(n, c, 8)) 423 + end 424 + -- Any remaining bits are stored in an array 425 + for i = bitsInN - 1, 0, -1 do 426 + extractedBits[bitsInN - i] = BOOL_TO_BIT[bit32.btest(n, powers_of_2[i])] 427 + end 428 + -- Said array is then used to write them to the buffer 429 + writeBits(table.unpack(extractedBits)) 430 + else 431 + -- If the width is greater than 32, the number has to be divided up into a few 32-bit or less numbers 432 + local leastSignificantChunk = n % 0x100000000 -- Get bits 0-31 (counting from the right side). 0x100000000 is 2^32. 433 + local mostSignificantChunk = math.floor(n / 0x100000000) -- Get any remaining bits by manually right shifting by 32 bits 434 + 435 + local c = width - 32 -- The number of bits in mostSignificantChunk is variable, but a counter is still needed 436 + for _ = 1, bytesInN - 4 do -- 32 bits is 4 bytes 437 + c = c - 8 438 + writeByte(bit32.extract(mostSignificantChunk, c, 8)) 439 + end 440 + -- `bitsInN` is always going to be the number of spare bits in `mostSignificantChunk` 441 + -- which comes before `leastSignificantChunk` 442 + for i = bitsInN - 1, 0, -1 do 443 + extractedBits[bitsInN - i] = BOOL_TO_BIT[bit32.btest(mostSignificantChunk, powers_of_2[i])] 444 + end 445 + writeBits(table.unpack(extractedBits)) 446 + 447 + for i = 3, 0, -1 do -- Then of course, write all 4 bytes of leastSignificantChunk 448 + writeByte(bit32.extract(leastSignificantChunk, i * 8, 8)) 449 + end 450 + end 451 + end 452 + 453 + local function writeSigned(width, n) 454 + assert(type(width) == "number", "argument #1 to BitBuffer.writeSigned should be a number") 455 + assert(width >= 2 and width <= 64, "argument #1 to BitBuffer.writeSigned should be in the range [2, 64]") 456 + assert(width % 1 == 0, "argument #1 to BitBuffer.writeSigned should be an integer") 457 + 458 + assert(type(n) == "number", "argument #2 to BitBuffer.writeSigned should be a number") 459 + assert( 460 + n >= -powers_of_2[width - 1] and n <= powers_of_2[width - 1] - 1, 461 + "argument #2 to BitBuffer.writeSigned is out of range" 462 + ) 463 + assert(n % 1 == 0, "argument #2 to BitBuffer.writeSigned should be an integer") 464 + -- Writes signed integers of arbitrary length to the buffer. 465 + -- These integers are stored using two's complement. 466 + -- Essentially, this means the first bit in the number is used to store whether it's positive or negative 467 + -- If the number is positive, it's stored normally. 468 + -- If it's negative, the number that's stored is equivalent to the max value of the width + the number 469 + if n >= 0 then 470 + writeBits(0) 471 + writeUnsigned(width - 1, n) -- One bit is used for the sign, so the stored number's width is actually width-1 472 + else 473 + writeBits(1) 474 + writeUnsigned(width - 1, powers_of_2[width - 1] + n) 475 + end 476 + end 477 + 478 + local function writeFloat(exponentWidth, mantissaWidth, n) 479 + assert(type(exponentWidth) == "number", "argument #1 to BitBuffer.writeFloat should be a number") 480 + assert( 481 + exponentWidth >= 1 and exponentWidth <= 64, 482 + "argument #1 to BitBuffer.writeFloat should be in the range [1, 64]" 483 + ) 484 + assert(exponentWidth % 1 == 0, "argument #1 to BitBuffer.writeFloat should be an integer") 485 + 486 + assert(type(mantissaWidth) == "number", "argument #2 to BitBuffer.writeFloat should be a number") 487 + assert( 488 + mantissaWidth >= 1 and mantissaWidth <= 64, 489 + "argument #2 to BitBuffer.writeFloat should be in the range [1, 64]" 490 + ) 491 + assert(mantissaWidth % 1 == 0, "argument #2 to BitBuffer.writeFloat should be an integer") 492 + 493 + assert(type(n) == "number", "argument #3 to BitBuffer.writeFloat should be a number") 494 + 495 + -- Given that floating point numbers are particularly hard to grasp, this function is annotated heavily. 496 + -- This stackoverflow answer is a great help if you just want an overview: 497 + -- https://stackoverflow.com/a/7645264 498 + -- Essentially, floating point numbers are scientific notation in binary. 499 + -- Instead of expressing numbers like 10^e*m, floating points instead use 2^e*m. 500 + -- For the sake of this function, `e` is referred to as `exponent` and `m` is referred to as `mantissa`. 501 + 502 + -- Floating point numbers are stored in memory as a sequence of bitfields. 503 + -- Every float has a set number of bits assigned for exponent values and mantissa values, along with one bit for the sign. 504 + -- The order of the bits in the memory is: sign, exponent, mantissa. 505 + 506 + -- Given that floating points have to represent numbers less than zero as well as those above them, 507 + -- some parts of the exponent are set aside to be negative exponents. In the case of floats, 508 + -- this is about half of the values. To calculate the 'real' value of an exponent a number that's half of the max exponent 509 + -- is added to the exponent. More info can be found here: https://stackoverflow.com/q/2835278 510 + -- This number is called the 'bias'. 511 + local bias = powers_of_2[exponentWidth - 1] - 1 512 + 513 + local sign = n < 0 -- The sign of a number is important. 514 + -- In this case, since we're using a lookup table for the sign bit, we want `sign` to indicate if the number is negative or not. 515 + n = math.abs(n) -- But it's annoying to work with negative numbers and the sign isn't important for decomposition. 516 + 517 + -- Lua has a function specifically for decomposing (or taking apart) a floating point number into its pieces. 518 + -- These pieces, as listed above, are the mantissa and exponent. 519 + local mantissa, exponent = math.frexp(n) 520 + 521 + -- Before we go further, there are some concepts that get special treatment in the floating point format. 522 + -- These have to be accounted for before normal floats are written to the buffer. 523 + 524 + if n == math.huge then 525 + -- Positive and negative infinities are specifically indicated with an exponent that's all 1s 526 + -- and a mantissa that's all 0s. 527 + writeBits(BOOL_TO_BIT[sign]) -- As previously said, there's a bit for the sign 528 + writeUnsigned(exponentWidth, powers_of_2[exponentWidth] - 1) -- Then comes the exponent 529 + writeUnsigned(mantissaWidth, 0) -- And finally the mantissa 530 + return 531 + elseif n ~= n then 532 + -- NaN is indicated with an exponent that's all 1s and a mantissa that isn't 0. 533 + -- In theory, the individual bits of NaN should be maintained but Lua doesn't allow that, 534 + -- so the mantissa is just being set to 10 for no particular reason. 535 + writeBits(BOOL_TO_BIT[sign]) 536 + writeUnsigned(exponentWidth, powers_of_2[exponentWidth] - 1) 537 + writeUnsigned(mantissaWidth, 10) 538 + return 539 + elseif n == 0 then 540 + -- Zero is represented with an exponent that's zero and a mantissa that's also zero. 541 + -- Lua doesn't have a signed zero, so that translates to the entire number being all 0s. 542 + writeUnsigned(exponentWidth + mantissaWidth + 1, 0) 543 + return 544 + elseif exponent + bias <= 1 then 545 + -- Subnormal numbers are a number that's exponent (when biased) is zero. 546 + -- Because of a quirk with the way Lua and C decompose numbers, subnormal numbers actually have an exponent of one when biased. 547 + 548 + -- The process behind this is explained below, so for the sake of brevity it isn't explained here. 549 + -- The only difference between processing subnormal and normal numbers is with the mantissa. 550 + -- As subnormal numbers always start with a 0 (in binary), it doesn't need to be removed or shifted out 551 + -- so it's a simple shift and round. 552 + mantissa = math.floor(mantissa * powers_of_2[mantissaWidth] + 0.5) 553 + 554 + writeBits(BOOL_TO_BIT[sign]) 555 + writeUnsigned(exponentWidth, 0) -- Subnormal numbers always have zero for an exponent 556 + writeUnsigned(mantissaWidth, mantissa) 557 + return 558 + end 559 + 560 + -- In every normal case, the mantissa of a number will have a 1 directly after the decimal point (in binary). 561 + -- As an example, 0.15625 has a mantissa of 0.625, which is 0.101 in binary. The 1 after the decimal point is always there. 562 + -- That means that for the sake of space efficiency that can be left out. 563 + -- The bit has to be removed. This uses subtraction and multiplication to do it since bit32 is for integers only. 564 + -- The mantissa is then shifted up by the width of the mantissa field and rounded. 565 + mantissa = math.floor((mantissa - 0.5) * 2 * powers_of_2[mantissaWidth] + 0.5) 566 + -- (The first fraction bit is equivalent to 0.5 in decimal) 567 + 568 + -- After that, it's just a matter of writing to the stream: 569 + writeBits(BOOL_TO_BIT[sign]) 570 + writeUnsigned(exponentWidth, exponent + bias - 1) -- The bias is added to the exponent to properly offset it 571 + -- The extra -1 is added because Lua, for whatever reason, doesn't normalize its results 572 + -- This is the cause of the 'quirk' mentioned when handling subnormal number 573 + -- As an example, math.frexp(0.15625) = 0.625, -2 574 + -- This means that 0.15625 = 0.625*2^-2 575 + -- Or, in binary: 0.00101 = 0.101 >> 2 576 + -- This is a correct statement but the actual result is meant to be: 577 + -- 0.00101 = 1.01 >> 3, or 0.15625 = 1.25*2^-3 578 + -- A small but important distinction that has made writing this module frustrating because no documentation notates this. 579 + writeUnsigned(mantissaWidth, mantissa) 580 + end 581 + 582 + local function writeBase64(input) 583 + assert(type(input) == "string", "argument #1 to BitBuffer.writeBase64 should be a string") 584 + assert( 585 + not string.find(input, "[^%w%+/=]"), 586 + "argument #1 to BitBuffer.writeBase64 should only contain valid base64 characters" 587 + ) 588 + 589 + for i = 1, #input, 4 do 590 + local b1, b2, b3, b4 = string.byte(input, i, i + 3) 591 + 592 + b1 = decode_char_set[b1] 593 + b2 = decode_char_set[b2] 594 + b3 = decode_char_set[b3] 595 + b4 = decode_char_set[b4] 596 + 597 + local packed = bit32.bor(bit32.lshift(b1, 18), bit32.lshift(b2, 12), bit32.lshift(b3 or 0, 6), b4 or 0) 598 + 599 + writeByte(bit32.rshift(packed, 16)) 600 + if not b3 then 601 + break 602 + end 603 + writeByte(bit32.band(bit32.rshift(packed, 8), 0xff)) 604 + if not b4 then 605 + break 606 + end 607 + writeByte(bit32.band(packed, 0xff)) 608 + end 609 + end 610 + 611 + local function writeString(str) 612 + assert(type(str) == "string", "argument #1 to BitBuffer.writeString should be a string") 613 + -- The default mode of writing strings is length-prefixed. 614 + -- This means that the length of the string is written before the contents of the string. 615 + -- For the sake of speed it has to be an even byte. 616 + -- One and two bytes is too few characters (255 bytes and 65535 bytes respectively), so it has to be higher. 617 + -- Three bytes is roughly 16.77mb, and four is roughly 4.295gb. Given this is Lua and is thus unlikely to be processing strings 618 + -- that large, this function uses three bytes, or 24 bits for the length 619 + 620 + writeUnsigned(24, #str) 621 + 622 + for i = 1, #str do 623 + writeByte(string.byte(str, i, i)) 624 + end 625 + end 626 + 627 + local function writeTerminatedString(str) 628 + assert(type(str) == "string", "argument #1 to BitBuffer.writeTerminatedString should be a string") 629 + -- This function writes strings that are null-terminated. 630 + -- Null-terminated strings are strings of bytes that end in a 0 byte (\0) 631 + -- This isn't the default because it doesn't allow for binary data to be written cleanly. 632 + 633 + for i = 1, #str do 634 + writeByte(string.byte(str, i, i)) 635 + end 636 + writeByte(0) 637 + end 638 + 639 + local function writeSetLengthString(str) 640 + assert(type(str) == "string", "argument #1 to BitBuffer.writeSetLengthString should be a string") 641 + -- This function writes strings as a pure string of bytes 642 + -- It doesn't store any data about the length of the string, 643 + -- so reading it requires knowledge of how many characters were stored 644 + 645 + for i = 1, #str do 646 + writeByte(string.byte(str, i, i)) 647 + end 648 + end 649 + 650 + local function writeField(...) 651 + -- This is equivalent to having a writeBitfield function. 652 + -- It combines all of the passed 'bits' into an unsigned number, then writes it. 653 + local field = 0 654 + local bools = table.pack(...) 655 + for i = 1, bools.n do 656 + field = field * 2 -- Shift `field`. Equivalent to field<<1. At the beginning of the loop to avoid an extra shift. 657 + 658 + local v = bools[i] 659 + if v then 660 + field = field + 1 -- If the bit is truthy, turn it on (it defaults to off so it's fine to not have a branch) 661 + end 662 + end 663 + 664 + writeUnsigned(bools.n, field) 665 + end 666 + 667 + -- All write functions below here are shorthands. For the sake of performance, these functions are implemented manually. 668 + -- As an example, while it would certainly be easier to make `writeInt16(n)` just call `writeUnsigned(16, n), 669 + -- it's more performant to just manually call writeByte twice for it. 670 + 671 + local function writeUInt8(n) 672 + assert(type(n) == "number", "argument #1 to BitBuffer.writeUInt8 should be a number") 673 + assert(n >= 0 and n <= 255, "argument #1 to BitBuffer.writeUInt8 should be in the range [0, 255]") 674 + assert(n % 1 == 0, "argument #1 to BitBuffer.writeUInt8 should be an integer") 675 + 676 + writeByte(n) 677 + end 678 + 679 + local function writeUInt16(n) 680 + assert(type(n) == "number", "argument #1 to BitBuffer.writeUInt16 should be a number") 681 + assert(n >= 0 and n <= 65535, "argument #1 to BitBuffer.writeInt16 should be in the range [0, 65535]") 682 + assert(n % 1 == 0, "argument #1 to BitBuffer.writeUInt16 should be an integer") 683 + 684 + writeByte(bit32.rshift(n, 8)) 685 + writeByte(bit32.band(n, 255)) 686 + end 687 + 688 + local function writeUInt32(n) 689 + assert(type(n) == "number", "argument #1 to BitBuffer.writeUInt32 should be a number") 690 + assert( 691 + n >= 0 and n <= 4294967295, 692 + "argument #1 to BitBuffer.writeUInt32 should be in the range [0, 4294967295]" 693 + ) 694 + assert(n % 1 == 0, "argument #1 to BitBuffer.writeUInt32 should be an integer") 695 + 696 + writeByte(bit32.rshift(n, 24)) 697 + writeByte(bit32.band(bit32.rshift(n, 16), 255)) 698 + writeByte(bit32.band(bit32.rshift(n, 8), 255)) 699 + writeByte(bit32.band(n, 255)) 700 + end 701 + 702 + local function writeInt8(n) 703 + assert(type(n) == "number", "argument #1 to BitBuffer.writeInt8 should be a number") 704 + assert(n >= -128 and n <= 127, "argument #1 to BitBuffer.writeInt8 should be in the range [-128, 127]") 705 + assert(n % 1 == 0, "argument #1 to BitBuffer.writeInt8 should be an integer") 706 + 707 + if n < 0 then 708 + n = (128 + n) + 128 709 + end 710 + 711 + writeByte(n) 712 + end 713 + 714 + local function writeInt16(n) 715 + assert(type(n) == "number", "argument #1 to BitBuffer.writeInt16 should be a number") 716 + assert(n >= -32768 and n <= 32767, "argument #1 to BitBuffer.writeInt16 should be in the range [-32768, 32767]") 717 + assert(n % 1 == 0, "argument #1 to BitBuffer.writeInt16 should be an integer") 718 + 719 + if n < 0 then 720 + n = (32768 + n) + 32768 721 + end 722 + 723 + writeByte(bit32.rshift(n, 8)) 724 + writeByte(bit32.band(n, 255)) 725 + end 726 + 727 + local function writeInt32(n) 728 + assert(type(n) == "number", "argument #1 to BitBuffer.writeInt32 should be a number") 729 + assert( 730 + n >= -2147483648 and n <= 2147483647, 731 + "argument #1 to BitBuffer.writeInt32 should be in the range [-2147483648, 2147483647]" 732 + ) 733 + assert(n % 1 == 0, "argument #1 to BitBuffer.writeInt32 should be an integer") 734 + 735 + if n < 0 then 736 + n = (2147483648 + n) + 2147483648 737 + end 738 + 739 + writeByte(bit32.rshift(n, 24)) 740 + writeByte(bit32.band(bit32.rshift(n, 16), 255)) 741 + writeByte(bit32.band(bit32.rshift(n, 8), 255)) 742 + writeByte(bit32.band(n, 255)) 743 + end 744 + 745 + local function writeFloat16(n) 746 + --assert(type(n) == "number", "argument #1 to BitBuffer.writeFloat16 should be a number") 747 + 748 + local sign = n < 0 749 + n = math.abs(n) 750 + 751 + local mantissa, exponent = math.frexp(n) 752 + 753 + if n == math.huge then 754 + if sign then 755 + writeByte(252) -- 11111100 756 + else 757 + writeByte(124) -- 01111100 758 + end 759 + writeByte(0) -- 00000000 760 + return 761 + elseif n ~= n then 762 + -- 01111111 11111111 763 + writeByte(127) 764 + writeByte(255) 765 + return 766 + elseif n == 0 then 767 + writeByte(0) 768 + writeByte(0) 769 + return 770 + elseif exponent + 15 <= 1 then -- Bias for halfs is 15 771 + mantissa = math.floor(mantissa * 1024 + 0.5) 772 + if sign then 773 + writeByte(128 + bit32.rshift(mantissa, 8)) -- Sign bit, 5 empty bits, 2 from mantissa 774 + else 775 + writeByte(bit32.rshift(mantissa, 8)) 776 + end 777 + writeByte(bit32.band(mantissa, 255)) -- Get last 8 bits from mantissa 778 + return 779 + end 780 + 781 + mantissa = math.floor((mantissa - 0.5) * 2048 + 0.5) 782 + 783 + -- The bias for halfs is 15, 15-1 is 14 784 + if sign then 785 + writeByte(128 + bit32.lshift(exponent + 14, 2) + bit32.rshift(mantissa, 8)) 786 + else 787 + writeByte(bit32.lshift(exponent + 14, 2) + bit32.rshift(mantissa, 8)) 788 + end 789 + writeByte(bit32.band(mantissa, 255)) 790 + end 791 + 792 + local function writeFloat32(n) 793 + --assert(type(n) == "number", "argument #1 to BitBuffer.writeFloat32 should be a number") 794 + 795 + local sign = n < 0 796 + n = math.abs(n) 797 + 798 + local mantissa, exponent = math.frexp(n) 799 + 800 + if n == math.huge then 801 + if sign then 802 + writeByte(255) -- 11111111 803 + else 804 + writeByte(127) -- 01111111 805 + end 806 + writeByte(128) -- 10000000 807 + writeByte(0) -- 00000000 808 + writeByte(0) -- 00000000 809 + return 810 + elseif n ~= n then 811 + -- 01111111 11111111 11111111 11111111 812 + writeByte(127) 813 + writeByte(255) 814 + writeByte(255) 815 + writeByte(255) 816 + return 817 + elseif n == 0 then 818 + writeByte(0) 819 + writeByte(0) 820 + writeByte(0) 821 + writeByte(0) 822 + return 823 + elseif exponent + 127 <= 1 then -- bias for singles is 127 824 + mantissa = math.floor(mantissa * 8388608 + 0.5) 825 + if sign then 826 + writeByte(128) -- Sign bit, 7 empty bits for exponent 827 + else 828 + writeByte(0) 829 + end 830 + writeByte(bit32.rshift(mantissa, 16)) 831 + writeByte(bit32.band(bit32.rshift(mantissa, 8), 255)) 832 + writeByte(bit32.band(mantissa, 255)) 833 + return 834 + end 835 + 836 + mantissa = math.floor((mantissa - 0.5) * 16777216 + 0.5) 837 + 838 + -- 127-1 = 126 839 + if sign then -- sign + 7 exponent 840 + writeByte(128 + bit32.rshift(exponent + 126, 1)) 841 + else 842 + writeByte(bit32.rshift(exponent + 126, 1)) 843 + end 844 + writeByte(bit32.band(bit32.lshift(exponent + 126, 7), 255) + bit32.rshift(mantissa, 16)) -- 1 exponent + 7 mantissa 845 + writeByte(bit32.band(bit32.rshift(mantissa, 8), 255)) -- 8 mantissa 846 + writeByte(bit32.band(mantissa, 255)) -- 8 mantissa 847 + end 848 + 849 + local function writeFloat64(n) 850 + assert(type(n) == "number", "argument #1 to BitBuffer.writeFloat64 should be a number") 851 + 852 + local sign = n < 0 853 + n = math.abs(n) 854 + 855 + local mantissa, exponent = math.frexp(n) 856 + 857 + if n == math.huge then 858 + if sign then 859 + writeByte(255) -- 11111111 860 + else 861 + writeByte(127) -- 01111111 862 + end 863 + writeByte(240) -- 11110000 864 + writeByte(0) -- 00000000 865 + writeByte(0) -- 00000000 866 + writeByte(0) -- 00000000 867 + writeByte(0) -- 00000000 868 + writeByte(0) -- 00000000 869 + writeByte(0) -- 00000000 870 + return 871 + elseif n ~= n then 872 + -- 01111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 873 + writeByte(127) 874 + writeByte(255) 875 + writeByte(255) 876 + writeByte(255) 877 + writeByte(255) 878 + writeByte(255) 879 + writeByte(255) 880 + writeByte(255) 881 + return 882 + elseif n == 0 then 883 + writeByte(0) 884 + return 885 + elseif exponent + 1023 <= 1 then -- bias for doubles is 1023 886 + mantissa = math.floor(mantissa * 4503599627370496 + 0.5) 887 + if sign then 888 + writeByte(128) -- Sign bit, 7 empty bits for exponent 889 + else 890 + writeByte(0) 891 + end 892 + 893 + -- This is labeled better below 894 + local leastSignificantChunk = mantissa % 0x100000000 -- 32 bits 895 + local mostSignificantChunk = math.floor(mantissa / 0x100000000) -- 20 bits 896 + 897 + writeByte(bit32.rshift(mostSignificantChunk, 16)) 898 + writeByte(bit32.band(bit32.rshift(mostSignificantChunk, 8), 255)) 899 + writeByte(bit32.band(mostSignificantChunk, 255)) 900 + writeByte(bit32.rshift(leastSignificantChunk, 24)) 901 + writeByte(bit32.band(bit32.rshift(leastSignificantChunk, 16), 255)) 902 + writeByte(bit32.band(bit32.rshift(leastSignificantChunk, 8), 255)) 903 + writeByte(bit32.band(leastSignificantChunk, 255)) 904 + return 905 + end 906 + 907 + mantissa = math.floor((mantissa - 0.5) * 9007199254740992 + 0.5) 908 + 909 + --1023-1 = 1022 910 + if sign then 911 + writeByte(128 + bit32.rshift(exponent + 1022, 4)) -- shift out 4 of the bits in exponent 912 + else 913 + writeByte(bit32.rshift(exponent + 1022, 4)) -- 01000001 0110 914 + end 915 + -- Things start to get a bit wack here because the mantissa is 52 bits, so bit32 *can't* be used. 916 + -- As the Offspring once said... You gotta keep 'em seperated. 917 + local leastSignificantChunk = mantissa % 0x100000000 -- 32 bits 918 + local mostSignificantChunk = math.floor(mantissa / 0x100000000) -- 20 bits 919 + 920 + -- First, the last 4 bits of the exponent and the first 4 bits of the mostSignificantChunk: 921 + writeByte(bit32.band(bit32.lshift(exponent + 1022, 4), 255) + bit32.rshift(mostSignificantChunk, 16)) 922 + -- Then, the next 16 bits: 923 + writeByte(bit32.band(bit32.rshift(mostSignificantChunk, 8), 255)) 924 + writeByte(bit32.band(mostSignificantChunk, 255)) 925 + -- Then... 4 bytes of the leastSignificantChunk 926 + writeByte(bit32.rshift(leastSignificantChunk, 24)) 927 + writeByte(bit32.band(bit32.rshift(leastSignificantChunk, 16), 255)) 928 + writeByte(bit32.band(bit32.rshift(leastSignificantChunk, 8), 255)) 929 + writeByte(bit32.band(leastSignificantChunk, 255)) 930 + end 931 + 932 + -- All write functions below here are Roblox specific datatypes. 933 + 934 + local function writeBrickColor(n) 935 + assert(typeof(n) == "BrickColor", "argument #1 to BitBuffer.writeBrickColor should be a BrickColor") 936 + 937 + writeUInt16(n.Number) 938 + end 939 + 940 + local function writeColor3(c3) 941 + assert(typeof(c3) == "Color3", "argument #1 to BitBuffer.writeColor3 should be a Color3") 942 + 943 + writeByte(math.floor(c3.R * 0xff + 0.5)) 944 + writeByte(math.floor(c3.G * 0xff + 0.5)) 945 + writeByte(math.floor(c3.B * 0xff + 0.5)) 946 + end 947 + 948 + local function writeCFrame(cf) 949 + assert(typeof(cf) == "CFrame", "argument #1 to BitBuffer.writeCFrame should be a CFrame") 950 + -- CFrames can be rather lengthy (if stored naively, they would each be 48 bytes long) so some optimization is done here. 951 + -- Specifically, if a CFrame is axis-aligned (it's only rotated in 90 degree increments), the rotation matrix isn't stored. 952 + -- Instead, an 'id' for its orientation is generated and that's stored instead of the rotation. 953 + -- This means that for the most common rotations, only 13 bytes are used. 954 + -- The downside is that non-axis-aligned CFrames use 49 bytes instead of 48, but that's a small price to pay. 955 + 956 + local upVector = cf.UpVector 957 + local rightVector = cf.RightVector 958 + 959 + -- This is an easy trick to check if a CFrame is axis-aligned: 960 + -- Essentially, in order for a vector to be axis-aligned, two of the components have to be 0 961 + -- This means that the dot product between the vector and a vector of all 1s will be 1 (0*x = 0) 962 + -- Since these are all unit vectors, there is no other combination that results in 1. 963 + local rightAligned = math.abs(rightVector:Dot(ONES_VECTOR)) 964 + local upAligned = math.abs(upVector:Dot(ONES_VECTOR)) 965 + -- At least one of these two vectors is guaranteed to not result in 0. 966 + 967 + local axisAligned = (math.abs(1 - rightAligned) < 0.00001 or rightAligned == 0) 968 + and (math.abs(1 - upAligned) < 0.00001 or upAligned == 0) 969 + -- There are limitations to `math.abs(a-b) < epsilon` but they're not relevant: 970 + -- The range of numbers is [0, 1] and this just needs to know if the number is approximately 1 971 + 972 + --todo special code for quaternions (0x01 in Roblox's format, would clash with 0x00 here) 973 + if axisAligned then 974 + local position = cf.Position 975 + -- The ID of an orientation is generated through what can best be described as 'hand waving'; 976 + -- This is how Roblox does it and it works, so it was chosen to do it this way too. 977 + local rightNormal, upNormal 978 + for i = 0, 5 do 979 + local v = NORMAL_ID_VECTORS[i] 980 + if 1 - v:Dot(rightVector) < 0.00001 then 981 + rightNormal = i 982 + end 983 + if 1 - v:Dot(upVector) < 0.00001 then 984 + upNormal = i 985 + end 986 + end 987 + -- The ID generated here is technically off by 1 from what Roblox would store, but that's not important 988 + -- It just means that 0x02 is actually 0x01 for the purposes of this module's implementation. 989 + writeByte(rightNormal * 6 + upNormal) 990 + writeFloat32(position.X) 991 + writeFloat32(position.Y) 992 + writeFloat32(position.Z) 993 + else 994 + -- If the CFrame isn't axis-aligned, the entire rotation matrix has to be written... 995 + writeByte(0) -- Along with a byte to indicate the matrix was written. 996 + local x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22 = cf:GetComponents() 997 + writeFloat32(x) 998 + writeFloat32(y) 999 + writeFloat32(z) 1000 + writeFloat32(r00) 1001 + writeFloat32(r01) 1002 + writeFloat32(r02) 1003 + writeFloat32(r10) 1004 + writeFloat32(r11) 1005 + writeFloat32(r12) 1006 + writeFloat32(r20) 1007 + writeFloat32(r21) 1008 + writeFloat32(r22) 1009 + end 1010 + end 1011 + 1012 + local function writeVector3(v3) 1013 + --assert(typeof(v3) == "Vector3", "argument #1 to BitBuffer.writeVector3 should be a Vector3") 1014 + 1015 + writeFloat32(v3.X) 1016 + writeFloat32(v3.Y) 1017 + writeFloat32(v3.Z) 1018 + end 1019 + 1020 + local function writeVector2(v2) 1021 + assert(typeof(v2) == "Vector2", "argument #1 to BitBuffer.writeVector2 should be a Vector2") 1022 + 1023 + writeFloat32(v2.X) 1024 + writeFloat32(v2.Y) 1025 + end 1026 + 1027 + local function writeUDim2(u2) 1028 + assert(typeof(u2) == "UDim2", "argument #1 to BitBuffer.writeUDim2 should be a UDim2") 1029 + 1030 + writeFloat32(u2.X.Scale) 1031 + writeInt32(u2.X.Offset) 1032 + writeFloat32(u2.Y.Scale) 1033 + writeInt32(u2.Y.Offset) 1034 + end 1035 + 1036 + local function writeUDim(u) 1037 + assert(typeof(u) == "UDim", "argument #1 to BitBuffer.writeUDim should be a UDim") 1038 + 1039 + writeFloat32(u.Scale) 1040 + writeInt32(u.Offset) 1041 + end 1042 + 1043 + local function writeRay(ray) 1044 + assert(typeof(ray) == "Ray", "argument #1 to BitBuffer.writeRay should be a Ray") 1045 + 1046 + writeFloat32(ray.Origin.X) 1047 + writeFloat32(ray.Origin.Y) 1048 + writeFloat32(ray.Origin.Z) 1049 + 1050 + writeFloat32(ray.Direction.X) 1051 + writeFloat32(ray.Direction.Y) 1052 + writeFloat32(ray.Direction.Z) 1053 + end 1054 + 1055 + local function writeRect(rect) 1056 + assert(typeof(rect) == "Rect", "argument #1 to BitBuffer.writeRect should be a Rect") 1057 + 1058 + writeFloat32(rect.Min.X) 1059 + writeFloat32(rect.Min.Y) 1060 + 1061 + writeFloat32(rect.Max.X) 1062 + writeFloat32(rect.Max.Y) 1063 + end 1064 + 1065 + local function writeRegion3(region) 1066 + assert(typeof(region) == "Region3", "argument #1 to BitBuffer.writeRegion3 should be a Region3") 1067 + 1068 + local min = region.CFrame.Position - (region.Size / 2) 1069 + local max = region.CFrame.Position + (region.Size / 2) 1070 + 1071 + writeFloat32(min.X) 1072 + writeFloat32(min.Y) 1073 + writeFloat32(min.Z) 1074 + 1075 + writeFloat32(max.X) 1076 + writeFloat32(max.Y) 1077 + writeFloat32(max.Z) 1078 + end 1079 + 1080 + local function writeEnum(enum) 1081 + assert(typeof(enum) == "EnumItem", "argument #1 to BitBuffer.writeEnum should be an EnumItem") 1082 + 1083 + -- Relying upon tostring is generally not good, but there's not any other options for this. 1084 + writeTerminatedString(tostring(enum.EnumType)) 1085 + writeUInt16(enum.Value) -- Optimistically assuming no Roblox Enum value will ever pass 65,535 1086 + end 1087 + 1088 + local function writeNumberRange(range) 1089 + assert(typeof(range) == "NumberRange", "argument #1 to BitBuffer.writeNumberRange should be a NumberRange") 1090 + 1091 + writeFloat32(range.Min) 1092 + writeFloat32(range.Max) 1093 + end 1094 + 1095 + local function writeNumberSequence(sequence) 1096 + assert( 1097 + typeof(sequence) == "NumberSequence", 1098 + "argument #1 to BitBuffer.writeNumberSequence should be a NumberSequence" 1099 + ) 1100 + 1101 + writeUInt32(#sequence.Keypoints) 1102 + for _, keypoint in ipairs(sequence.Keypoints) do 1103 + writeFloat32(keypoint.Time) 1104 + writeFloat32(keypoint.Value) 1105 + writeFloat32(keypoint.Envelope) 1106 + end 1107 + end 1108 + 1109 + local function writeColorSequence(sequence) 1110 + assert( 1111 + typeof(sequence) == "ColorSequence", 1112 + "argument #1 to BitBuffer.writeColorSequence should be a ColorSequence" 1113 + ) 1114 + 1115 + writeUInt32(#sequence.Keypoints) 1116 + for _, keypoint in ipairs(sequence.Keypoints) do 1117 + local c3 = keypoint.Value 1118 + writeFloat32(keypoint.Time) 1119 + writeByte(math.floor(c3.R * 0xff + 0.5)) 1120 + writeByte(math.floor(c3.G * 0xff + 0.5)) 1121 + writeByte(math.floor(c3.B * 0xff + 0.5)) 1122 + end 1123 + end 1124 + 1125 + -- These are the read functions for the 'abstract' data types. At the bottom, there are shorthand read functions. 1126 + 1127 + local function readBits(n) 1128 + assert(type(n) == "number", "argument #1 to BitBuffer.readBits should be a number") 1129 + assert(n > 0, "argument #1 to BitBuffer.readBits should be greater than zero") 1130 + assert(n % 1 == 0, "argument #1 to BitBuffer.readBits should be an integer") 1131 + 1132 + assert(pointer + n <= bitCount, "BitBuffer.readBits cannot read past the end of the stream") 1133 + 1134 + -- The first of two main functions for the actual 'reading' of the bitbuffer. 1135 + -- Reads `n` bits and returns an array of their values. 1136 + local output = table.create(n) --! 1137 + local byte = bytes[pointerByte] -- For the sake of efficiency, the current byte that the bits are coming from is stored 1138 + local c = pointer % 8 -- A counter is set with the current position of the pointer in the byte 1139 + for i = 1, n do 1140 + -- Then, it's as easy as moving through the bits of the byte 1141 + -- And getting the individiual bit values 1142 + local pow = powers_of_2[7 - c] 1143 + output[i] = BOOL_TO_BIT[bit32.btest(byte, pow)] -- Test if a bit is on by &ing it by 2^[bit position] 1144 + c = c + 1 1145 + if c == 8 then -- If the byte boundary is reached, increment pointerByte and store the new byte in `byte` 1146 + pointerByte = pointerByte + 1 1147 + byte = bytes[pointerByte] 1148 + c = 0 1149 + end 1150 + end 1151 + pointer = pointer + n -- Move the pointer forward 1152 + return output 1153 + end 1154 + 1155 + --Skip to the end of the current byte 1156 + local function skipStrayBits() 1157 + local c = pointer % 8 1158 + if (c > 0) then 1159 + pointer += 8 - c 1160 + pointerByte += 1 1161 + end 1162 + end 1163 + 1164 + local function readBytesFast() 1165 + return bytes 1166 + end 1167 + 1168 + 1169 + local function readByte() 1170 + assert(pointer + 8 <= bitCount, "BitBuffer.readByte cannot read past the end of the stream") 1171 + -- The second of two main functions for the actual 'reading' of the bitbuffer. 1172 + -- Reads a byte and returns it 1173 + local c = pointer % 8 -- How far into the pointerByte the pointer is 1174 + local byte1 = bytes[pointerByte] -- The pointerByte 1175 + pointer = pointer + 8 1176 + if c == 0 then -- Trivial if the pointer is at the beginning of the pointerByte 1177 + pointerByte = pointerByte + 1 1178 + return byte1 1179 + else 1180 + pointerByte = pointerByte + 1 1181 + -- Get the remainder of the first pointerByte and add it to the part of the new pointerByte that's required 1182 + -- Both these methods are explained in writeByte 1183 + return bit32.band(bit32.lshift(byte1, c), 255) + bit32.rshift(bytes[pointerByte], 8 - c) 1184 + end 1185 + end 1186 + 1187 + local function readUnsigned(width) 1188 + assert(type(width) == "number", "argument #1 to BitBuffer.readUnsigned should be a number") 1189 + assert(width >= 1 and width <= 64, "argument #1 to BitBuffer.readUnsigned should be in the range [1, 64]") 1190 + assert(width % 1 == 0, "argument #1 to BitBuffer.readUnsigned should be an integer") 1191 + 1192 + assert(pointer + width <= bitCount, "BitBuffer.readUnsigned cannot read past the end of the stream") 1193 + -- Implementing this on its own was considered because of a worry that it would be inefficient to call 1194 + -- readByte and readBit several times, but it was decided the simplicity is worth a minor performance hit. 1195 + local bytesInN, bitsInN = math.floor(width / 8), width % 8 1196 + 1197 + -- No check is required for if the width is greater than 32 because bit32 isn't used. 1198 + local n = 0 1199 + -- Shift and add a read byte however many times is necessary 1200 + -- Adding after shifting is importnat - it prevents there from being 8 empty bits of space 1201 + for _ = 1, bytesInN do 1202 + n = n * 0x100 -- 2^8; equivalent to n << 8 1203 + n = n + readByte() 1204 + end 1205 + -- The bits are then read and added to the number 1206 + if bitsInN ~= 0 then 1207 + for _, v in ipairs(readBits(width % 8)) do --todo benchmark against concat+tonumber; might be worth the code smell 1208 + n = n * 2 1209 + n = n + v 1210 + end 1211 + end 1212 + return n 1213 + end 1214 + 1215 + local function readSigned(width) 1216 + assert(type(width) == "number", "argument #1 to BitBuffer.readSigned should be a number") 1217 + assert(width >= 2 and width <= 64, "argument #1 to BitBuffer.readSigned should be in the range [2, 64]") 1218 + assert(width % 1 == 0, "argument #1 to BitBuffer.readSigned should be an integer") 1219 + 1220 + assert(pointer + 8 <= bitCount, "BitBuffer.readSigned cannot read past the end of the stream") 1221 + local sign = readBits(1)[1] 1222 + local n = readUnsigned(width - 1) -- Again, width-1 is because one bit is used for the sign 1223 + 1224 + -- As said in writeSigned, the written number is unmodified if the number is positive (the sign bit is 0) 1225 + if sign == 0 then 1226 + return n 1227 + else 1228 + -- And the number is equal to max value of the width + the number if the number is negative (the sign bit is 1) 1229 + -- To reverse that, the max value is subtracted from the stored number. 1230 + return n - powers_of_2[width - 1] 1231 + end 1232 + end 1233 + 1234 + local function readFloat(exponentWidth, mantissaWidth) 1235 + assert(type(exponentWidth) == "number", "argument #1 to BitBuffer.readFloat should be a number") 1236 + assert( 1237 + exponentWidth >= 1 and exponentWidth <= 64, 1238 + "argument #1 to BitBuffer.readFloat should be in the range [1, 64]" 1239 + ) 1240 + assert(exponentWidth % 1 == 0, "argument #1 to BitBuffer.readFloat should be an integer") 1241 + 1242 + assert(type(mantissaWidth) == "number", "argument #2 to BitBuffer.readFloat should be a number") 1243 + assert( 1244 + mantissaWidth >= 1 and mantissaWidth <= 64, 1245 + "argument #2 to BitBuffer.readFloat should be in the range [1, 64]" 1246 + ) 1247 + assert(mantissaWidth % 1 == 0, "argument #2 to BitBuffer.readFloat should be an integer") 1248 + 1249 + assert( 1250 + pointer + exponentWidth + mantissaWidth + 1 <= bitCount, 1251 + "BitBuffer.readFloat cannot read past the end of the stream" 1252 + ) 1253 + -- Recomposing floats is rather straightfoward. 1254 + -- The bias is subtracted from the exponent, the mantissa is shifted back by mantissaWidth, one is added to the mantissa 1255 + -- and the whole thing is recomposed with math.ldexp (this is identical to mantissa*(2^exponent)). 1256 + 1257 + local bias = powers_of_2[exponentWidth - 1] - 1 1258 + 1259 + local sign = readBits(1)[1] 1260 + local exponent = readUnsigned(exponentWidth) 1261 + local mantissa = readUnsigned(mantissaWidth) 1262 + 1263 + -- Before normal numbers are handled though, special cases and subnormal numbers are once again handled seperately 1264 + if exponent == powers_of_2[exponentWidth] - 1 then 1265 + if mantissa ~= 0 then -- If the exponent is all 1s and the mantissa isn't zero, the number is NaN 1266 + return 0 / 0 1267 + else -- Otherwise, it's positive or negative infinity 1268 + return sign == 0 and math.huge or -math.huge 1269 + end 1270 + elseif exponent == 0 then 1271 + if mantissa == 0 then -- If the exponent and mantissa are both zero, the number is zero. 1272 + return 0 1273 + else -- If the exponent is zero and the mantissa is not zero, the number is subnormal 1274 + -- Subnormal numbers are straightforward: shifting the mantissa so that it's a fraction is all that's required 1275 + mantissa = mantissa / powers_of_2[mantissaWidth] 1276 + 1277 + -- Since the exponent is 0, it's actual value is just -bias (it would be exponent-bias) 1278 + -- As previously touched on in writeFloat, the exponent value is off by 1 in Lua though. 1279 + return sign == 1 and -math.ldexp(mantissa, -bias + 1) or math.ldexp(mantissa, -bias + 1) 1280 + end 1281 + end 1282 + 1283 + -- First, the mantissa is shifted back by the mantissaWidth 1284 + -- Then, 1 is added to it to 'normalize' it. 1285 + mantissa = (mantissa / powers_of_2[mantissaWidth]) + 1 1286 + 1287 + -- Because the mantissa is normalized above (the leading 1 is in the ones place), it's accurate to say exponent-bias 1288 + return sign == 1 and -math.ldexp(mantissa, exponent - bias) or math.ldexp(mantissa, exponent - bias) 1289 + end 1290 + 1291 + local function readString() 1292 + assert(pointer + 24 <= bitCount, "BitBuffer.readString cannot read past the end of the stream") 1293 + -- Reading a length-prefixed string is rather straight forward. 1294 + -- The length is read, then that many bytes are read and put in a string. 1295 + 1296 + local stringLength = readUnsigned(24) 1297 + assert(pointer + (stringLength * 8) <= bitCount, "BitBuffer.readString cannot read past the end of the stream") 1298 + 1299 + local outputCharacters = table.create(stringLength) --! 1300 + 1301 + for i = 1, stringLength do 1302 + outputCharacters[i] = readByte() 1303 + end 1304 + 1305 + local output = table.create(math.ceil(stringLength / 4096)) 1306 + local k = 1 1307 + for i = 1, stringLength, 4096 do 1308 + output[k] = string.char(table.unpack(outputCharacters, i, math.min(stringLength, i + 4095))) 1309 + k = k + 1 1310 + end 1311 + 1312 + return table.concat(output) 1313 + end 1314 + 1315 + local function readTerminatedString() 1316 + local outputCharacters = {} 1317 + 1318 + -- Bytes are read continuously until either a nul-character is reached or until the stream runs out. 1319 + local length = 0 1320 + while true do 1321 + local byte = readByte() 1322 + if not byte then -- Stream has ended 1323 + error("BitBuffer.readTerminatedString cannot read past the end of the stream", 2) 1324 + elseif byte == 0 then -- String has ended 1325 + break 1326 + else -- Add byte to string 1327 + length = length + 1 1328 + outputCharacters[length] = byte 1329 + end 1330 + end 1331 + 1332 + local output = table.create(math.ceil(length / 4096)) 1333 + local k = 1 1334 + for l = 1, length, 4096 do 1335 + output[k] = string.char(table.unpack(outputCharacters, l, math.min(length, l + 4095))) 1336 + k = k + 1 1337 + end 1338 + 1339 + return table.concat(output) 1340 + end 1341 + 1342 + local function readSetLengthString(length) 1343 + assert(type(length) == "number", "argument #1 to BitBuffer.readSetLengthString should be a number") 1344 + assert(length >= 0, "argument #1 to BitBuffer.readSetLengthString should be zero or higher.") 1345 + assert(length % 1 == 0, "argument #1 to BitBuffer.readSetLengthString should be an integer") 1346 + 1347 + assert( 1348 + pointer + (length * 8) <= bitCount, 1349 + "BitBuffer.readSetLengthString cannot read past the end of the stream" 1350 + ) 1351 + -- `length` number of bytes are read and put into a string 1352 + 1353 + local outputCharacters = table.create(length) --! 1354 + 1355 + for i = 1, length do 1356 + outputCharacters[i] = readByte() 1357 + end 1358 + 1359 + local output = table.create(math.ceil(length / 4096)) 1360 + local k = 1 1361 + for i = 1, length, 4096 do 1362 + output[k] = string.char(table.unpack(outputCharacters, i, math.min(length, i + 4095))) 1363 + k = k + 1 1364 + end 1365 + 1366 + return table.concat(output) 1367 + end 1368 + 1369 + local function readField(n) 1370 + assert(type(n) == "number", "argument #1 to BitBuffer.readField should be a number") 1371 + assert(n > 0, "argument #1 to BitBuffer.readField should be above 0") 1372 + assert(n % 1 == 0, "argument #1 to BitBuffer.readField should be an integer") 1373 + 1374 + assert(pointer + n <= bitCount, "BitBuffer.readField cannot read past the end of the stream") 1375 + -- Reading a bit field is again rather simple. You read the actual field, then take the bits out. 1376 + local readInt = readUnsigned(n) 1377 + local output = table.create(n) --! 1378 + 1379 + for i = n, 1, -1 do -- In reverse order since we're pulling bits out from lsb to msb 1380 + output[i] = readInt % 2 == 1 -- Equivalent to an extraction of the lsb 1381 + readInt = math.floor(readInt / 2) -- Equivalent to readInt>>1 1382 + end 1383 + 1384 + return output 1385 + end 1386 + 1387 + -- All read functions below here are shorthands. 1388 + -- As with their write variants, these functions are implemented manually using readByte for performance reasons. 1389 + 1390 + local function readUInt8() 1391 + assert(pointer + 8 <= bitCount, "BitBuffer.readUInt8 cannot read past the end of the stream") 1392 + 1393 + return readByte() 1394 + end 1395 + 1396 + local function readUInt16() 1397 + assert(pointer + 16 <= bitCount, "BitBuffer.readUInt16 cannot read past the end of the stream") 1398 + 1399 + return bit32.lshift(readByte(), 8) + readByte() 1400 + end 1401 + 1402 + local function readUInt32() 1403 + assert(pointer + 32 <= bitCount, "BitBuffer.readUInt32 cannot read past the end of the stream") 1404 + 1405 + return bit32.lshift(readByte(), 24) + bit32.lshift(readByte(), 16) + bit32.lshift(readByte(), 8) + readByte() 1406 + end 1407 + 1408 + local function readInt8() 1409 + assert(pointer + 8 <= bitCount, "BitBuffer.readInt8 cannot read past the end of the stream") 1410 + 1411 + local n = readByte() 1412 + local sign = bit32.btest(n, 128) 1413 + n = bit32.band(n, 127) 1414 + 1415 + if sign then 1416 + return n - 128 1417 + else 1418 + return n 1419 + end 1420 + end 1421 + 1422 + local function readInt16() 1423 + assert(pointer + 16 <= bitCount, "BitBuffer.readInt16 cannot read past the end of the stream") 1424 + 1425 + local n = bit32.lshift(readByte(), 8) + readByte() 1426 + local sign = bit32.btest(n, 32768) 1427 + n = bit32.band(n, 32767) 1428 + 1429 + if sign then 1430 + return n - 32768 1431 + else 1432 + return n 1433 + end 1434 + end 1435 + 1436 + local function readInt32() 1437 + assert(pointer + 32 <= bitCount, "BitBuffer.readInt32 cannot read past the end of the stream") 1438 + 1439 + local n = bit32.lshift(readByte(), 24) + bit32.lshift(readByte(), 16) + bit32.lshift(readByte(), 8) + readByte() 1440 + local sign = bit32.btest(n, 2147483648) 1441 + n = bit32.band(n, 2147483647) 1442 + 1443 + if sign then 1444 + return n - 2147483648 1445 + else 1446 + return n 1447 + end 1448 + end 1449 + 1450 + local function readFloat16() 1451 + assert(pointer + 16 <= bitCount, "BitBuffer.readFloat16 cannot read past the end of the stream") 1452 + 1453 + local b0 = readByte() 1454 + local sign = bit32.btest(b0, 128) 1455 + local exponent = bit32.rshift(bit32.band(b0, 127), 2) 1456 + local mantissa = bit32.lshift(bit32.band(b0, 3), 8) + readByte() 1457 + 1458 + if exponent == 31 then --2^5-1 1459 + if mantissa ~= 0 then 1460 + return 0 / 0 1461 + else 1462 + return sign and -math.huge or math.huge 1463 + end 1464 + elseif exponent == 0 then 1465 + if mantissa == 0 then 1466 + return 0 1467 + else 1468 + return sign and -math.ldexp(mantissa / 1024, -14) or math.ldexp(mantissa / 1024, -14) 1469 + end 1470 + end 1471 + 1472 + mantissa = (mantissa / 1024) + 1 1473 + 1474 + return sign and -math.ldexp(mantissa, exponent - 15) or math.ldexp(mantissa, exponent - 15) 1475 + end 1476 + 1477 + local function readFloat32() 1478 + assert(pointer + 32 <= bitCount, "BitBuffer.readFloat32 cannot read past the end of the stream") 1479 + 1480 + local b0 = readByte() 1481 + local b1 = readByte() 1482 + local sign = bit32.btest(b0, 128) 1483 + local exponent = bit32.band(bit32.lshift(b0, 1), 255) + bit32.rshift(b1, 7) 1484 + local mantissa = bit32.lshift(bit32.band(b1, 127), 23 - 7) 1485 + + bit32.lshift(readByte(), 23 - 7 - 8) 1486 + + bit32.lshift(readByte(), 23 - 7 - 8 - 8) 1487 + 1488 + if exponent == 255 then -- 2^8-1 1489 + if mantissa ~= 0 then 1490 + return 0 / 0 1491 + else 1492 + return sign and -math.huge or math.huge 1493 + end 1494 + elseif exponent == 0 then 1495 + if mantissa == 0 then 1496 + return 0 1497 + else 1498 + -- -126 is the 0-bias+1 1499 + return sign and -math.ldexp(mantissa / 8388608, -126) or math.ldexp(mantissa / 8388608, -126) 1500 + end 1501 + end 1502 + 1503 + mantissa = (mantissa / 8388608) + 1 1504 + 1505 + return sign and -math.ldexp(mantissa, exponent - 127) or math.ldexp(mantissa, exponent - 127) 1506 + end 1507 + 1508 + local function readFloat64() 1509 + assert(pointer + 64 <= bitCount, "BitBuffer.readFloat64 cannot read past the end of the stream") 1510 + 1511 + local b0 = readByte() 1512 + local b1 = readByte() 1513 + 1514 + local sign = bit32.btest(b0, 128) 1515 + local exponent = bit32.lshift(bit32.band(b0, 127), 4) + bit32.rshift(b1, 4) 1516 + local mostSignificantChunk = bit32.lshift(bit32.band(b1, 15), 16) + bit32.lshift(readByte(), 8) + readByte() 1517 + local leastSignificantChunk = bit32.lshift(readByte(), 24) 1518 + + bit32.lshift(readByte(), 16) 1519 + + bit32.lshift(readByte(), 8) 1520 + + readByte() 1521 + 1522 + -- local mantissa = (bit32.lshift(bit32.band(b1, 15), 16)+bit32.lshift(readByte(), 8)+readByte())*0x100000000+ 1523 + -- bit32.lshift(readByte(), 24)+bit32.lshift(readByte(), 16)+bit32.lshift(readByte(), 8)+readByte() 1524 + 1525 + local mantissa = mostSignificantChunk * 0x100000000 + leastSignificantChunk 1526 + 1527 + if exponent == 2047 then -- 2^11-1 1528 + if mantissa ~= 0 then 1529 + return 0 / 0 1530 + else 1531 + return sign and -math.huge or math.huge 1532 + end 1533 + elseif exponent == 0 then 1534 + if mantissa == 0 then 1535 + return 0 1536 + else 1537 + return sign and -math.ldexp(mantissa / 4503599627370496, -1022) 1538 + or math.ldexp(mantissa / 4503599627370496, -1022) 1539 + end 1540 + end 1541 + 1542 + mantissa = (mantissa / 4503599627370496) + 1 1543 + 1544 + return sign and -math.ldexp(mantissa, exponent - 1023) or math.ldexp(mantissa, exponent - 1023) 1545 + end 1546 + 1547 + -- All read functions below here are Roblox specific datatypes. 1548 + 1549 + local function readBrickColor() 1550 + assert(pointer + 16 <= bitCount, "BitBuffer.readBrickColor cannot read past the end of the stream") 1551 + 1552 + return BrickColor.new(readUInt16()) 1553 + end 1554 + 1555 + local function readColor3() 1556 + assert(pointer + 24 <= bitCount, "BitBuffer.readColor3 cannot read past the end of the stream") 1557 + 1558 + return Color3.fromRGB(readByte(), readByte(), readByte()) 1559 + end 1560 + 1561 + local function readCFrame() 1562 + assert(pointer + 8 <= bitCount, "BitBuffer.readCFrame cannot read past the end of the stream") 1563 + 1564 + local id = readByte() 1565 + 1566 + if id == 0 then 1567 + assert(pointer + 384 <= bitCount, "BitBuffer.readCFrame cannot read past the end of the stream") -- 4*12 bytes = 383 bits 1568 + 1569 + -- stylua: ignore 1570 + return CFrame.new( 1571 + readFloat32(), readFloat32(), readFloat32(), 1572 + readFloat32(), readFloat32(), readFloat32(), 1573 + readFloat32(), readFloat32(), readFloat32(), 1574 + readFloat32(), readFloat32(), readFloat32() 1575 + ) 1576 + else 1577 + assert(pointer + 96 <= bitCount, "BitBuffer.readCFrame cannot read past the end of the stream") -- 4*3 bytes = 96 bits 1578 + 1579 + local rightVector = NORMAL_ID_VECTORS[math.floor(id / 6)] 1580 + local upVector = NORMAL_ID_VECTORS[id % 6] 1581 + local lookVector = rightVector:Cross(upVector) 1582 + 1583 + -- CFrame's full-matrix constructor takes right/up/look vectors as columns... 1584 + -- stylua: ignore 1585 + return CFrame.new( 1586 + readFloat32(), readFloat32(), readFloat32(), 1587 + rightVector.X, upVector.X, lookVector.X, 1588 + rightVector.Y, upVector.Y, lookVector.Y, 1589 + rightVector.Z, upVector.Z, lookVector.Z 1590 + ) 1591 + end 1592 + end 1593 + 1594 + local function readVector3() 1595 + assert(pointer + 96 <= bitCount, "BitBuffer.readVector3 cannot read past the end of the stream") 1596 + 1597 + return Vector3.new(readFloat32(), readFloat32(), readFloat32()) 1598 + end 1599 + 1600 + local function readVector2() 1601 + assert(pointer + 64 <= bitCount, "BitBuffer.readVector2 cannot read past the end of the stream") 1602 + 1603 + return Vector2.new(readFloat32(), readFloat32()) 1604 + end 1605 + 1606 + local function readUDim2() 1607 + assert(pointer + 128 <= bitCount, "BitBuffer.readUDim2 cannot read past the end of the stream") 1608 + 1609 + return UDim2.new(readFloat32(), readInt32(), readFloat32(), readInt32()) 1610 + end 1611 + 1612 + local function readUDim() 1613 + assert(pointer + 64 <= bitCount, "BitBuffer.readUDim cannot read past the end of the stream") 1614 + 1615 + return UDim.new(readFloat32(), readInt32()) 1616 + end 1617 + 1618 + local function readRay() 1619 + assert(pointer + 192 <= bitCount, "BitBuffer.readRay cannot read past the end of the stream") 1620 + 1621 + return Ray.new( 1622 + Vector3.new(readFloat32(), readFloat32(), readFloat32()), 1623 + Vector3.new(readFloat32(), readFloat32(), readFloat32()) 1624 + ) 1625 + end 1626 + 1627 + local function readRect() 1628 + assert(pointer + 128 <= bitCount, "BitBuffer.readRect cannot read past the end of the stream") 1629 + 1630 + return Rect.new(readFloat32(), readFloat32(), readFloat32(), readFloat32()) 1631 + end 1632 + 1633 + local function readRegion3() 1634 + assert(pointer + 192 <= bitCount, "BitBuffer.readRegion3 cannot read past the end of the stream") 1635 + 1636 + return Region3.new( 1637 + Vector3.new(readFloat32(), readFloat32(), readFloat32()), 1638 + Vector3.new(readFloat32(), readFloat32(), readFloat32()) 1639 + ) 1640 + end 1641 + 1642 + local function readEnum() 1643 + assert(pointer + 8 <= bitCount, "BitBuffer.readEnum cannot read past the end of the stream") 1644 + 1645 + local name = readTerminatedString() -- This might expose an error from readString to the end-user but it's not worth the hassle to fix. 1646 + 1647 + assert(pointer + 16 <= bitCount, "BitBuffer.readEnum cannot read past the end of the stream") 1648 + 1649 + local value = readUInt16() -- Again, optimistically assuming no Roblox Enum value will ever pass 65,535 1650 + 1651 + -- Catching a potential error only to throw it with different formatting seems... Superfluous. 1652 + -- Open an issue on github if you feel otherwise. 1653 + for _, v in ipairs(Enum[name]:GetEnumItems()) do 1654 + if v.Value == value then 1655 + return v 1656 + end 1657 + end 1658 + 1659 + error( 1660 + "BitBuffer.readEnum could not get value: `" 1661 + .. tostring(value) 1662 + .. "` is not a valid member of `" 1663 + .. name 1664 + .. "`", 1665 + 2 1666 + ) 1667 + end 1668 + 1669 + local function readNumberRange() 1670 + assert(pointer + 64 <= bitCount, "BitBuffer.readNumberRange cannot read past the end of the stream") 1671 + 1672 + return NumberRange.new(readFloat32(), readFloat32()) 1673 + end 1674 + 1675 + local function readNumberSequence() 1676 + assert(pointer + 32 <= bitCount, "BitBuffer.readNumberSequence cannot read past the end of the stream") 1677 + 1678 + local keypointCount = readUInt32() 1679 + 1680 + assert(pointer + keypointCount * 96, "BitBuffer.readColorSequence cannot read past the end of the stream") 1681 + 1682 + local keypoints = table.create(keypointCount) 1683 + 1684 + -- As it turns out, creating a NumberSequence with a negative value as its first argument (in the first and second constructor) 1685 + -- creates NumberSequenceKeypoints with negative envelopes. The envelope is read and saved properly, as you would expect, 1686 + -- but you can't create a NumberSequence with a negative envelope if you're using a table of keypoints (which is happening here). 1687 + -- If you're confused, run this snippet: NumberSequence.new(NumberSequence.new(-1).Keypoints) 1688 + -- As a result, there has to be some branching logic in this function. 1689 + -- ColorSequences don't have envelopes so it's not necessary for them. 1690 + 1691 + for i = 1, keypointCount do 1692 + local time, value, envelope = readFloat32(), readFloat32(), readFloat32() 1693 + if value < 0 then 1694 + envelope = nil 1695 + end 1696 + keypoints[i] = NumberSequenceKeypoint.new(time, value, envelope) 1697 + end 1698 + 1699 + return NumberSequence.new(keypoints) 1700 + end 1701 + 1702 + local function readColorSequence() 1703 + assert(pointer + 32 <= bitCount, "BitBuffer.readColorSequence cannot read past the end of the stream") 1704 + 1705 + local keypointCount = readUInt32() 1706 + 1707 + assert(pointer + keypointCount * 56, "BitBuffer.readColorSequence cannot read past the end of the stream") 1708 + 1709 + local keypoints = table.create(keypointCount) 1710 + 1711 + for i = 1, keypointCount do 1712 + keypoints[i] = ColorSequenceKeypoint.new(readFloat32(), Color3.fromRGB(readByte(), readByte(), readByte())) 1713 + end 1714 + 1715 + return ColorSequence.new(keypoints) 1716 + end 1717 + 1718 + return { 1719 + dumpBinary = dumpBinary, 1720 + dumpString = dumpString, 1721 + dumpHex = dumpHex, 1722 + dumpBase64 = dumpBase64, 1723 + exportChunk = exportChunk, 1724 + exportBase64Chunk = exportBase64Chunk, 1725 + exportHexChunk = exportHexChunk, 1726 + 1727 + crc32 = crc32, 1728 + getLength = getLength, 1729 + getByteLength = getByteLength, 1730 + getPointer = getPointer, 1731 + setPointer = setPointer, 1732 + setPointerFromEnd = setPointerFromEnd, 1733 + getPointerByte = getPointerByte, 1734 + setPointerByte = setPointerByte, 1735 + setPointerByteFromEnd = setPointerByteFromEnd, 1736 + isFinished = isFinished, 1737 + 1738 + writeBits = writeBits, 1739 + writeByte = writeByte, 1740 + writeBytesFast = writeBytesFast, 1741 + writeUnsigned = writeUnsigned, 1742 + writeSigned = writeSigned, 1743 + writeFloat = writeFloat, 1744 + writeBase64 = writeBase64, 1745 + writeString = writeString, 1746 + writeTerminatedString = writeTerminatedString, 1747 + writeSetLengthString = writeSetLengthString, 1748 + writeField = writeField, 1749 + 1750 + writeUInt8 = writeUInt8, 1751 + writeUInt16 = writeUInt16, 1752 + writeUInt32 = writeUInt32, 1753 + writeInt8 = writeInt8, 1754 + writeInt16 = writeInt16, 1755 + writeInt32 = writeInt32, 1756 + 1757 + writeFloat16 = writeFloat16, 1758 + writeFloat32 = writeFloat32, 1759 + writeFloat64 = writeFloat64, 1760 + 1761 + writeBrickColor = writeBrickColor, 1762 + writeColor3 = writeColor3, 1763 + writeCFrame = writeCFrame, 1764 + writeVector3 = writeVector3, 1765 + writeVector2 = writeVector2, 1766 + writeUDim2 = writeUDim2, 1767 + writeUDim = writeUDim, 1768 + writeRay = writeRay, 1769 + writeRect = writeRect, 1770 + writeRegion3 = writeRegion3, 1771 + writeEnum = writeEnum, 1772 + writeNumberRange = writeNumberRange, 1773 + writeNumberSequence = writeNumberSequence, 1774 + writeColorSequence = writeColorSequence, 1775 + 1776 + readBits = readBits, 1777 + readByte = readByte, 1778 + readUnsigned = readUnsigned, 1779 + readSigned = readSigned, 1780 + readFloat = readFloat, 1781 + readString = readString, 1782 + readTerminatedString = readTerminatedString, 1783 + readSetLengthString = readSetLengthString, 1784 + readField = readField, 1785 + readBytesFast = readBytesFast, 1786 + skipStrayBits = skipStrayBits, 1787 + 1788 + readUInt8 = readUInt8, 1789 + readUInt16 = readUInt16, 1790 + readUInt32 = readUInt32, 1791 + readInt8 = readInt8, 1792 + readInt16 = readInt16, 1793 + readInt32 = readInt32, 1794 + 1795 + readFloat16 = readFloat16, 1796 + readFloat32 = readFloat32, 1797 + readFloat64 = readFloat64, 1798 + 1799 + readBrickColor = readBrickColor, 1800 + readColor3 = readColor3, 1801 + readCFrame = readCFrame, 1802 + readVector3 = readVector3, 1803 + readVector2 = readVector2, 1804 + readUDim2 = readUDim2, 1805 + readUDim = readUDim, 1806 + readRay = readRay, 1807 + readRect = readRect, 1808 + readRegion3 = readRegion3, 1809 + readEnum = readEnum, 1810 + readNumberRange = readNumberRange, 1811 + readNumberSequence = readNumberSequence, 1812 + readColorSequence = readColorSequence, 1813 + } 1814 + end 1815 + 1816 + return bitBuffer
+38
src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/Node.lua
··· 1 + local Node = {} 2 + Node.__index = Node 3 + 4 + function Node.new(data) 5 + local node = setmetatable({}, Node) 6 + node.data = data 7 + node.left = nil 8 + node.right = nil 9 + return node 10 + end 11 + 12 + -- return an iterator that traverses the tree in order 13 + function Node:inorder() 14 + local stack = {} 15 + local current = {self, ""} 16 + table.insert(stack, current) 17 + 18 + return function() 19 + while current[1].left do 20 + local parent = current 21 + current = {parent[1].left, parent[2] .. "0"} 22 + table.insert(stack, current) 23 + end 24 + 25 + if #stack > 0 then 26 + local node = table.remove(stack) 27 + 28 + if node[1].right then 29 + local parent = node 30 + current = {parent[1].right, parent[2] .. "1"} 31 + table.insert(stack, current) 32 + end 33 + return node[1], node[2] 34 + end 35 + end 36 + end 37 + 38 + return Node
+232
src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/PriorityQueue.lua
··· 1 + --[[ 2 + 3 + PriorityQueue - v1.0.1 - public domain Lua priority queue 4 + implemented with indirect binary heap 5 + no warranty implied; use at your own risk 6 + 7 + based on binaryheap library (github.com/iskolbin/binaryheap) 8 + 9 + author: Ilya Kolbin (iskolbin@gmail.com) 10 + url: github.com/iskolbin/priorityqueue 11 + 12 + See documentation in README file. 13 + 14 + COMPATIBILITY 15 + 16 + Lua 5.1, 5.2, 5.3, LuaJIT 1, 2 17 + 18 + LICENSE 19 + 20 + This software is dual-licensed to the public domain and under the following 21 + license: you are granted a perpetual, irrevocable license to copy, modify, 22 + publish, and distribute this file as you see fit. 23 + 24 + --]] 25 + 26 + local floor, setmetatable = math.floor, setmetatable 27 + 28 + local function siftup( self, from ) 29 + local items, priorities, indices, higherpriority = self, self._priorities, self._indices, self._higherpriority 30 + local index = from 31 + local parent = floor( index / 2 ) 32 + while index > 1 and higherpriority( priorities[index], priorities[parent] ) do 33 + priorities[index], priorities[parent] = priorities[parent], priorities[index] 34 + items[index], items[parent] = items[parent], items[index] 35 + indices[items[index]], indices[items[parent]] = index, parent 36 + index = parent 37 + parent = floor( index / 2 ) 38 + end 39 + return index 40 + end 41 + 42 + local function siftdown( self, limit ) 43 + local items, priorities, indices, higherpriority, size = self, self._priorities, self._indices, self._higherpriority, self._size 44 + for index = limit, 1, -1 do 45 + local left = index + index 46 + local right = left + 1 47 + while left <= size do 48 + local smaller = left 49 + if right <= size and higherpriority( priorities[right], priorities[left] ) then 50 + smaller = right 51 + end 52 + if higherpriority( priorities[smaller], priorities[index] ) then 53 + items[index], items[smaller] = items[smaller], items[index] 54 + priorities[index], priorities[smaller] = priorities[smaller], priorities[index] 55 + indices[items[index]], indices[items[smaller]] = index, smaller 56 + else 57 + break 58 + end 59 + index = smaller 60 + left = index + index 61 + right = left + 1 62 + end 63 + end 64 + end 65 + 66 + local PriorityQueueMt 67 + 68 + local PriorityQueue = {} 69 + 70 + local function minishigher( a, b ) 71 + return a < b 72 + end 73 + 74 + local function maxishigher( a, b ) 75 + return a > b 76 + end 77 + 78 + function PriorityQueue.new( priority_or_array ) 79 + local t = type( priority_or_array ) 80 + local higherpriority = minishigher 81 + 82 + if t == 'table' then 83 + higherpriority = priority_or_array.higherpriority or higherpriority 84 + elseif t == 'function' or t == 'string' then 85 + higherpriority = priority_or_array 86 + elseif t ~= 'nil' then 87 + local msg = 'Wrong argument type to PriorityQueue.new, it must be table or function or string, has: %q' 88 + error( msg:format( t )) 89 + end 90 + 91 + if type( higherpriority ) == 'string' then 92 + if higherpriority == 'min' then 93 + higherpriority = minishigher 94 + elseif higherpriority == 'max' then 95 + higherpriority = maxishigher 96 + else 97 + local msg = 'Wrong string argument to PriorityQueue.new, it must be "min" or "max", has: %q' 98 + error( msg:format( tostring( higherpriority ))) 99 + end 100 + end 101 + 102 + local self = setmetatable( { 103 + _priorities = {}, 104 + _indices = {}, 105 + _size = 0, 106 + _higherpriority = higherpriority or minishigher 107 + }, PriorityQueueMt ) 108 + 109 + if t == 'table' then 110 + self:batchenq( priority_or_array ) 111 + end 112 + 113 + return self 114 + end 115 + 116 + function PriorityQueue:enqueue( item, priority ) 117 + local items, priorities, indices = self, self._priorities, self._indices 118 + if indices[item] ~= nil then 119 + error( 'Item ' .. tostring(indices[item]) .. ' is already in the heap' ) 120 + end 121 + local size = self._size + 1 122 + self._size = size 123 + items[size], priorities[size], indices[item] = item, priority, size 124 + siftup( self, size ) 125 + return self 126 + end 127 + 128 + function PriorityQueue:remove( item ) 129 + local index = self._indices[item] 130 + if index ~= nil then 131 + local size = self._size 132 + local items, priorities, indices = self, self._priorities, self._indices 133 + indices[item] = nil 134 + if size == index then 135 + items[size], priorities[size] = nil, nil 136 + self._size = size - 1 137 + else 138 + local lastitem = items[size] 139 + items[index], priorities[index] = items[size], priorities[size] 140 + items[size], priorities[size] = nil, nil 141 + indices[lastitem] = index 142 + size = size - 1 143 + self._size = size 144 + if size > 1 then 145 + siftdown( self, siftup( self, index )) 146 + end 147 + end 148 + return true 149 + else 150 + return false 151 + end 152 + end 153 + 154 + function PriorityQueue:contains( item ) 155 + return self._indices[item] ~= nil 156 + end 157 + 158 + function PriorityQueue:update( item, priority ) 159 + local ok = self:remove( item ) 160 + if ok then 161 + self:enqueue( item, priority ) 162 + return true 163 + else 164 + return false 165 + end 166 + end 167 + 168 + function PriorityQueue:dequeue() 169 + local size = self._size 170 + 171 + assert( size > 0, 'Heap is empty' ) 172 + 173 + local items, priorities, indices = self, self._priorities, self._indices 174 + local item, priority = items[1], priorities[1] 175 + indices[item] = nil 176 + 177 + if size > 1 then 178 + local newitem = items[size] 179 + items[1], priorities[1] = newitem, priorities[size] 180 + items[size], priorities[size] = nil, nil 181 + indices[newitem] = 1 182 + size = size - 1 183 + self._size = size 184 + siftdown( self, 1 ) 185 + else 186 + items[1], priorities[1] = nil, nil 187 + self._size = 0 188 + end 189 + 190 + return item, priority 191 + end 192 + 193 + function PriorityQueue:peek() 194 + return self[1], self._priorities[1] 195 + end 196 + 197 + function PriorityQueue:len() 198 + return self._size 199 + end 200 + 201 + function PriorityQueue:empty() 202 + return self._size <= 0 203 + end 204 + 205 + function PriorityQueue:batchenq( iparray ) 206 + local items, priorities, indices = self, self._priorities, self._indices 207 + local size = self._size 208 + for i = 1, #iparray, 2 do 209 + local item, priority = iparray[i], iparray[i+1] 210 + if indices[item] ~= nil then 211 + error( 'Item ' .. tostring(indices[item]) .. ' is already in the heap' ) 212 + end 213 + size = size + 1 214 + items[size], priorities[size] = item, priority 215 + indices[item] = size 216 + end 217 + self._size = size 218 + if size > 1 then 219 + siftdown( self, floor( size / 2 )) 220 + end 221 + end 222 + 223 + PriorityQueueMt = { 224 + __index = PriorityQueue, 225 + __len = PriorityQueue.len, 226 + } 227 + 228 + return setmetatable( PriorityQueue, { 229 + __call = function( _, ... ) 230 + return PriorityQueue.new( ... ) 231 + end 232 + } )
+114
src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/init.lua
··· 1 + --Huffman.lua 2 + --iiau, Sat May 18 2024 3 + --Implementation of huffman coding algorithm for use in Roblox 4 + 5 + local Huffman = {} 6 + local PriorityQueue = require(script.PriorityQueue) 7 + local Node = require(script.Node) 8 + local BitBuffer = require(script.BitBuffer) 9 + 10 + local CHUNK_SIZE = 256 11 + 12 + --thanks to https://stackoverflow.com/a/32220398 for helping me with this 13 + local function to_bin(n) 14 + local t = {} 15 + for _ = 1, 32 do 16 + n = bit32.rrotate(n, -1) 17 + table.insert(t, bit32.band(n, 1)) 18 + end 19 + return table.concat(t) 20 + end 21 + 22 + -- Encode a string to huffman coded string. Limitation is that the data should not be more than 16777215 bytes. 23 + -- @param data The string to encode 24 + -- @return The encoded string 25 + Huffman.encode = function(data: string) : string 26 + assert(#data > 0, "Data must not be empty") 27 + local buffer = BitBuffer() 28 + 29 + -- get the frequency of each character in the string 30 + local freq, dict, size = {}, {}, 0 31 + for c in data:gmatch(".") do 32 + freq[c] = (freq[c] or 0) + 1 33 + end 34 + for _ in pairs(freq) do 35 + size += 1 36 + end 37 + 38 + local q = PriorityQueue.new 'min' 39 + for k: string, v: number in pairs(freq) do 40 + local leaf = Node.new(string.byte(k)) 41 + q:enqueue(leaf, v) 42 + end 43 + 44 + while q:len() > 1 do 45 + local left, freq_l = q:dequeue() 46 + local right, freq_r = q:dequeue() 47 + local parent = Node.new() 48 + parent.left = left 49 + parent.right = right 50 + 51 + q:enqueue(parent, freq_l + freq_r) 52 + end 53 + local tree = q:dequeue() 54 + buffer.writeUInt8(size-1) 55 + buffer.writeUnsigned(24, #data) 56 + for node, bits: string in tree:inorder() do 57 + if not node.data then 58 + continue 59 + end 60 + local number = tonumber(bits, 2) 61 + local bit_array = string.split(bits, "") 62 + for i = 1, #bit_array do 63 + bit_array[i] = tonumber(bit_array[i]) 64 + end 65 + 66 + dict[string.char(node.data)] = bit_array 67 + buffer.writeUInt8(node.data) -- char 68 + buffer.writeUnsigned(5, #bits) -- number of bits 69 + buffer.writeUnsigned(#bits, number) -- bits 70 + end 71 + for c in data:gmatch(".") do 72 + buffer.writeBits(table.unpack(dict[c])) 73 + end 74 + 75 + -- to avoid the dreaded too many results to unpack error 76 + local chunks = {} 77 + for _, chunk in buffer.exportChunk(CHUNK_SIZE) do 78 + table.insert(chunks, chunk) 79 + end 80 + return table.concat(chunks) 81 + end 82 + 83 + -- Decode a string from huffman coded string 84 + -- @param data The string to decode 85 + -- @return The decoded string 86 + Huffman.decode = function(data: string) : string 87 + assert(#data > 0, "Data must not be empty") 88 + local buffer = BitBuffer(data) 89 + 90 + local dict_size = buffer.readUInt8()+1 91 + local len_data = buffer.readUnsigned(24) 92 + local dict, read = {}, 0 93 + 94 + for i = 1, dict_size do 95 + local char = buffer.readUInt8() 96 + local digits = buffer.readUnsigned(5) 97 + local bits = buffer.readUnsigned(digits) 98 + dict[to_bin(bits):sub(-digits)] = char 99 + end 100 + local decoded = {} 101 + local bits = "" 102 + while read < len_data do 103 + bits ..= buffer.readBits(1)[1] 104 + local char = dict[bits] 105 + if char then 106 + table.insert(decoded, string.char(char)) 107 + bits = "" 108 + read += 1 109 + end 110 + end 111 + return table.concat(decoded) 112 + end 113 + 114 + return Huffman
+170
src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/LZW.lua
··· 1 + -- Module by 1waffle1 and boatbomber, optimized and fixed by iiau 2 + -- https://devforum.roblox.com/t/text-compression/163637/37 3 + 4 + local dictionary = {} -- key to len 5 + 6 + do -- populate dictionary 7 + local length = 0 8 + for i = 32, 127 do 9 + if i ~= 34 and i ~= 92 then 10 + local c = string.char(i) 11 + dictionary[c] = length 12 + dictionary[length] = c 13 + length = length + 1 14 + end 15 + end 16 + end 17 + 18 + local escapemap_126, escapemap_127 = {}, {} 19 + local unescapemap_126, unescapemap_127 = {}, {} 20 + 21 + local blacklisted_126 = { 34, 92 } 22 + for i = 126, 180 do 23 + table.insert(blacklisted_126, i) 24 + end 25 + 26 + do -- Populate escape map 27 + -- represents the numbers 0-31, 34, 92, 126, 127 (36 characters) 28 + -- and 128-180 (52 characters) 29 + -- https://devforum.roblox.com/t/text-compression/163637/5 30 + for i = 0, 31 + #blacklisted_126 do 31 + local b = blacklisted_126[i - 31] 32 + local s = i + 32 33 + 34 + -- Note: 126 and 127 are magic numbers 35 + local c = string.char(b or i) 36 + local e = string.char(s + (s >= 34 and 1 or 0) + (s >= 91 and 1 or 0)) 37 + 38 + escapemap_126[c] = e 39 + unescapemap_126[e] = c 40 + end 41 + 42 + for i = 1, 255 - 180 do 43 + local c = string.char(i + 180) 44 + local s = i + 34 45 + local e = string.char(s + (s >= 92 and 1 or 0)) 46 + 47 + escapemap_127[c] = e 48 + unescapemap_127[e] = c 49 + end 50 + end 51 + 52 + local function escape(s) 53 + -- escape the control characters 0-31, double quote 34, backslash 92, Tilde 126, and DEL 127 (36 chars) 54 + -- escape characters 128-180 (53 chars) 55 + return string.gsub(string.gsub(s, '[%c"\\\126-\180]', function(c) 56 + return "\126" .. escapemap_126[c] 57 + end), '[\181-\255]', function(c) 58 + return "\127" .. escapemap_127[c] 59 + end) 60 + end 61 + local function unescape(s) 62 + return string.gsub(string.gsub(s, "\127(.)", function(e) 63 + return unescapemap_127[e] 64 + end), "\126(.)", function(e) 65 + return unescapemap_126[e] 66 + end) 67 + end 68 + 69 + local b93Cache = {} 70 + local function tobase93(n) 71 + local value = b93Cache[n] 72 + if value then 73 + return value 74 + end 75 + 76 + local c = n 77 + value = "" 78 + repeat 79 + local remainder = n % 93 80 + value = dictionary[remainder] .. value 81 + n = (n - remainder) / 93 82 + until n == 0 83 + 84 + b93Cache[c] = value 85 + return value 86 + end 87 + 88 + local b10Cache = {} 89 + local function tobase10(value) 90 + local n = b10Cache[value] 91 + if n then 92 + return n 93 + end 94 + 95 + n = 0 96 + for i = 1, #value do 97 + n = n + math.pow(93, i - 1) * dictionary[string.sub(value, -i, -i)] 98 + end 99 + 100 + b10Cache[value] = n 101 + return n 102 + end 103 + 104 + local function compress(text) 105 + assert(type(text) == "string", "bad argument #1 to 'compress' (string expected, got " .. typeof(text) .. ")") 106 + local dictionaryCopy = table.clone(dictionary) 107 + local key, sequence, size = "", {}, #dictionary 108 + 109 + local width, spans, span = 1, {}, 0 110 + local function listkey(k) 111 + local value = tobase93(dictionaryCopy[k]) 112 + local valueLength = #value 113 + if valueLength > width then 114 + width, span, spans[width] = valueLength, 0, span 115 + end 116 + table.insert(sequence, string.rep(" ", width - valueLength) .. value) 117 + span += 1 118 + end 119 + text = escape(text) 120 + for i = 1, #text do 121 + local c = string.sub(text, i, i) 122 + local new = key .. c 123 + if dictionaryCopy[new] then 124 + key = new 125 + else 126 + listkey(key) 127 + key = c 128 + size += 1 129 + dictionaryCopy[new] = size 130 + dictionaryCopy[size] = new 131 + end 132 + end 133 + listkey(key) 134 + spans[width] = span 135 + return table.concat(spans, ",") .. "|" .. table.concat(sequence) 136 + end 137 + 138 + local function decompress(text) 139 + assert(type(text) == "string", "bad argument #1 to 'decompress' (string expected, got " .. typeof(text) .. ")") 140 + local dictionaryCopy = table.clone(dictionary) 141 + local sequence, spans, content = {}, string.match(text, "(.-)|(.*)") 142 + local groups, start = {}, 1 143 + for span in string.gmatch(spans, "%d+") do 144 + local width = #groups + 1 145 + groups[width] = string.sub(content, start, start + span * width - 1) 146 + start = start + span * width 147 + end 148 + local previous 149 + 150 + for width, group in ipairs(groups) do 151 + for value in string.gmatch(group, string.rep(".", width)) do 152 + local entry = dictionaryCopy[tobase10(value)] 153 + if previous then 154 + if entry then 155 + table.insert(dictionaryCopy, previous .. string.sub(entry, 1, 1)) 156 + else 157 + entry = previous .. string.sub(previous, 1, 1) 158 + table.insert(dictionaryCopy, entry) 159 + end 160 + table.insert(sequence, entry) 161 + else 162 + sequence[1] = entry 163 + end 164 + previous = entry 165 + end 166 + end 167 + return unescape(table.concat(sequence)) 168 + end 169 + 170 + return { compress = compress, decompress = decompress }
+19
src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/init.lua
··· 1 + --Deflate.lua 2 + --iiau, Sat May 18 2024 3 + 4 + local Deflate = {} 5 + 6 + local LZW = require(script.LZW) 7 + local Huffman = require(script.Huffman) 8 + 9 + Deflate.encode = function(data: string) : string 10 + data = LZW.compress(data) 11 + return Huffman.encode(data) 12 + end 13 + 14 + Deflate.decode = function(data: string) : string 15 + data = Huffman.decode(data) 16 + return LZW.decompress(data) 17 + end 18 + 19 + return Deflate
+70
src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/init.lua
··· 1 + local TerrainGen = {} 2 + 3 + local deflate = require("./TerrainGen/Deflate") 4 + 5 + local DSS = game:GetService("DataStoreService") 6 + local WORLDNAME = "DEFAULT" 7 + local WORLDID = "b73bb5a6-297d-4352-b637-daec7e8c8f3e" 8 + local Store = DSS:GetDataStore("BlockscraftWorldV1", WORLDID) 9 + 10 + local ChunkManager = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared").ChunkManager) 11 + local Chunk = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared").ChunkManager.Chunk) 12 + 13 + TerrainGen.ServerChunkCache = {} :: {[typeof("")]: typeof(Chunk.new(0,0,0))} 14 + 15 + -- Load a chunk from the DataStore or generate it if not found 16 + function TerrainGen:GetChunk(x, y, z) 17 + local key = `{x},{y},{z}` 18 + if TerrainGen.ServerChunkCache[key] then 19 + return TerrainGen.ServerChunkCache[key] 20 + end 21 + 22 + -- Generate a new chunk if it doesn't exist 23 + local chunk = Chunk.new(x, y, z) 24 + if y == 1 then 25 + for cx = 1, 8 do 26 + for cz = 1, 8 do 27 + --local perlin = math.noise(((x*8)+cx)/100,((z*8)+cz)/100) 28 + chunk:CreateBlock(cx, 1, cz, { id = 1, state = {} }) 29 + --chunk:CreateBlock(x, 2, z, { id = 1, state = {} }) 30 + end 31 + end 32 + end 33 + if y == 0 then 34 + for cx = 1, 8 do 35 + for cy = 1, 8 do 36 + for cz = 1, 8 do 37 + --local perlin = math.noise(((x*8)+cx)/100,((z*8)+cz)/100) 38 + chunk:CreateBlock(cx, cy, cz, { id = 2, state = {} }) 39 + --chunk:CreateBlock(x, 2, z, { id = 1, state = {} }) 40 + end 41 + end 42 + end 43 + end 44 + 45 + TerrainGen.ServerChunkCache[key] = chunk 46 + return chunk 47 + end 48 + 49 + -- Fake Chunk 50 + function TerrainGen:GetFakeChunk(x, y, z) 51 + 52 + -- Generate a new chunk if it doesn't exist 53 + local chunk = Chunk.new(x, y, z) 54 + for cy = 1,8 do 55 + for cx = 1, 8 do 56 + for cz = 1, 8 do 57 + --local perlin = math.noise(((x*8)+cx)/100,((z*8)+cz)/100) 58 + chunk:CreateBlock(cx, cy, cz, { id = -2, state = {} }) 59 + --chunk:CreateBlock(x, 2, z, { id = 1, state = {} }) 60 + end 61 + end 62 + end 63 + 64 + return chunk 65 + end 66 + 67 + 68 + TerrainGen.CM = ChunkManager 69 + 70 + return TerrainGen
+189
src/ServerScriptService/Actor/ServerChunkManager/init.server.lua
··· 1 + print("Hello world!") 2 + 3 + task.synchronize() 4 + 5 + local ReplicatedStorage = game:GetService("ReplicatedStorage") 6 + 7 + 8 + local Shared = ReplicatedStorage:WaitForChild("Shared") 9 + local ModsFolder = ReplicatedStorage:WaitForChild("Mods") 10 + 11 + local Util = require(Shared.Util) 12 + local TG = require("./ServerChunkManager/TerrainGen") 13 + 14 + do 15 + local workspaceModFolder = game:GetService("Workspace"):WaitForChild("mods") 16 + 17 + for _,v in pairs(workspaceModFolder:GetChildren()) do 18 + v.Parent = ModsFolder 19 + end 20 + workspaceModFolder:Destroy() 21 + end 22 + 23 + local ML = require(Shared.ModLoader) 24 + ML.loadModsS() 25 + 26 + do 27 + local bv = Instance.new("BoolValue") 28 + bv.Name = "MLLoaded" 29 + bv.Value = true 30 + bv.Parent = ReplicatedStorage:WaitForChild("Objects") 31 + end 32 + 33 + local MAX_CHUNK_DIST = 200 34 + 35 + local FakeChunk = TG:GetFakeChunk(-5,-5,-5) 36 + 37 + task.synchronize() 38 + 39 + ReplicatedStorage.Tick.OnServerEvent:Connect(function(player: Player, v: string) 40 + if TG.ServerChunkCache[v] then 41 + pcall(function() 42 + TG.ServerChunkCache[v].inhabitedTime = tick() 43 + end) 44 + end 45 + end) 46 + 47 + ReplicatedStorage.RecieveChunkPacket.OnServerInvoke = function(plr: Player, x: number, y: number, z: number) 48 + -- validate xyz type and limit 49 + if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then 50 + return {} 51 + end 52 + 53 + if math.abs(x) > MAX_CHUNK_DIST or math.abs(y) > MAX_CHUNK_DIST or math.abs(z) > MAX_CHUNK_DIST then 54 + return FakeChunk.data 55 + end 56 + 57 + task.desynchronize() 58 + local chunk = TG:GetChunk(x, y, z) 59 + local chunkdata = chunk.data 60 + task.synchronize() 61 + 62 + return chunkdata 63 + 64 + end 65 + 66 + local tickRemote = ReplicatedStorage.Tick 67 + local remotes = ReplicatedStorage:WaitForChild("Remotes") 68 + local placeRemote = remotes:WaitForChild("PlaceBlock") 69 + local breakRemote = remotes:WaitForChild("BreakBlock") 70 + local blocksFolder = ReplicatedStorage:WaitForChild("Blocks") 71 + local function propogate(a, cx, cy, cz, x, y, z, bd) 72 + task.synchronize() 73 + tickRemote:FireAllClients(a, cx, cy, cz, x, y, z, bd) 74 + task.desynchronize() 75 + end 76 + 77 + local MAX_REACH = 24 78 + local blockIdMap = {} 79 + 80 + local function rebuildBlockIdMap() 81 + table.clear(blockIdMap) 82 + for _, block in ipairs(blocksFolder:GetChildren()) do 83 + local id = block:GetAttribute("n") 84 + if id ~= nil then 85 + blockIdMap[id] = id 86 + blockIdMap[tostring(id)] = id 87 + end 88 + end 89 + end 90 + 91 + rebuildBlockIdMap() 92 + blocksFolder.ChildAdded:Connect(rebuildBlockIdMap) 93 + blocksFolder.ChildRemoved:Connect(rebuildBlockIdMap) 94 + 95 + local function getPlayerPosition(player: Player): Vector3? 96 + local character = player.Character 97 + if not character then 98 + return nil 99 + end 100 + local root = character:FindFirstChild("HumanoidRootPart") 101 + if not root then 102 + return nil 103 + end 104 + return root.Position 105 + end 106 + 107 + local function isWithinReach(player: Player, cx: number, cy: number, cz: number, x: number, y: number, z: number): boolean 108 + local playerPos = getPlayerPosition(player) 109 + if not playerPos then 110 + return false 111 + end 112 + local blockPos = Util.ChunkPosToCFrame(Vector3.new(cx, cy, cz), Vector3.new(x, y, z)).Position 113 + return (blockPos - playerPos).Magnitude <= MAX_REACH 114 + end 115 + 116 + local function resolveBlockId(blockId: any): string | number | nil 117 + return blockIdMap[blockId] 118 + end 119 + 120 + local function getServerChunk(cx: number, cy: number, cz: number) 121 + task.desynchronize() 122 + local chunk = TG:GetChunk(cx, cy, cz) 123 + task.synchronize() 124 + return chunk 125 + end 126 + 127 + placeRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z, blockId) 128 + --print("place",player, cx, cy, cz, x, y, z, blockData) 129 + 130 + if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then 131 + return 132 + end 133 + if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then 134 + return 135 + end 136 + if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then 137 + return 138 + end 139 + if math.abs(cx) > MAX_CHUNK_DIST or math.abs(cy) > MAX_CHUNK_DIST or math.abs(cz) > MAX_CHUNK_DIST then 140 + --return 141 + end 142 + if not isWithinReach(player, cx, cy, cz, x, y, z) then 143 + return 144 + end 145 + local resolvedId = resolveBlockId(blockId) 146 + if not resolvedId then 147 + return 148 + end 149 + 150 + local chunk = getServerChunk(cx, cy, cz) 151 + if chunk:GetBlockAt(x, y, z) then 152 + return 153 + end 154 + local data = { 155 + id = resolvedId, 156 + state = {} 157 + } 158 + chunk:CreateBlock(x, y, z, data) 159 + propogate("B_C", cx, cy, cz, x, y, z, data) 160 + end) 161 + 162 + breakRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z) 163 + --print("del",player, cx, cy, cz, x, y, z) 164 + 165 + if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then 166 + return 167 + end 168 + if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then 169 + return 170 + end 171 + if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then 172 + return 173 + end 174 + if math.abs(cx) > MAX_CHUNK_DIST or math.abs(cy) > MAX_CHUNK_DIST or math.abs(cz) > MAX_CHUNK_DIST then 175 + return 176 + end 177 + if not isWithinReach(player, cx, cy, cz, x, y, z) then 178 + return 179 + end 180 + 181 + local chunk = getServerChunk(cx, cy, cz) 182 + if not chunk:GetBlockAt(x, y, z) then 183 + return 184 + end 185 + chunk:RemoveBlock(x, y, z) 186 + propogate("B_D", cx, cy, cz, x, y, z, 0) 187 + end) 188 + 189 + task.desynchronize()
+4
src/ServerScriptService/Actor/init.meta.json
··· 1 + { 2 + "className": "Actor", 3 + "ignoreUnknownInstances": true 4 + }
+22
src/StarterGui/Crosshair/LocalScript.client.lua
··· 1 + -- force first person mode on the person's camera 2 + 3 + if not game:IsLoaded() then 4 + game.Loaded:Wait() 5 + end 6 + 7 + local ReplicatedStorage = game:GetService("ReplicatedStorage") 8 + local UIS = game:GetService("UserInputService") 9 + 10 + ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded") 11 + 12 + game:GetService("Players").LocalPlayer.CameraMode = Enum.CameraMode.LockFirstPerson 13 + UIS.MouseIconEnabled = false 14 + 15 + UIS.InputEnded:Connect(function(k) 16 + if k.KeyCode == Enum.KeyCode.M then 17 + local v = not script.Parent.DummyButton.Modal 18 + UIS.MouseIconEnabled = v 19 + script.Parent.CrosshairLabel.Visible = not v 20 + script.Parent.DummyButton.Modal = v 21 + end 22 + end)
+4
src/StarterGui/Crosshair/init.meta.json
··· 1 + { 2 + "className": "ScreenGui", 3 + "ignoreUnknownInstances": true 4 + }
+62
src/StarterGui/Game_UI/LocalScript.client.lua
··· 1 + if not game:IsLoaded() then 2 + game.Loaded:Wait() 3 + end 4 + 5 + local ui = script.Parent 6 + 7 + local ReplicatedStorage = game:GetService("ReplicatedStorage") 8 + 9 + ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded") 10 + 11 + local cd = ReplicatedStorage.Objects.ChunkDebug:Clone() 12 + local sky = ReplicatedStorage.Objects.Sky:Clone() 13 + local base = ReplicatedStorage.Objects.FakeBaseplate:Clone() 14 + 15 + 16 + cd.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") 17 + sky.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") 18 + base.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") 19 + 20 + 21 + game:GetService("RunService").RenderStepped:Connect(function(dt) 22 + local fps = math.round(1/dt) 23 + pcall(function() 24 + -- pos in chunks of 32 studs of char 25 + local pos = game:GetService("Players").LocalPlayer.Character:GetPivot() 26 + local chunk = { 27 + x = math.round(pos.X/32), 28 + y = math.round(pos.Y/32), 29 + z = math.round(pos.Z/32) 30 + } 31 + 32 + if math.abs(chunk.x) == 0 then chunk.x = 0 end 33 + if math.abs(chunk.y) == 0 then chunk.y = 0 end 34 + if math.abs(chunk.z) == 0 then chunk.z = 0 end 35 + 36 + local bpos = { 37 + x = math.round(pos.X/4), 38 + y = math.round(pos.Y/4), 39 + z = math.round(pos.Z/4) 40 + } 41 + 42 + if math.abs(bpos.x) == 0 then bpos.x = 0 end 43 + if math.abs(bpos.y) == 0 then bpos.y = 0 end 44 + if math.abs(bpos.z) == 0 then bpos.z = 0 end 45 + 46 + sky.CFrame = pos 47 + ui.DebugUpperText.Text = `Chunk {chunk.x} {chunk.y} {chunk.z}\nPos {bpos.x} {bpos.y} {bpos.z}\n<b>{fps} FPS</b>` 48 + 49 + cd:PivotTo(CFrame.new( 50 + chunk.x*32, 51 + chunk.y*32, 52 + chunk.z*32 53 + )) 54 + 55 + base.CFrame = CFrame.new( 56 + chunk.x*32, 57 + -24, 58 + chunk.z*32 59 + ) 60 + 61 + end) 62 + end)
+4
src/StarterGui/Game_UI/init.meta.json
··· 1 + { 2 + "className": "ScreenGui", 3 + "ignoreUnknownInstances": true 4 + }
+26
src/StarterPlayer/StarterPlayerScripts/Actor/ActorInit.client.lua
··· 1 + if not game:IsLoaded() then 2 + game.Loaded:Wait() 3 + end 4 + 5 + pcall(function() 6 + task.synchronize() 7 + game:GetService("Workspace"):WaitForChild("$blockscraft_server",5):Destroy() 8 + end) 9 + 10 + local ReplicatedStorage = game:GetService("ReplicatedStorage") 11 + 12 + ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded") 13 + 14 + local ML = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ModLoader")) 15 + 16 + ML.loadModsC() 17 + 18 + do 19 + local PM = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("PlacementManager")) 20 + PM:Init() 21 + end 22 + 23 + do 24 + local CM = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ChunkManager")) 25 + CM:Init() 26 + end
+92
src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua
··· 1 + if not game:IsLoaded() then 2 + game.Loaded:Wait() 3 + end 4 + 5 + local ReplicatedStorage = game:GetService("ReplicatedStorage") 6 + local UIS = game:GetService("UserInputService") 7 + 8 + ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded") 9 + 10 + local blocksFolder = ReplicatedStorage:WaitForChild("Blocks") 11 + local PM = require(ReplicatedStorage.Shared.PlacementManager) 12 + 13 + local HOTBAR_SIZE = 9 14 + local hotbar = table.create(HOTBAR_SIZE) 15 + local selectedSlot = 1 16 + 17 + local keyToSlot = { 18 + [Enum.KeyCode.One] = 1, 19 + [Enum.KeyCode.Two] = 2, 20 + [Enum.KeyCode.Three] = 3, 21 + [Enum.KeyCode.Four] = 4, 22 + [Enum.KeyCode.Five] = 5, 23 + [Enum.KeyCode.Six] = 6, 24 + [Enum.KeyCode.Seven] = 7, 25 + [Enum.KeyCode.Eight] = 8, 26 + [Enum.KeyCode.Nine] = 9, 27 + } 28 + 29 + local function rebuildHotbar() 30 + local ids = {} 31 + for _, block in ipairs(blocksFolder:GetChildren()) do 32 + local id = block:GetAttribute("n") 33 + if id ~= nil then 34 + table.insert(ids, tostring(id)) 35 + end 36 + end 37 + 38 + table.sort(ids) 39 + for i = 1, HOTBAR_SIZE do 40 + hotbar[i] = ids[i] or "" 41 + end 42 + selectedSlot = math.clamp(selectedSlot, 1, HOTBAR_SIZE) 43 + end 44 + 45 + local function getSelectedBlockId(): string? 46 + local id = hotbar[selectedSlot] 47 + if id == "" then 48 + return nil 49 + end 50 + return id 51 + end 52 + 53 + local function setSelectedSlot(slot: number) 54 + if slot < 1 or slot > HOTBAR_SIZE then 55 + return 56 + end 57 + selectedSlot = slot 58 + end 59 + 60 + rebuildHotbar() 61 + blocksFolder.ChildAdded:Connect(rebuildHotbar) 62 + blocksFolder.ChildRemoved:Connect(rebuildHotbar) 63 + 64 + UIS.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean) 65 + if gameProcessedEvent then 66 + return 67 + end 68 + 69 + local slot = keyToSlot[input.KeyCode] 70 + if slot then 71 + setSelectedSlot(slot) 72 + return 73 + end 74 + 75 + if input.UserInputType == Enum.UserInputType.MouseButton1 then 76 + local mouseBlock = PM:GetBlockAtMouse() 77 + if not mouseBlock then 78 + return 79 + end 80 + PM:BreakBlock(mouseBlock.chunk.X, mouseBlock.chunk.Y, mouseBlock.chunk.Z, mouseBlock.block.X, mouseBlock.block.Y, mouseBlock.block.Z) 81 + elseif input.UserInputType == Enum.UserInputType.MouseButton2 then 82 + local mouseBlock = PM:GetPlacementAtMouse() 83 + if not mouseBlock then 84 + return 85 + end 86 + local blockId = getSelectedBlockId() 87 + if not blockId then 88 + return 89 + end 90 + PM:PlaceBlock(mouseBlock.chunk.X, mouseBlock.chunk.Y, mouseBlock.chunk.Z, mouseBlock.block.X, mouseBlock.block.Y, mouseBlock.block.Z, blockId) 91 + end 92 + end)
+4
src/StarterPlayer/StarterPlayerScripts/Actor/init.meta.json
··· 1 + { 2 + "className": "Actor", 3 + "ignoreUnknownInstances": true 4 + }
+3
src/Workspace/mods/init.meta.json
··· 1 + { 2 + "ignoreUnknownInstances": true 3 + }
+23
src/Workspace/mods/mc/init.lua
··· 1 + local mod = { 2 + name = "Blockscraft", 3 + description = "Base Blockscraft blocks", 4 + ns = "mc", 5 + author = { "ocbwoy3" } 6 + } 7 + 8 + local rep = game:GetService("ReplicatedStorage"):WaitForChild("Blocks") 9 + local upd = game:GetService("ReplicatedStorage"):WaitForChild("BlockUpdateOperations") 10 + 11 + function mod.init() 12 + for a,b in pairs(script.blocks:GetChildren()) do 13 + local upop = b:FindFirstChild("BlockUpdateOperation") 14 + if upop then 15 + upop.Name = b:GetAttribute("n") 16 + upop:SetAttribute("n",b:GetAttribute("n")) 17 + upop.Parent = upd 18 + end 19 + b:Clone().Parent = rep 20 + end 21 + end 22 + 23 + return mod
+3
src/Workspace/mods/mc/init.meta.json
··· 1 + { 2 + "ignoreUnknownInstances": true 3 + }
+3
src/Workspace/mods/mc/upds/BlockUpdateOperation.lua
··· 1 + return function(p: typeof(script.Parent.Parent.blocks["mc:grass_block"])) 2 + p.block.Grass.Color = Color3.new(p:GetAttribute("x"),p:GetAttribute("x"),p:GetAttribute("x")) 3 + end
+3
src/Workspace/mods/mc/upds/init.meta.json
··· 1 + { 2 + "ignoreUnknownInstances": true 3 + }