From 0ac6d6e21413536dbeb152db7296646911b495b6 Mon Sep 17 00:00:00 2001 From: ocbwoy3 Date: Tue, 6 Jan 2026 21:53:52 +0200 Subject: [PATCH] chore: init --- default.project.json | 35 + .../Shared/ChunkManager/BlockManager.lua | 86 + .../ChunkManager/BlockManager.meta.json | 3 + .../Shared/ChunkManager/Chunk.lua | 180 ++ .../Shared/ChunkManager/ChunkBuilder.lua | 257 +++ .../Shared/ChunkManager/init.lua | 232 +++ src/ReplicatedStorage/Shared/ModLoader.lua | 47 + .../Shared/PlacementManager.lua | 221 ++ .../Shared/PlacementManager.meta.json | 3 + src/ReplicatedStorage/Shared/Util.lua | 48 + src/ReplicatedStorage/Shared/init.meta.json | 3 + .../TerrainGen/Deflate/Huffman/BitBuffer.lua | 1816 +++++++++++++++++ .../TerrainGen/Deflate/Huffman/Node.lua | 38 + .../Deflate/Huffman/PriorityQueue.lua | 232 +++ .../TerrainGen/Deflate/Huffman/init.lua | 114 ++ .../TerrainGen/Deflate/LZW.lua | 170 ++ .../TerrainGen/Deflate/init.lua | 19 + .../ServerChunkManager/TerrainGen/init.lua | 70 + .../Actor/ServerChunkManager/init.server.lua | 189 ++ src/ServerScriptService/Actor/init.meta.json | 4 + .../Crosshair/LocalScript.client.lua | 22 + src/StarterGui/Crosshair/init.meta.json | 4 + src/StarterGui/Game_UI/LocalScript.client.lua | 62 + src/StarterGui/Game_UI/init.meta.json | 4 + .../Actor/ActorInit.client.lua | 26 + .../Actor/BlockInteraction.client.lua | 92 + .../StarterPlayerScripts/Actor/init.meta.json | 4 + src/Workspace/mods/init.meta.json | 3 + src/Workspace/mods/mc/init.lua | 23 + src/Workspace/mods/mc/init.meta.json | 3 + .../mods/mc/upds/BlockUpdateOperation.lua | 3 + src/Workspace/mods/mc/upds/init.meta.json | 3 + 32 files changed, 4016 insertions(+) create mode 100644 default.project.json create mode 100644 src/ReplicatedStorage/Shared/ChunkManager/BlockManager.lua create mode 100644 src/ReplicatedStorage/Shared/ChunkManager/BlockManager.meta.json create mode 100644 src/ReplicatedStorage/Shared/ChunkManager/Chunk.lua create mode 100644 src/ReplicatedStorage/Shared/ChunkManager/ChunkBuilder.lua create mode 100644 src/ReplicatedStorage/Shared/ChunkManager/init.lua create mode 100644 src/ReplicatedStorage/Shared/ModLoader.lua create mode 100644 src/ReplicatedStorage/Shared/PlacementManager.lua create mode 100644 src/ReplicatedStorage/Shared/PlacementManager.meta.json create mode 100644 src/ReplicatedStorage/Shared/Util.lua create mode 100644 src/ReplicatedStorage/Shared/init.meta.json create mode 100644 src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/BitBuffer.lua create mode 100644 src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/Node.lua create mode 100644 src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/PriorityQueue.lua create mode 100644 src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/init.lua create mode 100644 src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/LZW.lua create mode 100644 src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/init.lua create mode 100644 src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/init.lua create mode 100644 src/ServerScriptService/Actor/ServerChunkManager/init.server.lua create mode 100644 src/ServerScriptService/Actor/init.meta.json create mode 100644 src/StarterGui/Crosshair/LocalScript.client.lua create mode 100644 src/StarterGui/Crosshair/init.meta.json create mode 100644 src/StarterGui/Game_UI/LocalScript.client.lua create mode 100644 src/StarterGui/Game_UI/init.meta.json create mode 100644 src/StarterPlayer/StarterPlayerScripts/Actor/ActorInit.client.lua create mode 100644 src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua create mode 100644 src/StarterPlayer/StarterPlayerScripts/Actor/init.meta.json create mode 100644 src/Workspace/mods/init.meta.json create mode 100644 src/Workspace/mods/mc/init.lua create mode 100644 src/Workspace/mods/mc/init.meta.json create mode 100644 src/Workspace/mods/mc/upds/BlockUpdateOperation.lua create mode 100644 src/Workspace/mods/mc/upds/init.meta.json diff --git a/default.project.json b/default.project.json new file mode 100644 index 0000000..bddf56a --- /dev/null +++ b/default.project.json @@ -0,0 +1,35 @@ +{ + "name": "project", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "$className": "ReplicatedStorage", + "$ignoreUnknownInstances": true, + "$path": "src/ReplicatedStorage" + }, + "ServerScriptService": { + "$className": "ServerScriptService", + "$ignoreUnknownInstances": true, + "$path": "src/ServerScriptService" + }, + "StarterGui": { + "$className": "StarterGui", + "$ignoreUnknownInstances": true, + "$path": "src/StarterGui" + }, + "StarterPlayer": { + "$className": "StarterPlayer", + "StarterPlayerScripts": { + "$className": "StarterPlayerScripts", + "$ignoreUnknownInstances": true, + "$path": "src/StarterPlayer/StarterPlayerScripts" + }, + "$ignoreUnknownInstances": true + }, + "Workspace": { + "$className": "Workspace", + "$ignoreUnknownInstances": true, + "$path": "src/Workspace" + } + } +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Shared/ChunkManager/BlockManager.lua b/src/ReplicatedStorage/Shared/ChunkManager/BlockManager.lua new file mode 100644 index 0000000..002c9d3 --- /dev/null +++ b/src/ReplicatedStorage/Shared/ChunkManager/BlockManager.lua @@ -0,0 +1,86 @@ +local BlockManager = {} + +BlockManager.BlockIdMappings = {} :: {BasePart} + +for i,v in pairs(game:GetService("ReplicatedStorage"):WaitForChild("Blocks"):GetChildren()) do + BlockManager.BlockIdMappings[v:GetAttribute("n")] = v +end + +BlockManager.UpdateIdMappings = {} :: {BasePart} + +for i,v in pairs(game:GetService("ReplicatedStorage"):WaitForChild("BlockUpdateOperations"):GetChildren()) do + local success, reason = pcall(function() + BlockManager.UpdateIdMappings[v:GetAttribute("n")] = require(v) + end) + if not success then + warn("[BLOCKMANAGER] Invalid update operation",v:GetAttribute("n"),":",reason) + end +end + +local warnedBlockIds = {} + +function BlockManager:GetBlock(blockId: number, attr: {[typeof("")]: any}?) + + task.synchronize() + + if not BlockManager.BlockIdMappings[blockId] then + if not warnedBlockIds[blockId] then + warnedBlockIds[blockId] = true + warn("[BLOCKMANAGER] Invalid block id",blockId) + end + return script.invalid:Clone() + end + + local b = BlockManager.BlockIdMappings[blockId]:Clone() + b.Size = Vector3.new(3.95,3.95,3.95) + + for i,v in pairs(attr or {}) do + b:SetAttribute(i,v) + end + + if BlockManager.UpdateIdMappings[blockId] then + local success, reason = pcall(function() + BlockManager.UpdateIdMappings[blockId](b) + end) + if not success then + warn("[BLOCKMANAGER] Failed update operation",blockId,":",reason) + end + end + + return b +end + +-- ChatGPT Generated Func!!!! +function BlockManager:GetBlockRotated(blockId: number, face: Enum.NormalId, attr: {[typeof("")]: any}?) + -- Returns block with id blockId, rotated so the given face (NormalId) points north (+X). + local block = BlockManager:GetBlock(blockId, attr) + local rot = CFrame.new() + + task.synchronize() + + if face == Enum.NormalId.Front then + rot = CFrame.Angles(0, 0, 0) -- no rot + elseif face == Enum.NormalId.Back then + rot = CFrame.Angles(0, math.rad(180), 0) + elseif face == Enum.NormalId.Left then + rot = CFrame.Angles(0, math.rad(90), 0) + elseif face == Enum.NormalId.Right then + rot = CFrame.Angles(0, math.rad(-90), 0) + elseif face == Enum.NormalId.Top then + rot = CFrame.Angles(math.rad(-90), 0, 0) -- top +x + elseif face == Enum.NormalId.Bottom then + rot = CFrame.Angles(math.rad(90), 0, 0) -- bottom +x + end + + + -- ocbwoy3's fix + block:PivotTo(rot) + block.CFrame = CFrame.new(rot.Position) + + return block +end + + + + +return BlockManager diff --git a/src/ReplicatedStorage/Shared/ChunkManager/BlockManager.meta.json b/src/ReplicatedStorage/Shared/ChunkManager/BlockManager.meta.json new file mode 100644 index 0000000..1025b06 --- /dev/null +++ b/src/ReplicatedStorage/Shared/ChunkManager/BlockManager.meta.json @@ -0,0 +1,3 @@ +{ + "ignoreUnknownInstances": true +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Shared/ChunkManager/Chunk.lua b/src/ReplicatedStorage/Shared/ChunkManager/Chunk.lua new file mode 100644 index 0000000..3a45620 --- /dev/null +++ b/src/ReplicatedStorage/Shared/ChunkManager/Chunk.lua @@ -0,0 +1,180 @@ +local Chunk = {} +Chunk.__index = Chunk + +Chunk.UpdateBlockBindable = Instance.new("BindableEvent") :: BindableEvent + +Chunk.AllChunks = {} :: {[typeof("")]: typeof(Chunk.new(0,0,0))} + +local RunService = game:GetService("RunService") + +local function Swait(l) + for i = 1,l do + RunService.Stepped:Wait() + end +end + + +export type BlockData = { + id: number, + state: { + [typeof("")]: string | boolean | number + } +} + +function Chunk.new(x,y,z) + local self = setmetatable({}, Chunk) + self.pos = Vector3.new(x,y,z) + + -- Tick ONLY in a 5 chunk distance of LP's char + self.inhabitedTime = tick() + self.instance = Instance.new("Folder") + self.unloadChunkHook = function() end + self.UpdateBlockBindableL = Instance.new("BindableEvent") :: BindableEvent + + self.loaded = false + self.loading = false + + self.data = {} :: {[typeof("")]: BlockData} -- "X,Y,Z": BlockData ("-1,-1,1": BlockData) + return self +end + +function Chunk.from(x,y,z,data) + local self = setmetatable({}, Chunk) + self.pos = Vector3.new(x,y,z) + + -- Tick ONLY in a 5 chunk distance of LP's char + self.inhabitedTime = tick() + self.instance = Instance.new("Folder") + self.unloadChunkHook = function() end + self.UpdateBlockBindableL = Instance.new("BindableEvent") :: BindableEvent + + self.data = data :: {[typeof("")]: BlockData} -- "X,Y,Z": BlockData ("-1,-1,1": BlockData) + return self +end + +function Chunk:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, offsetX, offsetY, offsetZ) + task.desynchronize() + -- Calculate the local position of the neighboring block + local neighborRX, neighborRY, neighborRZ = rx + offsetX, ry + offsetY, rz + offsetZ + local neighborGX, neighborGY, neighborGZ = gx, gy, gz + + -- Adjust for chunk boundaries + if neighborRX < 1 then + neighborRX = 8 + neighborGX = gx - 1 + elseif neighborRX > 8 then + neighborRX = 1 + neighborGX = gx + 1 + end + + if neighborRY < 1 then + neighborRY = 8 + neighborGY = gy - 1 + elseif neighborRY > 8 then + neighborRY = 1 + neighborGY = gy + 1 + end + + if neighborRZ < 1 then + neighborRZ = 8 + neighborGZ = gz - 1 + elseif neighborRZ > 8 then + neighborRZ = 1 + neighborGZ = gz + 1 + end + + if neighborGY < 0 then + return true + end + + -- Get the neighboring chunk + local neighborChunk = Chunk.AllChunks[`{neighborGX},{neighborGY},{neighborGZ}`] + if not neighborChunk then + return false -- Treat unloaded chunks as empty so edges render + end + + -- Check if the block exists in the neighboring chunk + return neighborChunk:GetBlockAt(neighborRX, neighborRY, neighborRZ) ~= nil +end + +function Chunk:IsBlockRenderable(rx, ry, rz) + task.desynchronize() + local gx, gy, gz = self.pos.X, self.pos.Y, self.pos.Z + -- Check all six neighboring blocks + local d = not ( + self:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, 1, 0, 0) and + self:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, -1, 0, 0) and + self:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, 0, 1, 0) and + self:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, 0, -1, 0) and + self:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, 0, 0, 1) and + self:DoesNeighboringBlockExist(rx, ry, rz, gx, gy, gz, 0, 0, -1) + ) + return d +end + + +function Chunk:Tick() + self.inhabitedTime = tick() +end + +function Chunk:PropogateChanges(x: number,y: number,z: number,d:BlockData) + self.UpdateBlockBindableL:Fire(x,y,z,d) +end + +function Chunk:GetBlockAt(x,y,z) + task.desynchronize() + if not self.data[`{tostring(x)},{tostring(y)},{tostring(z)}`] then + return nil + end + return self.data[`{tostring(x)},{tostring(y)},{tostring(z)}`] +end + +function Chunk:CreateBlock(x: number,y: number,z: number,d:BlockData) + self.data[`{tostring(x)},{tostring(y)},{tostring(z)}`] = d + self:PropogateChanges(x,y,z,d) + return self:GetBlockAt(x,y,z) +end + +function Chunk:RemoveBlock(x, y, z) + self.data[x .. "," .. y .. "," .. z] = nil + self:PropogateChanges(x,y,z,0) +end + +-- unsure why this exists +function Chunk:Load() +end + +function Chunk:Unload() + + task.synchronize() + self.loaded = false + + -- SLOWCLEAR + + task.defer(function() + local g = 0 + for _,v in pairs(self.instance:GetChildren()) do + pcall(function() + v:Destroy() + end) + g += 1 + if g == 30 then + g = 0 + Swait(2) + end + end + + task.synchronize() + self.instance.Parent = nil + self.instance:Destroy() + self.unloadChunkHook() + task.desynchronize() + end) +end + +-- DO NOT INTERACT WITH CHUNK AFTER CALLING THIS +function Chunk:Destroy() + self.data = {} +end + +return Chunk :: typeof(Chunk) diff --git a/src/ReplicatedStorage/Shared/ChunkManager/ChunkBuilder.lua b/src/ReplicatedStorage/Shared/ChunkManager/ChunkBuilder.lua new file mode 100644 index 0000000..a207873 --- /dev/null +++ b/src/ReplicatedStorage/Shared/ChunkManager/ChunkBuilder.lua @@ -0,0 +1,257 @@ +local ChunkBuilder = {} + +local Chunk = require("./Chunk") +local BlockManager = require("./BlockManager") +local util = require("../Util") + +local objects = script.Parent.Parent.Parent.Objects + +local RunService = game:GetService("RunService") + +local ChunkBorderFolder = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") + +local NEIGHBOR_OFFSETS = { + {-1, 0, 0}, {1, 0, 0}, + {0, -1, 0}, {0, 1, 0}, + {0, 0, -1}, {0, 0, 1} +} + +-- TODO: Move to Chunk +type BlockData = { + id: number, + state: { + [typeof("")]: string | boolean | number + } +} + +local function placeBorder(a,b,c) + local pos = util.ChunkPosToCFrame(Vector3.new(a,b,c),Vector3.new(1,1,1)).Position - Vector3.new(2,2,2) + local d = objects.ChunkLoading:Clone() + d:PivotTo(CFrame.new(pos)) + d.Parent = ChunkBorderFolder + return d +end + +local function Swait(l) + for i = 1,l do + RunService.Stepped:Wait() + end +end + +local function propogateNeighboringBlockChanges(cx, cy, cz, x, y, z) + --warn("propogateNeighboringBlockChanges",cx,cy,cz,x,y,z) + -- updates block in another chunk + local c = Chunk.AllChunks[`{cx},{cy},{cz}`] + if not c then return end + + local d = c.data[`{x},{y},{z}`] + if not d then return end + + if c:IsBlockRenderable(x, y, z) then + if c.instance:FindFirstChild(`{x},{y},{z}`) then return end + task.synchronize() + local block = BlockManager:GetBlockRotated(d.id, util.RotationStringToNormalId(d.state["r"] or "f"), d.state) + block.Name = `{x},{y},{z}` + block:PivotTo(util.ChunkPosToCFrame(c.pos, Vector3.new(x, y, z))) + block.Parent = c.instance + task.desynchronize() + else + local existing = c.instance:FindFirstChild(`{x},{y},{z}`) + if existing then + task.synchronize() + existing:Destroy() + task.desynchronize() + end + end +end + +function ChunkBuilder:BuildChunk(c: typeof(Chunk.new(0,0,0)),parent: Instance?) + + if c.loaded then + return c.instance + end + + local blocks = c.data + local newcache = {} :: {[typeof("")]: BlockData} + + local finished = false + + + local ch = Instance.new("Folder") + ch.Parent = parent + ch.Name = `{c.pos.X},{c.pos.Y},{c.pos.Z}` + + local conn = c.UpdateBlockBindableL.Event:Connect(function(x: number, y: number, z: number, d: BlockData) + task.desynchronize() + if finished == false then + newcache[`{x},{y},{z}`] = d + return + end + task.synchronize() + for _, o in pairs(NEIGHBOR_OFFSETS) do + --warn("propogate",o[1],o[2],o[3]) + -- Adjust for chunk boundaries + local b = {x = x + o[1], y = y + o[2], z = z + o[3]} + local ch = {x = c.pos.X, y = c.pos.Y, z = c.pos.Z} + + if b.x < 1 then ch.x = c.pos.X - 1 b.x = 8 end + if b.x > 8 then ch.x = c.pos.X + 1 b.x = 1 end + if b.y < 1 then ch.y = c.pos.Y - 1 b.y = 8 end + if b.y > 8 then ch.y = c.pos.Y + 1 b.y = 1 end + if b.z < 1 then ch.z = c.pos.Z - 1 b.z = 8 end + if b.z > 8 then ch.z = c.pos.Z + 1 b.z = 1 end + + propogateNeighboringBlockChanges(ch.x, ch.y, ch.z, b.x, b.y, b.z) + --BlockManager:GetBlock(ch.x) + end + + local blockName = `{x},{y},{z}` + local existing = ch:FindFirstChild(blockName) + if d == 0 then + if existing then + task.synchronize() + existing:Destroy() + task.desynchronize() + end + return + end + if not c:IsBlockRenderable(x, y, z) then + if existing then + task.synchronize() + existing:Destroy() + task.desynchronize() + end + return + end + if existing then + task.synchronize() + existing:Destroy() + task.desynchronize() + end + if not d then return end + if d.id == 0 then return end + local N = util.RotationStringToNormalId(d.state["r"] or "f") + task.synchronize() + local block = BlockManager:GetBlockRotated(d.id, N, d.state) + block.Name = blockName + block:PivotTo(util.ChunkPosToCFrame(c.pos, Vector3.new(x, y, z))) + block.Parent = ch + task.desynchronize() + end) + + c.unloadChunkHook = function() + conn:Disconnect() + blocks = nil + c = nil + end + + task.defer(function() + + local p = 0 + + task.synchronize() + + local hb = false + + for a,b in pairs(blocks) do + hb = true + end + + local border = Instance.new("Part") + if hb == true then + border:Destroy() + border = placeBorder(c.pos.X, c.pos.Y, c.pos.Z) + end + + for a,b in pairs(blocks) do + task.desynchronize() + local coords = util.BlockPosStringToCoords(a) + if not c:IsBlockRenderable(coords.X, coords.Y, coords.Z) then + if ch:FindFirstChild(a) then + task.synchronize() + ch:FindFirstChild(a):Destroy() + task.desynchronize() + end + continue + end + task.desynchronize() + local N = util.RotationStringToNormalId(b.state["r"] or "f") + task.synchronize() + local block = BlockManager:GetBlockRotated(b.id, N, b.state) + if ch:FindFirstChild(a) then + ch:FindFirstChild(a):Destroy() + end + block.Name = a + block:PivotTo(util.ChunkPosToCFrame(c.pos, coords)) + block.Parent = ch + p += 1 + if p == 15 then + p = 0 + Swait(1) + end + end + + finished = true + + task.synchronize() + border:Destroy() + task.desynchronize() + + task.defer(function() + task.synchronize() + for key, data in pairs(newcache) do + local coords = util.BlockPosStringToCoords(key) + for _, o in pairs(NEIGHBOR_OFFSETS) do + -- chunks are 8x8x8 + local nb = {x = coords.X + o[1], y = coords.Y + o[2], z = coords.Z + o[3]} + local chCoords = {x = c.pos.X, y = c.pos.Y, z = c.pos.Z} + if nb.x == 0 then chCoords.x = c.pos.X - 1 nb.x = 8 end + if nb.x == 9 then chCoords.x = c.pos.X + 1 nb.x = 1 end + + if nb.y == 0 then chCoords.y = c.pos.Y - 1 nb.y = 8 end + if nb.y == 9 then chCoords.y = c.pos.Y + 1 nb.y = 1 end + + if nb.z == 0 then chCoords.z = c.pos.Z - 1 nb.z = 8 end + if nb.z == 9 then chCoords.z = c.pos.Z + 1 nb.z = 1 end + + propogateNeighboringBlockChanges(chCoords.x, chCoords.y, chCoords.z, nb.x, nb.y, nb.z) + end + + local existing = ch:FindFirstChild(key) + if data == 0 or (data and data.id == 0) then + if existing then + existing:Destroy() + end + continue + end + if not c:IsBlockRenderable(coords.X, coords.Y, coords.Z) then + if existing then + existing:Destroy() + end + continue + end + if existing then + existing:Destroy() + end + if not data then + continue + end + local N = util.RotationStringToNormalId(data.state["r"] or "f") + local block = BlockManager:GetBlockRotated(data.id, N, data.state) + block.Name = key + block:PivotTo(util.ChunkPosToCFrame(c.pos, coords)) + block.Parent = ch + end + newcache = nil + blocks = nil + end) + task.desynchronize() + end) + + c.loaded = true + + return ch + +end + +return ChunkBuilder diff --git a/src/ReplicatedStorage/Shared/ChunkManager/init.lua b/src/ReplicatedStorage/Shared/ChunkManager/init.lua new file mode 100644 index 0000000..aaa60d0 --- /dev/null +++ b/src/ReplicatedStorage/Shared/ChunkManager/init.lua @@ -0,0 +1,232 @@ +local ChunkManager = {} + +local RunService = game:GetService("RunService") + +local Chunk = require("./ChunkManager/Chunk") +local BlockManager = require("./ChunkManager/BlockManager") +local ChunkBuilder = require("./ChunkManager/ChunkBuilder") + +local remote = game:GetService("ReplicatedStorage"):WaitForChild("RecieveChunkPacket") +local tickremote = game:GetService("ReplicatedStorage"):WaitForChild("Tick") + +local ChunkFolder = Instance.new("Folder") +ChunkFolder.Name = "$blockscraft_client" + +ChunkManager.ChunkFolder = ChunkFolder + +local CHUNK_RADIUS = 5 +local LOAD_BATCH = 8 +local FORCELOAD_CHUNKS = { + {0, 1, 0} +} + +local unloadingChunks = {} +local pendingChunkRequests = {} + +local CHUNK_OFFSETS = {} +do + for y = -CHUNK_RADIUS, CHUNK_RADIUS do + for x = -CHUNK_RADIUS, CHUNK_RADIUS do + for z = -CHUNK_RADIUS, CHUNK_RADIUS do + table.insert(CHUNK_OFFSETS, {x, y, z, (x * x) + (y * y) + (z * z)}) + end + end + end + table.sort(CHUNK_OFFSETS, function(a, b) + return a[4] < b[4] + end) +end + +local function Swait(l) + task.synchronize() + for _ = 1, l do + RunService.Stepped:Wait() + end +end + +function ChunkManager:GetChunk(x, y, z) + local key = `{x},{y},{z}` + if Chunk.AllChunks[key] then + return Chunk.AllChunks[key] + end + + if pendingChunkRequests[key] then + task.synchronize() + while pendingChunkRequests[key] do + task.wait() + end + return Chunk.AllChunks[key] + end + + task.synchronize() + pendingChunkRequests[key] = true + local ok, data = pcall(function() + return remote:InvokeServer(x, y, z) + end) + if not ok then + data = {} + end + task.synchronize() + local ch = Chunk.from(x, y, z, data) + Chunk.AllChunks[key] = ch + pendingChunkRequests[key] = nil + return ch +end + +local function ensureNeighboringChunksLoaded(x, y, z) + local offsets = { + {1, 0, 0}, {-1, 0, 0}, + {0, 1, 0}, {0, -1, 0}, + {0, 0, 1}, {0, 0, -1} + } + + for _, offset in ipairs(offsets) do + local nx, ny, nz = x + offset[1], y + offset[2], z + offset[3] + ChunkManager:GetChunk(nx, ny, nz):Tick() + end +end + +function ChunkManager:LoadChunk(x, y, z) + local key = `{x},{y},{z}` + if unloadingChunks[key] or not Chunk.AllChunks[key] or Chunk.AllChunks[key].loaded then + return + end + + unloadingChunks[key] = true + task.defer(function() + task.desynchronize() + ensureNeighboringChunksLoaded(x, y, z) + + local chunk = Chunk.AllChunks[key] + if not chunk then + chunk = ChunkManager:GetChunk(x, y, z) + Chunk.AllChunks[key] = chunk + end + + task.synchronize() + local instance = ChunkBuilder:BuildChunk(chunk, ChunkFolder) + chunk.instance = instance + chunk.loaded = true + unloadingChunks[key] = nil + end) +end + +function ChunkManager:ForceTick() + for _, coords in ipairs(FORCELOAD_CHUNKS) do + local key = `{coords[1]},{coords[2]},{coords[3]}` + local chunk = Chunk.AllChunks[key] + if not chunk then + ChunkManager:LoadChunk(coords[1], coords[2], coords[3]) + else + chunk:Tick() + end + end +end + +function ChunkManager:TickI() + for key, chunk in pairs(Chunk.AllChunks) do + if tick() - chunk.inhabitedTime <= 5 then + tickremote:FireServer(key) + end + end +end + +function ChunkManager:Tick() + ChunkManager:ForceTick() + local player = game:GetService("Players").LocalPlayer + if not player.Character then + return + end + + local pos = player.Character:GetPivot().Position + local chunkPos = { + x = math.round(pos.X / 32), + y = math.round(pos.Y / 32), + z = math.round(pos.Z / 32) + } + + task.defer(function() + local processed = 0 + for _, offset in ipairs(CHUNK_OFFSETS) do + local cx, cy, cz = chunkPos.x + offset[1], chunkPos.y + offset[2], chunkPos.z + offset[3] + local chunk = ChunkManager:GetChunk(cx, cy, cz) + chunk.inhabitedTime = tick() + if not chunk.loaded then + ChunkManager:LoadChunk(cx, cy, cz) + processed += 1 + if processed % LOAD_BATCH == 0 then + Swait(1) + end + end + end + end) + + --[[ + task.defer(function() + for y = 0, 2 do + task.defer(function() + for x = -CHUNK_RADIUS, CHUNK_RADIUS do + task.desynchronize() + for z = -CHUNK_RADIUS, CHUNK_RADIUS do + local cx, cy, cz = chunkPos.x + x, y, chunkPos.z + z + local key = `{cx},{cy},{cz}` + local chunk = ChunkManager:GetChunk(cx, cy, cz) + chunk.inhabitedTime = tick() + if not chunk.loaded then + ChunkManager:LoadChunk(cx, cy, cz) + Swait(2) + end + end + task.synchronize() + end + end) + Swait(10) + end + end) + --]] + + for key, loadedChunk in pairs(Chunk.AllChunks) do + if tick() - loadedChunk.inhabitedTime > 15 and not unloadingChunks[key] then + unloadingChunks[key] = true + task.defer(function() + task.synchronize() + loadedChunk:Unload() + loadedChunk:Destroy() + Chunk.AllChunks[key] = nil + unloadingChunks[key] = nil + end) + end + end +end + +function ChunkManager:Init() + if not RunService:IsClient() then + error("ChunkManager:Init can only be called on the client") + end + + ChunkFolder.Parent = game:GetService("Workspace") + ChunkManager:ForceTick() + + task.defer(function() + while true do + wait(2) + ChunkManager:TickI() + end + end) + + task.defer(function() + while true do + task.defer(function() + local success, err = pcall(function() + ChunkManager:Tick() + end) + if not success then + warn("[CHUNKMANAGER]", err) + end + end) + Swait(20) + end + end) +end + +return ChunkManager diff --git a/src/ReplicatedStorage/Shared/ModLoader.lua b/src/ReplicatedStorage/Shared/ModLoader.lua new file mode 100644 index 0000000..7049b8d --- /dev/null +++ b/src/ReplicatedStorage/Shared/ModLoader.lua @@ -0,0 +1,47 @@ +local ML = {} + +type modContext = { + name: string, + description: string, + ns: string, + author: {string}, + init: typeof(function()end) +} + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Shared = ReplicatedStorage:WaitForChild("Shared") +local ModsFolder = ReplicatedStorage:WaitForChild("Mods") + +function ML.loadModsS() + print("[SSModLoader] Loading Mods") + + for _, m in pairs(ModsFolder:GetChildren()) do + local success, reason = pcall(function() + -- ignore type err + local mod: modContext = require(m) + mod.init() + print(`[SSModLoader] Loaded {mod.name} ({mod.ns}) by {table.concat(mod.author,", ")}`) + end) + if not success then + warn(`[CSModLoader] Error loading {m.Name}: {reason}`) + end + end +end + +function ML.loadModsC() + print("[CSModLoader] Loading Mods") + + for _, m in pairs(ModsFolder:GetChildren()) do + local success, reason = pcall(function() + -- ignore type err + local mod: modContext = require(m) + mod.init() + print(`[CSModLoader] Loaded {mod.name} ({mod.ns}) by {table.concat(mod.author,", ")}`) + end) + if not success then + warn(`[CSModLoader] Error loading {m.Name}: {reason}`) + end + end +end + +return ML diff --git a/src/ReplicatedStorage/Shared/PlacementManager.lua b/src/ReplicatedStorage/Shared/PlacementManager.lua new file mode 100644 index 0000000..96c316f --- /dev/null +++ b/src/ReplicatedStorage/Shared/PlacementManager.lua @@ -0,0 +1,221 @@ +local PlacementManager = {} + +local ChunkManager = require("./ChunkManager") +local Util = require("./Util") + +PlacementManager.ChunkFolder = ChunkManager.ChunkFolder + +local raycastParams = RaycastParams.new() +raycastParams.FilterDescendantsInstances = {PlacementManager.ChunkFolder} +raycastParams.FilterType = Enum.RaycastFilterType.Include +raycastParams.IgnoreWater = true + +if _G.SB then return nil end +_G.SB = true + +PlacementManager.SelectionBox = script.SelectionBox:Clone() +PlacementManager.SelectionBox.Name = "$SelectionBox"..(game:GetService("RunService"):IsServer() and "_SERVER" or "") +PlacementManager.SelectionBox.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") + +-- Trash method TODO: Fix this +local function findParent(i: Instance): Instance + local f = i:FindFirstAncestorOfClass("Folder") + local d = i + repeat + d = d.Parent + until d.Parent == f + return d +end + +local Mouse: Mouse = nil +local lastNormalId: Enum.NormalId? = nil + +local function normalIdToOffset(normal: Enum.NormalId): Vector3 + if normal == Enum.NormalId.Top then + return Vector3.new(0, 1, 0) + elseif normal == Enum.NormalId.Bottom then + return Vector3.new(0, -1, 0) + elseif normal == Enum.NormalId.Left then + return Vector3.new(-1, 0, 0) + elseif normal == Enum.NormalId.Right then + return Vector3.new(1, 0, 0) + elseif normal == Enum.NormalId.Back then + return Vector3.new(0, 0, 1) + elseif normal == Enum.NormalId.Front then + return Vector3.new(0, 0, -1) + end + return Vector3.new(0, 0, 0) +end + +local function offsetChunkBlock(chunk: Vector3, block: Vector3, offset: Vector3) + local cx, cy, cz = chunk.X, chunk.Y, chunk.Z + local bx, by, bz = block.X + offset.X, block.Y + offset.Y, block.Z + offset.Z + + if bx < 1 then + bx = 8 + cx -= 1 + elseif bx > 8 then + bx = 1 + cx += 1 + end + + if by < 1 then + by = 8 + cy -= 1 + elseif by > 8 then + by = 1 + cy += 1 + end + + if bz < 1 then + bz = 8 + cz -= 1 + elseif bz > 8 then + bz = 1 + cz += 1 + end + + return Vector3.new(cx, cy, cz), Vector3.new(bx, by, bz) +end + +-- Gets the block and normalid of the block (and surface) the player is looking at +function PlacementManager:Raycast() + if not Mouse then + Mouse = game:GetService("Players").LocalPlayer:GetMouse() + end + task.synchronize() + local objLookingAt = Mouse.Target + local dir = Mouse.TargetSurface + if not objLookingAt then + PlacementManager.SelectionBox.Adornee = nil + script.RaycastResult.Value = nil + lastNormalId = nil + return + end + + --if not objLookingAt:IsDescendantOf(ChunkManager.ChunkFolder) then return end + local parent = findParent(objLookingAt) + if parent:GetAttribute("ns") == true then + PlacementManager.SelectionBox.Adornee = nil + script.RaycastResult.Value = nil + lastNormalId = nil + return + end + PlacementManager.SelectionBox.Adornee = parent + script.RaycastResult.Value = parent + lastNormalId = dir + return parent, dir +end + +function PlacementManager:RaycastGetResult() + return script.RaycastResult.Value +end + +local remotes = game:GetService("ReplicatedStorage"):WaitForChild("Remotes") +local placeRemote = remotes:WaitForChild("PlaceBlock") +local breakRemote = remotes:WaitForChild("BreakBlock") +local tickRemote = game:GetService("ReplicatedStorage").Tick + +-- FIRES REMOTE +function PlacementManager:PlaceBlock(cx, cy, cz, x, y, z, blockId: string) + --print("placeblock") + --local chunk = ChunkManager:GetChunk(cx, cy, cz) + --chunk:CreateBlock(x, y, z, blockData) + placeRemote:FireServer(cx, cy, cz, x, y, z, blockId) +end + +-- FIRES REMOTE +function PlacementManager:BreakBlock(cx, cy, cz, x, y, z) + --print("breakblock") + --local chunk = ChunkManager:GetChunk(cx, cy, cz) + --chunk:RemoveBlock(x, y, z) + breakRemote:FireServer(cx, cy, cz, x, y, z) +end + +-- CLIENTSIDED +function PlacementManager:PlaceBlockLocal(cx, cy, cz, x, y, z, blockData) + local chunk = ChunkManager:GetChunk(cx, cy, cz) + chunk:CreateBlock(x, y, z, blockData) +end + +-- CLIENTSIDED +function PlacementManager:BreakBlockLocal(cx, cy, cz, x, y, z) + local chunk = ChunkManager:GetChunk(cx, cy, cz) + chunk:RemoveBlock(x, y, z) +end + +function PlacementManager:GetBlockAtMouse(): nil | {chunk:Vector3, block: Vector3} + local selectedPart = PlacementManager:RaycastGetResult() + --print(selectedPart and selectedPart:GetFullName() or nil) + if selectedPart == nil then + PlacementManager.SelectionBox.Adornee = nil + script.RaycastResult.Value = nil + lastNormalId = nil + return nil + end + if not selectedPart.Parent then + PlacementManager.SelectionBox.Adornee = nil + script.RaycastResult.Value = nil + lastNormalId = nil + return nil + end + local chunkCoords = Util.BlockPosStringToCoords(selectedPart.Parent.Name) + local blockCoords = Util.BlockPosStringToCoords(selectedPart.Name) + + return { + chunk = chunkCoords, + block = blockCoords + } + +end + +function PlacementManager:GetTargetAtMouse(): nil | {chunk:Vector3, block: Vector3, normal: Enum.NormalId} + local hit = PlacementManager:GetBlockAtMouse() + if not hit or not lastNormalId then + return nil + end + + return { + chunk = hit.chunk, + block = hit.block, + normal = lastNormalId + } +end + +function PlacementManager:GetPlacementAtMouse(): nil | {chunk:Vector3, block: Vector3} + local hit = PlacementManager:GetTargetAtMouse() + if not hit then + return nil + end + local offset = normalIdToOffset(hit.normal) + local placeChunk, placeBlock = offsetChunkBlock(hit.chunk, hit.block, offset) + return { + chunk = placeChunk, + block = placeBlock + } +end + +function PlacementManager:Init() + game:GetService("RunService").Heartbeat:Connect(function() + local a,b = pcall(function() + PlacementManager:Raycast() + end) + if not a then + task.synchronize() + PlacementManager.SelectionBox.Adornee = nil + script.RaycastResult.Value = nil + task.desynchronize() + end + end) + tickRemote.OnClientEvent:Connect(function(m, cx, cy, cz, x, y, z, d) + --warn("PROPOGATED TICK", m, cx, cy, cz, x, y, z, d) + if m == "B_C" then + PlacementManager:PlaceBlockLocal(cx, cy, cz, x, y ,z, d) + end + if m == "B_D" then + PlacementManager:BreakBlockLocal(cx, cy, cz, x, y ,z) + end + end) +end + +return PlacementManager diff --git a/src/ReplicatedStorage/Shared/PlacementManager.meta.json b/src/ReplicatedStorage/Shared/PlacementManager.meta.json new file mode 100644 index 0000000..1025b06 --- /dev/null +++ b/src/ReplicatedStorage/Shared/PlacementManager.meta.json @@ -0,0 +1,3 @@ +{ + "ignoreUnknownInstances": true +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Shared/Util.lua b/src/ReplicatedStorage/Shared/Util.lua new file mode 100644 index 0000000..15b9fab --- /dev/null +++ b/src/ReplicatedStorage/Shared/Util.lua @@ -0,0 +1,48 @@ +local module = {} + +function module.BlockPosStringToCoords(s: string): Vector3 + -- a,b,c + local split = string.split(s,",") + return Vector3.new(tonumber(split[1]), tonumber(split[2]), tonumber(split[3])) +end + +function module.RotationStringToNormalId(s: string): Enum.NormalId + if not s then return Enum.NormalId.Front end + if s == "f" then + return Enum.NormalId.Front + end + if s == "b" then + return Enum.NormalId.Back + end + if s == "l" then + return Enum.NormalId.Left + end + if s == "r" then + return Enum.NormalId.Right + end + if s == "t" then + return Enum.NormalId.Top + end + if s == "b" then + return Enum.NormalId.Bottom + end + warn("Could not convert",s,"to Enum.NormalId") + return Enum.NormalId.Front +end + +-- chunk size 8x8x8 relative block indexs 1-8 +function module.GlobalBlockPosToRelative(x: number,y:number,z:number) + return x%8,y%8,z%8 +end + +function module.ChunkPosToCFrame(chunk: Vector3, block: Vector3): CFrame + return CFrame.new( + CFrame.new( + (32*chunk.X)+(block.X*4)-18, + (32*chunk.Y)+(block.Y*4)-18, + (32*chunk.Z)+(block.Z*4)-18 + ).Position + ) +end + +return module diff --git a/src/ReplicatedStorage/Shared/init.meta.json b/src/ReplicatedStorage/Shared/init.meta.json new file mode 100644 index 0000000..1025b06 --- /dev/null +++ b/src/ReplicatedStorage/Shared/init.meta.json @@ -0,0 +1,3 @@ +{ + "ignoreUnknownInstances": true +} \ No newline at end of file diff --git a/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/BitBuffer.lua b/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/BitBuffer.lua new file mode 100644 index 0000000..a854c6c --- /dev/null +++ b/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/BitBuffer.lua @@ -0,0 +1,1816 @@ +local CHAR_SET = [[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/]] + +-- Tradition is to use chars for the lookup table instead of codepoints. +-- But due to how we're running the encode function, it's faster to use codepoints. +local encode_char_set = {} +local decode_char_set = {} +for i = 1, 64 do + encode_char_set[i - 1] = string.byte(CHAR_SET, i, i) + decode_char_set[string.byte(CHAR_SET, i, i)] = i - 1 +end + +-- stylua: ignore +local HEX_TO_BIN = { + ["0"] = "0000", ["1"] = "0001", ["2"] = "0010", ["3"] = "0011", + ["4"] = "0100", ["5"] = "0101", ["6"] = "0110", ["7"] = "0111", + ["8"] = "1000", ["9"] = "1001", ["a"] = "1010", ["b"] = "1011", + ["c"] = "1100", ["d"] = "1101", ["e"] = "1110", ["f"] = "1111" +} + +-- stylua: ignore +local NORMAL_ID_VECTORS = { -- [Enum.Value] = Vector3.fromNormalId(Enum) + [0] = Vector3.new(1, 0, 0), -- Enum.NormalId.Right + [1] = Vector3.new(0, 1, 0), -- Enum.NormalId.Top + [2] = Vector3.new(0, 0, 1), -- Enum.NormalId.Back + [3] = Vector3.new(-1, 0, 0), -- Enum.NormalId.Left + [4] = Vector3.new(0, -1, 0), -- Enum.NormalId.Bottom + [5] = Vector3.new(0, 0, -1) -- Enum.NormalId.Front +} + +local ONES_VECTOR = Vector3.new(1, 1, 1) + +local BOOL_TO_BIT = { [true] = 1, [false] = 0 } + +local CRC32_POLYNOMIAL = 0xedb88320 + +local crc32_poly_lookup = {} +for i = 0, 255 do + local crc = i + for _ = 1, 8 do + local mask = -bit32.band(crc, 1) + crc = bit32.bxor(bit32.rshift(crc, 1), bit32.band(CRC32_POLYNOMIAL, mask)) + end + crc32_poly_lookup[i] = crc +end + +local powers_of_2 = {} +for i = 0, 64 do + powers_of_2[i] = 2 ^ i +end + +local byte_to_hex = {} +for i = 0, 255 do + byte_to_hex[i] = string.format("%02x", i) +end + +local function bitBuffer(stream) + if stream ~= nil then + assert(type(stream) == "string", "argument to BitBuffer constructor must be either nil or a string") + end + + -- The bit buffer works by keeping an array of bytes, a 'final' byte, and how many bits are currently in that last byte + -- 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. + -- This byte is also stored seperately, so that table operations aren't needed to read or modify its value. + -- The byte array is called `bytes`. The last byte is stored in `lastByte`. The bit counter is stored in `bits`. + + local bits = 0 -- How many free floating bits there are. + local bytes = {} --! -- Array of bytes currently in the buffer + local lastByte = 0 -- The most recent byte in the buffer, made up of free floating bits + + local byteCount = 0 -- This variable keeps track of how many bytes there are total in the bit buffer. + local bitCount = 0 -- This variable keeps track of how many bits there are total in the bit buffer + + local pointer = 0 -- This variable keeps track of what bit the read functions start at + 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. + + if stream then + byteCount = #stream + bitCount = byteCount * 8 + + bytes = table.create(#stream) + + for i = 1, byteCount do + bytes[i] = string.byte(stream, i, i) + end + end + + local function dumpBinary() + -- This function is for debugging or analysis purposes. + -- It dumps the contents of the byte array and the remaining bits into a string of binary digits. + -- Thus, bytes [97, 101] with bits [1, 1, 0] would output "01100001 01100101 110" + local output = table.create(byteCount) --! + for i, v in ipairs(bytes) do + output[i] = string.gsub(byte_to_hex[v], "%x", HEX_TO_BIN) + end + if bits ~= 0 then + -- Because the last byte (where the free floating bits are stored) is in the byte array, it has to be overwritten. + output[byteCount] = string.sub(output[byteCount], 1, bits) + end + + return table.concat(output, " ") + end + + local function dumpStringOld() + -- This function is for accessing the total contents of the bitbuffer. + -- This function combines all the bytes, including the last byte, into a string of binary data. + -- Thus, bytes [97, 101] and bits [1, 1, 0] would become (in hex) "0x61 0x65 0x06" + + -- It's substantially faster to create several smaller strings before using table.concat. (well maybe it was, but it isn't now post 2022) + local output = table.create(math.ceil(byteCount / 4096)) --! + local c = 1 + for i = 1, byteCount, 4096 do -- groups of 4096 bytes is the point at which there are diminishing returns + output[c] = string.char(table.unpack(bytes, i, math.min(byteCount, i + 4095))) + c = c + 1 + end + + return table.concat(output, "") + end + + --Let lua be lua + local function dumpString() + return string.char(table.unpack(bytes)) + end + + + local function dumpHex() + -- This function is for getting the hex of the bitbuffer's contents, should that be desired + local output = table.create(byteCount) --! + for i, v in ipairs(bytes) do + output[i] = byte_to_hex[v] + end + + return table.concat(output, "") + end + + local function dumpBase64() + -- Base64 is a safe and easy way to convert binary data to be entirely printable + -- It works on the principle that groups of 3 bytes (24 bits) can evenly be divided into 4 groups of 6 + -- And 2^6 is a mere 64, far less than the number of printable characters. + -- If there are any missing bytes, `=` is added to the end as padding. + -- Base64 increases the size of its input by 33%. + local output = table.create(math.ceil(byteCount * 1.333)) --! + + local c = 1 + for i = 1, byteCount, 3 do + local b1, b2, b3 = bytes[i], bytes[i + 1], bytes[i + 2] + local packed = bit32.bor(bit32.lshift(b1, 16), bit32.lshift(b2 or 0, 8), b3 or 0) + + -- This can be done with bit32.extract (and/or bit32.lshift, bit32.band, bit32.rshift) + -- But bit masking and shifting is more eloquent in my opinion. + output[c] = encode_char_set[bit32.rshift(bit32.band(packed, 0xfc0000), 0x12)] + output[c + 1] = encode_char_set[bit32.rshift(bit32.band(packed, 0x3f000), 0xc)] + output[c + 2] = b2 and encode_char_set[bit32.rshift(bit32.band(packed, 0xfc0), 0x6)] or 0x3d -- 0x3d == "=" + output[c + 3] = b3 and encode_char_set[bit32.band(packed, 0x3f)] or 0x3d + + c = c + 4 + end + c = c - 1 -- c will always be 1 more than the length of `output` + + local realOutput = table.create(math.ceil(c / 0x1000)) --! + local k = 1 + for i = 1, c, 0x1000 do + realOutput[k] = string.char(table.unpack(output, i, math.min(c, i + 0xfff))) + k = k + 1 + end + + return table.concat(realOutput, "") + end + + local function exportChunk(chunkLength) + assert(type(chunkLength) == "number", "argument #1 to BitBuffer.exportChunk should be a number") + assert(chunkLength > 0, "argument #1 to BitBuffer.exportChunk should be above zero") + assert(chunkLength % 1 == 0, "argument #1 to BitBuffer.exportChunk should be an integer") + + -- Since `i` is being returned, the most eloquent way to handle this is with a coroutine + -- This allows returning the existing value of `i` without having to increment it first. + -- The alternative was starting at `i = -(chunkLength-1)` and incrementing at the start of the iterator function. + return coroutine.wrap(function() + local realChunkLength = chunkLength - 1 + -- Since this function only has one 'state', it's perfectly fine to use a for-loop. + for i = 1, byteCount, chunkLength do + local chunk = string.char(table.unpack(bytes, i, math.min(byteCount, i + realChunkLength))) + coroutine.yield(i, chunk) + end + end) + end + + local function exportBase64Chunk(chunkLength) + chunkLength = chunkLength or 76 + assert(type(chunkLength) == "number", "argument #1 to BitBuffer.exportBase64Chunk should be a number") + assert(chunkLength > 0, "argument #1 to BitBuffer.exportBase64Chunk should be above zero") + assert(chunkLength % 1 == 0, "argument #1 to BitBuffer.exportBase64Chunk should be an integer") + + local output = table.create(math.ceil(byteCount * 0.333)) --! + + local c = 1 + for i = 1, byteCount, 3 do + local b1, b2, b3 = bytes[i], bytes[i + 1], bytes[i + 2] + local packed = bit32.bor(bit32.lshift(b1, 16), bit32.lshift(b2 or 0, 8), b3 or 0) + + output[c] = encode_char_set[bit32.rshift(bit32.band(packed, 0xfc0000), 0x12)] + output[c + 1] = encode_char_set[bit32.rshift(bit32.band(packed, 0x3f000), 0xc)] + output[c + 2] = b2 and encode_char_set[bit32.rshift(bit32.band(packed, 0xfc0), 0x6)] or 0x3d + output[c + 3] = b3 and encode_char_set[bit32.band(packed, 0x3f)] or 0x3d + + c = c + 4 + end + c = c - 1 + + return coroutine.wrap(function() + local realChunkLength = chunkLength - 1 + for i = 1, c, chunkLength do + local chunk = string.char(table.unpack(output, i, math.min(c, i + realChunkLength))) + coroutine.yield(chunk) + end + end) + end + + local function exportHexChunk(chunkLength) + assert(type(chunkLength) == "number", "argument #1 to BitBuffer.exportHexChunk should be a number") + assert(chunkLength > 0, "argument #1 to BitBuffer.exportHexChunk should be above zero") + assert(chunkLength % 1 == 0, "argument #1 to BitBuffer.exportHexChunk should be an integer") + + local halfLength = math.floor(chunkLength / 2) + + if chunkLength % 2 == 0 then + return coroutine.wrap(function() + local output = {} --! + for i = 1, byteCount, halfLength do + for c = 0, halfLength - 1 do + output[c] = byte_to_hex[bytes[i + c]] + end + coroutine.yield(table.concat(output, "", 0)) + end + end) + else + return coroutine.wrap(function() + local output = { [0] = "" } --! + local remainder = "" + + local i = 1 + while i <= byteCount do + if remainder == "" then + output[0] = "" + for c = 0, halfLength - 1 do + output[c + 1] = byte_to_hex[bytes[i + c]] + end + local endByte = byte_to_hex[bytes[i + halfLength]] + if endByte then + output[halfLength + 1] = string.sub(endByte, 1, 1) + remainder = string.sub(endByte, 2, 2) + end + i = i + 1 + else + output[0] = remainder + for c = 0, halfLength - 1 do + output[c + 1] = byte_to_hex[bytes[i + c]] + end + output[halfLength + 1] = "" + remainder = "" + end + + coroutine.yield(table.concat(output, "", 0)) + i = i + halfLength + end + end) + end + end + + local function crc32() + local crc = 0xffffffff -- 2^32 + + for _, v in ipairs(bytes) do + local poly = crc32_poly_lookup[bit32.band(bit32.bxor(crc, v), 255)] + crc = bit32.bxor(bit32.rshift(crc, 8), poly) + end + + return bit32.bnot(crc) % 0xffffffff -- 2^32 + end + + local function getLength() + return bitCount + end + + local function getByteLength() + return byteCount + end + + local function getPointer() + -- This function gets the value of the pointer. This is self-explanatory. + return pointer + end + + local function setPointer(n) + assert(type(n) == "number", "argument #1 to BitBuffer.setPointer should be a number") + assert(n >= 0, "argument #1 to BitBuffer.setPointer should be zero or higher") + assert(n % 1 == 0, "argument #1 to BitBuffer.setPointer should be an integer") + assert(n <= bitCount, "argument #1 to BitBuffer.setPointerByte should within range of the buffer") + -- This function sets the value of pointer. This is self-explanatory. + pointer = n + pointerByte = math.floor(n / 8) + 1 + end + + local function setPointerFromEnd(n) + assert(type(n) == "number", "argument #1 to BitBuffer.setPointerFromEnd should be a number") + assert(n >= 0, "argument #1 to BitBuffer.setPointerFromEnd should be zero or higher") + assert(n % 1 == 0, "argument #1 to BitBuffer.setPointerFromEnd should be an integer") + assert(n <= bitCount, "argument #1 to BitBuffer.setPointerFromEnd should within range of the buffer") + + pointer = bitCount - n + pointerByte = math.floor(pointer / 8 + 1) + end + + local function getPointerByte() + return pointerByte + end + + local function setPointerByte(n) + assert(type(n) == "number", "argument #1 to BitBuffer.setPointerByte should be a number") + assert(n > 0, "argument #1 to BitBuffer.setPointerByte should be positive") + assert(n % 1 == 0, "argument #1 to BitBuffer.setPointerByte should be an integer") + assert(n <= byteCount, "argument #1 to BitBuffer.setPointerByte should be within range of the buffer") + -- Sets the value of the pointer in bytes instead of bits + pointer = n * 8 + pointerByte = n + end + + local function setPointerByteFromEnd(n) + assert(type(n) == "number", "argument #1 to BitBuffer.setPointerByteFromEnd should be a number") + assert(n >= 0, "argument #1 to BitBuffer.setPointerByteFromEnd should be zero or higher") + assert(n % 1 == 0, "argument #1 to BitBuffer.setPointerByteFromEnd should be an integer") + assert(n <= byteCount, "argument #1 to BitBuffer.setPointerByteFromEnd should be within range of the buffer") + + pointerByte = byteCount - n + pointer = pointerByte * 8 + end + + local function isFinished() + return pointer == bitCount + end + + local function writeBits(...) + -- The first of two main functions for the actual 'writing' of the bitbuffer. + -- This function takes a vararg of 1s and 0s and writes them to the buffer. + local bitN = select("#", ...) + if bitN == 0 then + return + end -- Throwing here seems unnecessary + bitCount = bitCount + bitN + local packed = table.pack(...) + for _, v in ipairs(packed) do + assert(v == 1 or v == 0, "arguments to BitBuffer.writeBits should be either 1 or 0") + if bits == 0 then -- If the bit count is 0, increment the byteCount + -- This is the case at the beginning of the buffer as well as when the the buffer reaches 7 bits, + -- so it's done at the beginning of the loop. + byteCount = byteCount + 1 + end + lastByte = lastByte + (v == 1 and powers_of_2[7 - bits] or 0) -- Add the current bit to lastByte, from right to left + bits = bits + 1 + 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 + bits = 0 + bytes[byteCount] = lastByte + lastByte = 0 + end + end + if bits ~= 0 then -- If there are some bits in lastByte, it has to be put into lastByte + -- If this is done regardless of the bit count, there might be a trailing zero byte + bytes[byteCount] = lastByte + end + end + + local function writeByte(n) + --assert(type(n) == "number", "argument #1 to BitBuffer.writeByte should be a number") + --assert(n >= 0 and n <= 255, "argument #1 to BitBuffer.writeByte should be in the range [0, 255]") + --assert(n % 1 == 0, "argument #1 to BitBuffer.writeByte should be an integer") + + -- The second of two main functions for the actual 'writing' of the bitbuffer. + -- This function takes a byte (an 8-bit integer) and writes it to the buffer. + if bits == 0 then + -- If there aren't any free-floating bits, this is easy. + byteCount = byteCount + 1 + bytes[byteCount] = n + else + local nibble = bit32.rshift(n, bits) -- Shift `bits` number of bits out of `n` (they go into the aether) + bytes[byteCount] = lastByte + nibble -- Manually set the most recent byte to the lastByte + the front part of `n` + byteCount = byteCount + 1 + lastByte = bit32.band(bit32.lshift(n, 8 - bits), 255) -- Shift `n` forward `8-bits` and get what remains in the first 8 bits + bytes[byteCount] = lastByte + end + bitCount = bitCount + 8 -- Increment the bit counter + end + + local function writeBytesFast(tab) + assert(bits == 0, "writeBytesFast can only work for whole byte streams") + local count = #tab + table.move(tab, 1 , count, byteCount + 1, bytes) + byteCount+= count + bitCount += count * 8 + end + + local function writeUnsigned(width, n) + assert(type(width) == "number", "argument #1 to BitBuffer.writeUnsigned should be a number") + assert(width >= 1 and width <= 64, "argument #1 to BitBuffer.writeUnsigned should be in the range [1, 64]") + assert(width % 1 == 0, "argument #1 to BitBuffer.writeUnsigned should be an integer") + + assert(type(n) == "number", "argument #2 to BitBuffer.writeUnsigned should be a number") + assert(n >= 0 and n <= powers_of_2[width] - 1, "argument #2 to BitBuffer.writeUnsigned is out of range") + assert(n % 1 == 0, "argument #2 to BitBuffer.writeUnsigned should be an integer") + -- Writes unsigned integers of arbitrary length to the buffer. + -- This is the first function that uses other functions in the buffer to function. + -- This is done because the space taken up would be rather large for very little performance gain. + + -- Get the number of bytes and number of floating bits in the specified width + local bytesInN, bitsInN = math.floor(width / 8), width % 8 + local extractedBits = table.create(bitsInN) --! + + -- If the width is less than or equal to 32-bits, bit32 can be used without any problem. + if width <= 32 then + -- Counting down from the left side, the bytes are written to the buffer + local c = width + for _ = 1, bytesInN do + c = c - 8 + writeByte(bit32.extract(n, c, 8)) + end + -- Any remaining bits are stored in an array + for i = bitsInN - 1, 0, -1 do + extractedBits[bitsInN - i] = BOOL_TO_BIT[bit32.btest(n, powers_of_2[i])] + end + -- Said array is then used to write them to the buffer + writeBits(table.unpack(extractedBits)) + else + -- If the width is greater than 32, the number has to be divided up into a few 32-bit or less numbers + local leastSignificantChunk = n % 0x100000000 -- Get bits 0-31 (counting from the right side). 0x100000000 is 2^32. + local mostSignificantChunk = math.floor(n / 0x100000000) -- Get any remaining bits by manually right shifting by 32 bits + + local c = width - 32 -- The number of bits in mostSignificantChunk is variable, but a counter is still needed + for _ = 1, bytesInN - 4 do -- 32 bits is 4 bytes + c = c - 8 + writeByte(bit32.extract(mostSignificantChunk, c, 8)) + end + -- `bitsInN` is always going to be the number of spare bits in `mostSignificantChunk` + -- which comes before `leastSignificantChunk` + for i = bitsInN - 1, 0, -1 do + extractedBits[bitsInN - i] = BOOL_TO_BIT[bit32.btest(mostSignificantChunk, powers_of_2[i])] + end + writeBits(table.unpack(extractedBits)) + + for i = 3, 0, -1 do -- Then of course, write all 4 bytes of leastSignificantChunk + writeByte(bit32.extract(leastSignificantChunk, i * 8, 8)) + end + end + end + + local function writeSigned(width, n) + assert(type(width) == "number", "argument #1 to BitBuffer.writeSigned should be a number") + assert(width >= 2 and width <= 64, "argument #1 to BitBuffer.writeSigned should be in the range [2, 64]") + assert(width % 1 == 0, "argument #1 to BitBuffer.writeSigned should be an integer") + + assert(type(n) == "number", "argument #2 to BitBuffer.writeSigned should be a number") + assert( + n >= -powers_of_2[width - 1] and n <= powers_of_2[width - 1] - 1, + "argument #2 to BitBuffer.writeSigned is out of range" + ) + assert(n % 1 == 0, "argument #2 to BitBuffer.writeSigned should be an integer") + -- Writes signed integers of arbitrary length to the buffer. + -- These integers are stored using two's complement. + -- Essentially, this means the first bit in the number is used to store whether it's positive or negative + -- If the number is positive, it's stored normally. + -- If it's negative, the number that's stored is equivalent to the max value of the width + the number + if n >= 0 then + writeBits(0) + writeUnsigned(width - 1, n) -- One bit is used for the sign, so the stored number's width is actually width-1 + else + writeBits(1) + writeUnsigned(width - 1, powers_of_2[width - 1] + n) + end + end + + local function writeFloat(exponentWidth, mantissaWidth, n) + assert(type(exponentWidth) == "number", "argument #1 to BitBuffer.writeFloat should be a number") + assert( + exponentWidth >= 1 and exponentWidth <= 64, + "argument #1 to BitBuffer.writeFloat should be in the range [1, 64]" + ) + assert(exponentWidth % 1 == 0, "argument #1 to BitBuffer.writeFloat should be an integer") + + assert(type(mantissaWidth) == "number", "argument #2 to BitBuffer.writeFloat should be a number") + assert( + mantissaWidth >= 1 and mantissaWidth <= 64, + "argument #2 to BitBuffer.writeFloat should be in the range [1, 64]" + ) + assert(mantissaWidth % 1 == 0, "argument #2 to BitBuffer.writeFloat should be an integer") + + assert(type(n) == "number", "argument #3 to BitBuffer.writeFloat should be a number") + + -- Given that floating point numbers are particularly hard to grasp, this function is annotated heavily. + -- This stackoverflow answer is a great help if you just want an overview: + -- https://stackoverflow.com/a/7645264 + -- Essentially, floating point numbers are scientific notation in binary. + -- Instead of expressing numbers like 10^e*m, floating points instead use 2^e*m. + -- For the sake of this function, `e` is referred to as `exponent` and `m` is referred to as `mantissa`. + + -- Floating point numbers are stored in memory as a sequence of bitfields. + -- Every float has a set number of bits assigned for exponent values and mantissa values, along with one bit for the sign. + -- The order of the bits in the memory is: sign, exponent, mantissa. + + -- Given that floating points have to represent numbers less than zero as well as those above them, + -- some parts of the exponent are set aside to be negative exponents. In the case of floats, + -- this is about half of the values. To calculate the 'real' value of an exponent a number that's half of the max exponent + -- is added to the exponent. More info can be found here: https://stackoverflow.com/q/2835278 + -- This number is called the 'bias'. + local bias = powers_of_2[exponentWidth - 1] - 1 + + local sign = n < 0 -- The sign of a number is important. + -- 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. + n = math.abs(n) -- But it's annoying to work with negative numbers and the sign isn't important for decomposition. + + -- Lua has a function specifically for decomposing (or taking apart) a floating point number into its pieces. + -- These pieces, as listed above, are the mantissa and exponent. + local mantissa, exponent = math.frexp(n) + + -- Before we go further, there are some concepts that get special treatment in the floating point format. + -- These have to be accounted for before normal floats are written to the buffer. + + if n == math.huge then + -- Positive and negative infinities are specifically indicated with an exponent that's all 1s + -- and a mantissa that's all 0s. + writeBits(BOOL_TO_BIT[sign]) -- As previously said, there's a bit for the sign + writeUnsigned(exponentWidth, powers_of_2[exponentWidth] - 1) -- Then comes the exponent + writeUnsigned(mantissaWidth, 0) -- And finally the mantissa + return + elseif n ~= n then + -- NaN is indicated with an exponent that's all 1s and a mantissa that isn't 0. + -- In theory, the individual bits of NaN should be maintained but Lua doesn't allow that, + -- so the mantissa is just being set to 10 for no particular reason. + writeBits(BOOL_TO_BIT[sign]) + writeUnsigned(exponentWidth, powers_of_2[exponentWidth] - 1) + writeUnsigned(mantissaWidth, 10) + return + elseif n == 0 then + -- Zero is represented with an exponent that's zero and a mantissa that's also zero. + -- Lua doesn't have a signed zero, so that translates to the entire number being all 0s. + writeUnsigned(exponentWidth + mantissaWidth + 1, 0) + return + elseif exponent + bias <= 1 then + -- Subnormal numbers are a number that's exponent (when biased) is zero. + -- Because of a quirk with the way Lua and C decompose numbers, subnormal numbers actually have an exponent of one when biased. + + -- The process behind this is explained below, so for the sake of brevity it isn't explained here. + -- The only difference between processing subnormal and normal numbers is with the mantissa. + -- As subnormal numbers always start with a 0 (in binary), it doesn't need to be removed or shifted out + -- so it's a simple shift and round. + mantissa = math.floor(mantissa * powers_of_2[mantissaWidth] + 0.5) + + writeBits(BOOL_TO_BIT[sign]) + writeUnsigned(exponentWidth, 0) -- Subnormal numbers always have zero for an exponent + writeUnsigned(mantissaWidth, mantissa) + return + end + + -- In every normal case, the mantissa of a number will have a 1 directly after the decimal point (in binary). + -- 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. + -- That means that for the sake of space efficiency that can be left out. + -- The bit has to be removed. This uses subtraction and multiplication to do it since bit32 is for integers only. + -- The mantissa is then shifted up by the width of the mantissa field and rounded. + mantissa = math.floor((mantissa - 0.5) * 2 * powers_of_2[mantissaWidth] + 0.5) + -- (The first fraction bit is equivalent to 0.5 in decimal) + + -- After that, it's just a matter of writing to the stream: + writeBits(BOOL_TO_BIT[sign]) + writeUnsigned(exponentWidth, exponent + bias - 1) -- The bias is added to the exponent to properly offset it + -- The extra -1 is added because Lua, for whatever reason, doesn't normalize its results + -- This is the cause of the 'quirk' mentioned when handling subnormal number + -- As an example, math.frexp(0.15625) = 0.625, -2 + -- This means that 0.15625 = 0.625*2^-2 + -- Or, in binary: 0.00101 = 0.101 >> 2 + -- This is a correct statement but the actual result is meant to be: + -- 0.00101 = 1.01 >> 3, or 0.15625 = 1.25*2^-3 + -- A small but important distinction that has made writing this module frustrating because no documentation notates this. + writeUnsigned(mantissaWidth, mantissa) + end + + local function writeBase64(input) + assert(type(input) == "string", "argument #1 to BitBuffer.writeBase64 should be a string") + assert( + not string.find(input, "[^%w%+/=]"), + "argument #1 to BitBuffer.writeBase64 should only contain valid base64 characters" + ) + + for i = 1, #input, 4 do + local b1, b2, b3, b4 = string.byte(input, i, i + 3) + + b1 = decode_char_set[b1] + b2 = decode_char_set[b2] + b3 = decode_char_set[b3] + b4 = decode_char_set[b4] + + local packed = bit32.bor(bit32.lshift(b1, 18), bit32.lshift(b2, 12), bit32.lshift(b3 or 0, 6), b4 or 0) + + writeByte(bit32.rshift(packed, 16)) + if not b3 then + break + end + writeByte(bit32.band(bit32.rshift(packed, 8), 0xff)) + if not b4 then + break + end + writeByte(bit32.band(packed, 0xff)) + end + end + + local function writeString(str) + assert(type(str) == "string", "argument #1 to BitBuffer.writeString should be a string") + -- The default mode of writing strings is length-prefixed. + -- This means that the length of the string is written before the contents of the string. + -- For the sake of speed it has to be an even byte. + -- One and two bytes is too few characters (255 bytes and 65535 bytes respectively), so it has to be higher. + -- Three bytes is roughly 16.77mb, and four is roughly 4.295gb. Given this is Lua and is thus unlikely to be processing strings + -- that large, this function uses three bytes, or 24 bits for the length + + writeUnsigned(24, #str) + + for i = 1, #str do + writeByte(string.byte(str, i, i)) + end + end + + local function writeTerminatedString(str) + assert(type(str) == "string", "argument #1 to BitBuffer.writeTerminatedString should be a string") + -- This function writes strings that are null-terminated. + -- Null-terminated strings are strings of bytes that end in a 0 byte (\0) + -- This isn't the default because it doesn't allow for binary data to be written cleanly. + + for i = 1, #str do + writeByte(string.byte(str, i, i)) + end + writeByte(0) + end + + local function writeSetLengthString(str) + assert(type(str) == "string", "argument #1 to BitBuffer.writeSetLengthString should be a string") + -- This function writes strings as a pure string of bytes + -- It doesn't store any data about the length of the string, + -- so reading it requires knowledge of how many characters were stored + + for i = 1, #str do + writeByte(string.byte(str, i, i)) + end + end + + local function writeField(...) + -- This is equivalent to having a writeBitfield function. + -- It combines all of the passed 'bits' into an unsigned number, then writes it. + local field = 0 + local bools = table.pack(...) + for i = 1, bools.n do + field = field * 2 -- Shift `field`. Equivalent to field<<1. At the beginning of the loop to avoid an extra shift. + + local v = bools[i] + if v then + field = field + 1 -- If the bit is truthy, turn it on (it defaults to off so it's fine to not have a branch) + end + end + + writeUnsigned(bools.n, field) + end + + -- All write functions below here are shorthands. For the sake of performance, these functions are implemented manually. + -- As an example, while it would certainly be easier to make `writeInt16(n)` just call `writeUnsigned(16, n), + -- it's more performant to just manually call writeByte twice for it. + + local function writeUInt8(n) + assert(type(n) == "number", "argument #1 to BitBuffer.writeUInt8 should be a number") + assert(n >= 0 and n <= 255, "argument #1 to BitBuffer.writeUInt8 should be in the range [0, 255]") + assert(n % 1 == 0, "argument #1 to BitBuffer.writeUInt8 should be an integer") + + writeByte(n) + end + + local function writeUInt16(n) + assert(type(n) == "number", "argument #1 to BitBuffer.writeUInt16 should be a number") + assert(n >= 0 and n <= 65535, "argument #1 to BitBuffer.writeInt16 should be in the range [0, 65535]") + assert(n % 1 == 0, "argument #1 to BitBuffer.writeUInt16 should be an integer") + + writeByte(bit32.rshift(n, 8)) + writeByte(bit32.band(n, 255)) + end + + local function writeUInt32(n) + assert(type(n) == "number", "argument #1 to BitBuffer.writeUInt32 should be a number") + assert( + n >= 0 and n <= 4294967295, + "argument #1 to BitBuffer.writeUInt32 should be in the range [0, 4294967295]" + ) + assert(n % 1 == 0, "argument #1 to BitBuffer.writeUInt32 should be an integer") + + writeByte(bit32.rshift(n, 24)) + writeByte(bit32.band(bit32.rshift(n, 16), 255)) + writeByte(bit32.band(bit32.rshift(n, 8), 255)) + writeByte(bit32.band(n, 255)) + end + + local function writeInt8(n) + assert(type(n) == "number", "argument #1 to BitBuffer.writeInt8 should be a number") + assert(n >= -128 and n <= 127, "argument #1 to BitBuffer.writeInt8 should be in the range [-128, 127]") + assert(n % 1 == 0, "argument #1 to BitBuffer.writeInt8 should be an integer") + + if n < 0 then + n = (128 + n) + 128 + end + + writeByte(n) + end + + local function writeInt16(n) + assert(type(n) == "number", "argument #1 to BitBuffer.writeInt16 should be a number") + assert(n >= -32768 and n <= 32767, "argument #1 to BitBuffer.writeInt16 should be in the range [-32768, 32767]") + assert(n % 1 == 0, "argument #1 to BitBuffer.writeInt16 should be an integer") + + if n < 0 then + n = (32768 + n) + 32768 + end + + writeByte(bit32.rshift(n, 8)) + writeByte(bit32.band(n, 255)) + end + + local function writeInt32(n) + assert(type(n) == "number", "argument #1 to BitBuffer.writeInt32 should be a number") + assert( + n >= -2147483648 and n <= 2147483647, + "argument #1 to BitBuffer.writeInt32 should be in the range [-2147483648, 2147483647]" + ) + assert(n % 1 == 0, "argument #1 to BitBuffer.writeInt32 should be an integer") + + if n < 0 then + n = (2147483648 + n) + 2147483648 + end + + writeByte(bit32.rshift(n, 24)) + writeByte(bit32.band(bit32.rshift(n, 16), 255)) + writeByte(bit32.band(bit32.rshift(n, 8), 255)) + writeByte(bit32.band(n, 255)) + end + + local function writeFloat16(n) + --assert(type(n) == "number", "argument #1 to BitBuffer.writeFloat16 should be a number") + + local sign = n < 0 + n = math.abs(n) + + local mantissa, exponent = math.frexp(n) + + if n == math.huge then + if sign then + writeByte(252) -- 11111100 + else + writeByte(124) -- 01111100 + end + writeByte(0) -- 00000000 + return + elseif n ~= n then + -- 01111111 11111111 + writeByte(127) + writeByte(255) + return + elseif n == 0 then + writeByte(0) + writeByte(0) + return + elseif exponent + 15 <= 1 then -- Bias for halfs is 15 + mantissa = math.floor(mantissa * 1024 + 0.5) + if sign then + writeByte(128 + bit32.rshift(mantissa, 8)) -- Sign bit, 5 empty bits, 2 from mantissa + else + writeByte(bit32.rshift(mantissa, 8)) + end + writeByte(bit32.band(mantissa, 255)) -- Get last 8 bits from mantissa + return + end + + mantissa = math.floor((mantissa - 0.5) * 2048 + 0.5) + + -- The bias for halfs is 15, 15-1 is 14 + if sign then + writeByte(128 + bit32.lshift(exponent + 14, 2) + bit32.rshift(mantissa, 8)) + else + writeByte(bit32.lshift(exponent + 14, 2) + bit32.rshift(mantissa, 8)) + end + writeByte(bit32.band(mantissa, 255)) + end + + local function writeFloat32(n) + --assert(type(n) == "number", "argument #1 to BitBuffer.writeFloat32 should be a number") + + local sign = n < 0 + n = math.abs(n) + + local mantissa, exponent = math.frexp(n) + + if n == math.huge then + if sign then + writeByte(255) -- 11111111 + else + writeByte(127) -- 01111111 + end + writeByte(128) -- 10000000 + writeByte(0) -- 00000000 + writeByte(0) -- 00000000 + return + elseif n ~= n then + -- 01111111 11111111 11111111 11111111 + writeByte(127) + writeByte(255) + writeByte(255) + writeByte(255) + return + elseif n == 0 then + writeByte(0) + writeByte(0) + writeByte(0) + writeByte(0) + return + elseif exponent + 127 <= 1 then -- bias for singles is 127 + mantissa = math.floor(mantissa * 8388608 + 0.5) + if sign then + writeByte(128) -- Sign bit, 7 empty bits for exponent + else + writeByte(0) + end + writeByte(bit32.rshift(mantissa, 16)) + writeByte(bit32.band(bit32.rshift(mantissa, 8), 255)) + writeByte(bit32.band(mantissa, 255)) + return + end + + mantissa = math.floor((mantissa - 0.5) * 16777216 + 0.5) + + -- 127-1 = 126 + if sign then -- sign + 7 exponent + writeByte(128 + bit32.rshift(exponent + 126, 1)) + else + writeByte(bit32.rshift(exponent + 126, 1)) + end + writeByte(bit32.band(bit32.lshift(exponent + 126, 7), 255) + bit32.rshift(mantissa, 16)) -- 1 exponent + 7 mantissa + writeByte(bit32.band(bit32.rshift(mantissa, 8), 255)) -- 8 mantissa + writeByte(bit32.band(mantissa, 255)) -- 8 mantissa + end + + local function writeFloat64(n) + assert(type(n) == "number", "argument #1 to BitBuffer.writeFloat64 should be a number") + + local sign = n < 0 + n = math.abs(n) + + local mantissa, exponent = math.frexp(n) + + if n == math.huge then + if sign then + writeByte(255) -- 11111111 + else + writeByte(127) -- 01111111 + end + writeByte(240) -- 11110000 + writeByte(0) -- 00000000 + writeByte(0) -- 00000000 + writeByte(0) -- 00000000 + writeByte(0) -- 00000000 + writeByte(0) -- 00000000 + writeByte(0) -- 00000000 + return + elseif n ~= n then + -- 01111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 + writeByte(127) + writeByte(255) + writeByte(255) + writeByte(255) + writeByte(255) + writeByte(255) + writeByte(255) + writeByte(255) + return + elseif n == 0 then + writeByte(0) + return + elseif exponent + 1023 <= 1 then -- bias for doubles is 1023 + mantissa = math.floor(mantissa * 4503599627370496 + 0.5) + if sign then + writeByte(128) -- Sign bit, 7 empty bits for exponent + else + writeByte(0) + end + + -- This is labeled better below + local leastSignificantChunk = mantissa % 0x100000000 -- 32 bits + local mostSignificantChunk = math.floor(mantissa / 0x100000000) -- 20 bits + + writeByte(bit32.rshift(mostSignificantChunk, 16)) + writeByte(bit32.band(bit32.rshift(mostSignificantChunk, 8), 255)) + writeByte(bit32.band(mostSignificantChunk, 255)) + writeByte(bit32.rshift(leastSignificantChunk, 24)) + writeByte(bit32.band(bit32.rshift(leastSignificantChunk, 16), 255)) + writeByte(bit32.band(bit32.rshift(leastSignificantChunk, 8), 255)) + writeByte(bit32.band(leastSignificantChunk, 255)) + return + end + + mantissa = math.floor((mantissa - 0.5) * 9007199254740992 + 0.5) + + --1023-1 = 1022 + if sign then + writeByte(128 + bit32.rshift(exponent + 1022, 4)) -- shift out 4 of the bits in exponent + else + writeByte(bit32.rshift(exponent + 1022, 4)) -- 01000001 0110 + end + -- Things start to get a bit wack here because the mantissa is 52 bits, so bit32 *can't* be used. + -- As the Offspring once said... You gotta keep 'em seperated. + local leastSignificantChunk = mantissa % 0x100000000 -- 32 bits + local mostSignificantChunk = math.floor(mantissa / 0x100000000) -- 20 bits + + -- First, the last 4 bits of the exponent and the first 4 bits of the mostSignificantChunk: + writeByte(bit32.band(bit32.lshift(exponent + 1022, 4), 255) + bit32.rshift(mostSignificantChunk, 16)) + -- Then, the next 16 bits: + writeByte(bit32.band(bit32.rshift(mostSignificantChunk, 8), 255)) + writeByte(bit32.band(mostSignificantChunk, 255)) + -- Then... 4 bytes of the leastSignificantChunk + writeByte(bit32.rshift(leastSignificantChunk, 24)) + writeByte(bit32.band(bit32.rshift(leastSignificantChunk, 16), 255)) + writeByte(bit32.band(bit32.rshift(leastSignificantChunk, 8), 255)) + writeByte(bit32.band(leastSignificantChunk, 255)) + end + + -- All write functions below here are Roblox specific datatypes. + + local function writeBrickColor(n) + assert(typeof(n) == "BrickColor", "argument #1 to BitBuffer.writeBrickColor should be a BrickColor") + + writeUInt16(n.Number) + end + + local function writeColor3(c3) + assert(typeof(c3) == "Color3", "argument #1 to BitBuffer.writeColor3 should be a Color3") + + writeByte(math.floor(c3.R * 0xff + 0.5)) + writeByte(math.floor(c3.G * 0xff + 0.5)) + writeByte(math.floor(c3.B * 0xff + 0.5)) + end + + local function writeCFrame(cf) + assert(typeof(cf) == "CFrame", "argument #1 to BitBuffer.writeCFrame should be a CFrame") + -- CFrames can be rather lengthy (if stored naively, they would each be 48 bytes long) so some optimization is done here. + -- Specifically, if a CFrame is axis-aligned (it's only rotated in 90 degree increments), the rotation matrix isn't stored. + -- Instead, an 'id' for its orientation is generated and that's stored instead of the rotation. + -- This means that for the most common rotations, only 13 bytes are used. + -- The downside is that non-axis-aligned CFrames use 49 bytes instead of 48, but that's a small price to pay. + + local upVector = cf.UpVector + local rightVector = cf.RightVector + + -- This is an easy trick to check if a CFrame is axis-aligned: + -- Essentially, in order for a vector to be axis-aligned, two of the components have to be 0 + -- This means that the dot product between the vector and a vector of all 1s will be 1 (0*x = 0) + -- Since these are all unit vectors, there is no other combination that results in 1. + local rightAligned = math.abs(rightVector:Dot(ONES_VECTOR)) + local upAligned = math.abs(upVector:Dot(ONES_VECTOR)) + -- At least one of these two vectors is guaranteed to not result in 0. + + local axisAligned = (math.abs(1 - rightAligned) < 0.00001 or rightAligned == 0) + and (math.abs(1 - upAligned) < 0.00001 or upAligned == 0) + -- There are limitations to `math.abs(a-b) < epsilon` but they're not relevant: + -- The range of numbers is [0, 1] and this just needs to know if the number is approximately 1 + + --todo special code for quaternions (0x01 in Roblox's format, would clash with 0x00 here) + if axisAligned then + local position = cf.Position + -- The ID of an orientation is generated through what can best be described as 'hand waving'; + -- This is how Roblox does it and it works, so it was chosen to do it this way too. + local rightNormal, upNormal + for i = 0, 5 do + local v = NORMAL_ID_VECTORS[i] + if 1 - v:Dot(rightVector) < 0.00001 then + rightNormal = i + end + if 1 - v:Dot(upVector) < 0.00001 then + upNormal = i + end + end + -- The ID generated here is technically off by 1 from what Roblox would store, but that's not important + -- It just means that 0x02 is actually 0x01 for the purposes of this module's implementation. + writeByte(rightNormal * 6 + upNormal) + writeFloat32(position.X) + writeFloat32(position.Y) + writeFloat32(position.Z) + else + -- If the CFrame isn't axis-aligned, the entire rotation matrix has to be written... + writeByte(0) -- Along with a byte to indicate the matrix was written. + local x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22 = cf:GetComponents() + writeFloat32(x) + writeFloat32(y) + writeFloat32(z) + writeFloat32(r00) + writeFloat32(r01) + writeFloat32(r02) + writeFloat32(r10) + writeFloat32(r11) + writeFloat32(r12) + writeFloat32(r20) + writeFloat32(r21) + writeFloat32(r22) + end + end + + local function writeVector3(v3) + --assert(typeof(v3) == "Vector3", "argument #1 to BitBuffer.writeVector3 should be a Vector3") + + writeFloat32(v3.X) + writeFloat32(v3.Y) + writeFloat32(v3.Z) + end + + local function writeVector2(v2) + assert(typeof(v2) == "Vector2", "argument #1 to BitBuffer.writeVector2 should be a Vector2") + + writeFloat32(v2.X) + writeFloat32(v2.Y) + end + + local function writeUDim2(u2) + assert(typeof(u2) == "UDim2", "argument #1 to BitBuffer.writeUDim2 should be a UDim2") + + writeFloat32(u2.X.Scale) + writeInt32(u2.X.Offset) + writeFloat32(u2.Y.Scale) + writeInt32(u2.Y.Offset) + end + + local function writeUDim(u) + assert(typeof(u) == "UDim", "argument #1 to BitBuffer.writeUDim should be a UDim") + + writeFloat32(u.Scale) + writeInt32(u.Offset) + end + + local function writeRay(ray) + assert(typeof(ray) == "Ray", "argument #1 to BitBuffer.writeRay should be a Ray") + + writeFloat32(ray.Origin.X) + writeFloat32(ray.Origin.Y) + writeFloat32(ray.Origin.Z) + + writeFloat32(ray.Direction.X) + writeFloat32(ray.Direction.Y) + writeFloat32(ray.Direction.Z) + end + + local function writeRect(rect) + assert(typeof(rect) == "Rect", "argument #1 to BitBuffer.writeRect should be a Rect") + + writeFloat32(rect.Min.X) + writeFloat32(rect.Min.Y) + + writeFloat32(rect.Max.X) + writeFloat32(rect.Max.Y) + end + + local function writeRegion3(region) + assert(typeof(region) == "Region3", "argument #1 to BitBuffer.writeRegion3 should be a Region3") + + local min = region.CFrame.Position - (region.Size / 2) + local max = region.CFrame.Position + (region.Size / 2) + + writeFloat32(min.X) + writeFloat32(min.Y) + writeFloat32(min.Z) + + writeFloat32(max.X) + writeFloat32(max.Y) + writeFloat32(max.Z) + end + + local function writeEnum(enum) + assert(typeof(enum) == "EnumItem", "argument #1 to BitBuffer.writeEnum should be an EnumItem") + + -- Relying upon tostring is generally not good, but there's not any other options for this. + writeTerminatedString(tostring(enum.EnumType)) + writeUInt16(enum.Value) -- Optimistically assuming no Roblox Enum value will ever pass 65,535 + end + + local function writeNumberRange(range) + assert(typeof(range) == "NumberRange", "argument #1 to BitBuffer.writeNumberRange should be a NumberRange") + + writeFloat32(range.Min) + writeFloat32(range.Max) + end + + local function writeNumberSequence(sequence) + assert( + typeof(sequence) == "NumberSequence", + "argument #1 to BitBuffer.writeNumberSequence should be a NumberSequence" + ) + + writeUInt32(#sequence.Keypoints) + for _, keypoint in ipairs(sequence.Keypoints) do + writeFloat32(keypoint.Time) + writeFloat32(keypoint.Value) + writeFloat32(keypoint.Envelope) + end + end + + local function writeColorSequence(sequence) + assert( + typeof(sequence) == "ColorSequence", + "argument #1 to BitBuffer.writeColorSequence should be a ColorSequence" + ) + + writeUInt32(#sequence.Keypoints) + for _, keypoint in ipairs(sequence.Keypoints) do + local c3 = keypoint.Value + writeFloat32(keypoint.Time) + writeByte(math.floor(c3.R * 0xff + 0.5)) + writeByte(math.floor(c3.G * 0xff + 0.5)) + writeByte(math.floor(c3.B * 0xff + 0.5)) + end + end + + -- These are the read functions for the 'abstract' data types. At the bottom, there are shorthand read functions. + + local function readBits(n) + assert(type(n) == "number", "argument #1 to BitBuffer.readBits should be a number") + assert(n > 0, "argument #1 to BitBuffer.readBits should be greater than zero") + assert(n % 1 == 0, "argument #1 to BitBuffer.readBits should be an integer") + + assert(pointer + n <= bitCount, "BitBuffer.readBits cannot read past the end of the stream") + + -- The first of two main functions for the actual 'reading' of the bitbuffer. + -- Reads `n` bits and returns an array of their values. + local output = table.create(n) --! + local byte = bytes[pointerByte] -- For the sake of efficiency, the current byte that the bits are coming from is stored + local c = pointer % 8 -- A counter is set with the current position of the pointer in the byte + for i = 1, n do + -- Then, it's as easy as moving through the bits of the byte + -- And getting the individiual bit values + local pow = powers_of_2[7 - c] + output[i] = BOOL_TO_BIT[bit32.btest(byte, pow)] -- Test if a bit is on by &ing it by 2^[bit position] + c = c + 1 + if c == 8 then -- If the byte boundary is reached, increment pointerByte and store the new byte in `byte` + pointerByte = pointerByte + 1 + byte = bytes[pointerByte] + c = 0 + end + end + pointer = pointer + n -- Move the pointer forward + return output + end + + --Skip to the end of the current byte + local function skipStrayBits() + local c = pointer % 8 + if (c > 0) then + pointer += 8 - c + pointerByte += 1 + end + end + + local function readBytesFast() + return bytes + end + + + local function readByte() + assert(pointer + 8 <= bitCount, "BitBuffer.readByte cannot read past the end of the stream") + -- The second of two main functions for the actual 'reading' of the bitbuffer. + -- Reads a byte and returns it + local c = pointer % 8 -- How far into the pointerByte the pointer is + local byte1 = bytes[pointerByte] -- The pointerByte + pointer = pointer + 8 + if c == 0 then -- Trivial if the pointer is at the beginning of the pointerByte + pointerByte = pointerByte + 1 + return byte1 + else + pointerByte = pointerByte + 1 + -- Get the remainder of the first pointerByte and add it to the part of the new pointerByte that's required + -- Both these methods are explained in writeByte + return bit32.band(bit32.lshift(byte1, c), 255) + bit32.rshift(bytes[pointerByte], 8 - c) + end + end + + local function readUnsigned(width) + assert(type(width) == "number", "argument #1 to BitBuffer.readUnsigned should be a number") + assert(width >= 1 and width <= 64, "argument #1 to BitBuffer.readUnsigned should be in the range [1, 64]") + assert(width % 1 == 0, "argument #1 to BitBuffer.readUnsigned should be an integer") + + assert(pointer + width <= bitCount, "BitBuffer.readUnsigned cannot read past the end of the stream") + -- Implementing this on its own was considered because of a worry that it would be inefficient to call + -- readByte and readBit several times, but it was decided the simplicity is worth a minor performance hit. + local bytesInN, bitsInN = math.floor(width / 8), width % 8 + + -- No check is required for if the width is greater than 32 because bit32 isn't used. + local n = 0 + -- Shift and add a read byte however many times is necessary + -- Adding after shifting is importnat - it prevents there from being 8 empty bits of space + for _ = 1, bytesInN do + n = n * 0x100 -- 2^8; equivalent to n << 8 + n = n + readByte() + end + -- The bits are then read and added to the number + if bitsInN ~= 0 then + for _, v in ipairs(readBits(width % 8)) do --todo benchmark against concat+tonumber; might be worth the code smell + n = n * 2 + n = n + v + end + end + return n + end + + local function readSigned(width) + assert(type(width) == "number", "argument #1 to BitBuffer.readSigned should be a number") + assert(width >= 2 and width <= 64, "argument #1 to BitBuffer.readSigned should be in the range [2, 64]") + assert(width % 1 == 0, "argument #1 to BitBuffer.readSigned should be an integer") + + assert(pointer + 8 <= bitCount, "BitBuffer.readSigned cannot read past the end of the stream") + local sign = readBits(1)[1] + local n = readUnsigned(width - 1) -- Again, width-1 is because one bit is used for the sign + + -- As said in writeSigned, the written number is unmodified if the number is positive (the sign bit is 0) + if sign == 0 then + return n + else + -- And the number is equal to max value of the width + the number if the number is negative (the sign bit is 1) + -- To reverse that, the max value is subtracted from the stored number. + return n - powers_of_2[width - 1] + end + end + + local function readFloat(exponentWidth, mantissaWidth) + assert(type(exponentWidth) == "number", "argument #1 to BitBuffer.readFloat should be a number") + assert( + exponentWidth >= 1 and exponentWidth <= 64, + "argument #1 to BitBuffer.readFloat should be in the range [1, 64]" + ) + assert(exponentWidth % 1 == 0, "argument #1 to BitBuffer.readFloat should be an integer") + + assert(type(mantissaWidth) == "number", "argument #2 to BitBuffer.readFloat should be a number") + assert( + mantissaWidth >= 1 and mantissaWidth <= 64, + "argument #2 to BitBuffer.readFloat should be in the range [1, 64]" + ) + assert(mantissaWidth % 1 == 0, "argument #2 to BitBuffer.readFloat should be an integer") + + assert( + pointer + exponentWidth + mantissaWidth + 1 <= bitCount, + "BitBuffer.readFloat cannot read past the end of the stream" + ) + -- Recomposing floats is rather straightfoward. + -- The bias is subtracted from the exponent, the mantissa is shifted back by mantissaWidth, one is added to the mantissa + -- and the whole thing is recomposed with math.ldexp (this is identical to mantissa*(2^exponent)). + + local bias = powers_of_2[exponentWidth - 1] - 1 + + local sign = readBits(1)[1] + local exponent = readUnsigned(exponentWidth) + local mantissa = readUnsigned(mantissaWidth) + + -- Before normal numbers are handled though, special cases and subnormal numbers are once again handled seperately + if exponent == powers_of_2[exponentWidth] - 1 then + if mantissa ~= 0 then -- If the exponent is all 1s and the mantissa isn't zero, the number is NaN + return 0 / 0 + else -- Otherwise, it's positive or negative infinity + return sign == 0 and math.huge or -math.huge + end + elseif exponent == 0 then + if mantissa == 0 then -- If the exponent and mantissa are both zero, the number is zero. + return 0 + else -- If the exponent is zero and the mantissa is not zero, the number is subnormal + -- Subnormal numbers are straightforward: shifting the mantissa so that it's a fraction is all that's required + mantissa = mantissa / powers_of_2[mantissaWidth] + + -- Since the exponent is 0, it's actual value is just -bias (it would be exponent-bias) + -- As previously touched on in writeFloat, the exponent value is off by 1 in Lua though. + return sign == 1 and -math.ldexp(mantissa, -bias + 1) or math.ldexp(mantissa, -bias + 1) + end + end + + -- First, the mantissa is shifted back by the mantissaWidth + -- Then, 1 is added to it to 'normalize' it. + mantissa = (mantissa / powers_of_2[mantissaWidth]) + 1 + + -- Because the mantissa is normalized above (the leading 1 is in the ones place), it's accurate to say exponent-bias + return sign == 1 and -math.ldexp(mantissa, exponent - bias) or math.ldexp(mantissa, exponent - bias) + end + + local function readString() + assert(pointer + 24 <= bitCount, "BitBuffer.readString cannot read past the end of the stream") + -- Reading a length-prefixed string is rather straight forward. + -- The length is read, then that many bytes are read and put in a string. + + local stringLength = readUnsigned(24) + assert(pointer + (stringLength * 8) <= bitCount, "BitBuffer.readString cannot read past the end of the stream") + + local outputCharacters = table.create(stringLength) --! + + for i = 1, stringLength do + outputCharacters[i] = readByte() + end + + local output = table.create(math.ceil(stringLength / 4096)) + local k = 1 + for i = 1, stringLength, 4096 do + output[k] = string.char(table.unpack(outputCharacters, i, math.min(stringLength, i + 4095))) + k = k + 1 + end + + return table.concat(output) + end + + local function readTerminatedString() + local outputCharacters = {} + + -- Bytes are read continuously until either a nul-character is reached or until the stream runs out. + local length = 0 + while true do + local byte = readByte() + if not byte then -- Stream has ended + error("BitBuffer.readTerminatedString cannot read past the end of the stream", 2) + elseif byte == 0 then -- String has ended + break + else -- Add byte to string + length = length + 1 + outputCharacters[length] = byte + end + end + + local output = table.create(math.ceil(length / 4096)) + local k = 1 + for l = 1, length, 4096 do + output[k] = string.char(table.unpack(outputCharacters, l, math.min(length, l + 4095))) + k = k + 1 + end + + return table.concat(output) + end + + local function readSetLengthString(length) + assert(type(length) == "number", "argument #1 to BitBuffer.readSetLengthString should be a number") + assert(length >= 0, "argument #1 to BitBuffer.readSetLengthString should be zero or higher.") + assert(length % 1 == 0, "argument #1 to BitBuffer.readSetLengthString should be an integer") + + assert( + pointer + (length * 8) <= bitCount, + "BitBuffer.readSetLengthString cannot read past the end of the stream" + ) + -- `length` number of bytes are read and put into a string + + local outputCharacters = table.create(length) --! + + for i = 1, length do + outputCharacters[i] = readByte() + end + + local output = table.create(math.ceil(length / 4096)) + local k = 1 + for i = 1, length, 4096 do + output[k] = string.char(table.unpack(outputCharacters, i, math.min(length, i + 4095))) + k = k + 1 + end + + return table.concat(output) + end + + local function readField(n) + assert(type(n) == "number", "argument #1 to BitBuffer.readField should be a number") + assert(n > 0, "argument #1 to BitBuffer.readField should be above 0") + assert(n % 1 == 0, "argument #1 to BitBuffer.readField should be an integer") + + assert(pointer + n <= bitCount, "BitBuffer.readField cannot read past the end of the stream") + -- Reading a bit field is again rather simple. You read the actual field, then take the bits out. + local readInt = readUnsigned(n) + local output = table.create(n) --! + + for i = n, 1, -1 do -- In reverse order since we're pulling bits out from lsb to msb + output[i] = readInt % 2 == 1 -- Equivalent to an extraction of the lsb + readInt = math.floor(readInt / 2) -- Equivalent to readInt>>1 + end + + return output + end + + -- All read functions below here are shorthands. + -- As with their write variants, these functions are implemented manually using readByte for performance reasons. + + local function readUInt8() + assert(pointer + 8 <= bitCount, "BitBuffer.readUInt8 cannot read past the end of the stream") + + return readByte() + end + + local function readUInt16() + assert(pointer + 16 <= bitCount, "BitBuffer.readUInt16 cannot read past the end of the stream") + + return bit32.lshift(readByte(), 8) + readByte() + end + + local function readUInt32() + assert(pointer + 32 <= bitCount, "BitBuffer.readUInt32 cannot read past the end of the stream") + + return bit32.lshift(readByte(), 24) + bit32.lshift(readByte(), 16) + bit32.lshift(readByte(), 8) + readByte() + end + + local function readInt8() + assert(pointer + 8 <= bitCount, "BitBuffer.readInt8 cannot read past the end of the stream") + + local n = readByte() + local sign = bit32.btest(n, 128) + n = bit32.band(n, 127) + + if sign then + return n - 128 + else + return n + end + end + + local function readInt16() + assert(pointer + 16 <= bitCount, "BitBuffer.readInt16 cannot read past the end of the stream") + + local n = bit32.lshift(readByte(), 8) + readByte() + local sign = bit32.btest(n, 32768) + n = bit32.band(n, 32767) + + if sign then + return n - 32768 + else + return n + end + end + + local function readInt32() + assert(pointer + 32 <= bitCount, "BitBuffer.readInt32 cannot read past the end of the stream") + + local n = bit32.lshift(readByte(), 24) + bit32.lshift(readByte(), 16) + bit32.lshift(readByte(), 8) + readByte() + local sign = bit32.btest(n, 2147483648) + n = bit32.band(n, 2147483647) + + if sign then + return n - 2147483648 + else + return n + end + end + + local function readFloat16() + assert(pointer + 16 <= bitCount, "BitBuffer.readFloat16 cannot read past the end of the stream") + + local b0 = readByte() + local sign = bit32.btest(b0, 128) + local exponent = bit32.rshift(bit32.band(b0, 127), 2) + local mantissa = bit32.lshift(bit32.band(b0, 3), 8) + readByte() + + if exponent == 31 then --2^5-1 + if mantissa ~= 0 then + return 0 / 0 + else + return sign and -math.huge or math.huge + end + elseif exponent == 0 then + if mantissa == 0 then + return 0 + else + return sign and -math.ldexp(mantissa / 1024, -14) or math.ldexp(mantissa / 1024, -14) + end + end + + mantissa = (mantissa / 1024) + 1 + + return sign and -math.ldexp(mantissa, exponent - 15) or math.ldexp(mantissa, exponent - 15) + end + + local function readFloat32() + assert(pointer + 32 <= bitCount, "BitBuffer.readFloat32 cannot read past the end of the stream") + + local b0 = readByte() + local b1 = readByte() + local sign = bit32.btest(b0, 128) + local exponent = bit32.band(bit32.lshift(b0, 1), 255) + bit32.rshift(b1, 7) + local mantissa = bit32.lshift(bit32.band(b1, 127), 23 - 7) + + bit32.lshift(readByte(), 23 - 7 - 8) + + bit32.lshift(readByte(), 23 - 7 - 8 - 8) + + if exponent == 255 then -- 2^8-1 + if mantissa ~= 0 then + return 0 / 0 + else + return sign and -math.huge or math.huge + end + elseif exponent == 0 then + if mantissa == 0 then + return 0 + else + -- -126 is the 0-bias+1 + return sign and -math.ldexp(mantissa / 8388608, -126) or math.ldexp(mantissa / 8388608, -126) + end + end + + mantissa = (mantissa / 8388608) + 1 + + return sign and -math.ldexp(mantissa, exponent - 127) or math.ldexp(mantissa, exponent - 127) + end + + local function readFloat64() + assert(pointer + 64 <= bitCount, "BitBuffer.readFloat64 cannot read past the end of the stream") + + local b0 = readByte() + local b1 = readByte() + + local sign = bit32.btest(b0, 128) + local exponent = bit32.lshift(bit32.band(b0, 127), 4) + bit32.rshift(b1, 4) + local mostSignificantChunk = bit32.lshift(bit32.band(b1, 15), 16) + bit32.lshift(readByte(), 8) + readByte() + local leastSignificantChunk = bit32.lshift(readByte(), 24) + + bit32.lshift(readByte(), 16) + + bit32.lshift(readByte(), 8) + + readByte() + + -- local mantissa = (bit32.lshift(bit32.band(b1, 15), 16)+bit32.lshift(readByte(), 8)+readByte())*0x100000000+ + -- bit32.lshift(readByte(), 24)+bit32.lshift(readByte(), 16)+bit32.lshift(readByte(), 8)+readByte() + + local mantissa = mostSignificantChunk * 0x100000000 + leastSignificantChunk + + if exponent == 2047 then -- 2^11-1 + if mantissa ~= 0 then + return 0 / 0 + else + return sign and -math.huge or math.huge + end + elseif exponent == 0 then + if mantissa == 0 then + return 0 + else + return sign and -math.ldexp(mantissa / 4503599627370496, -1022) + or math.ldexp(mantissa / 4503599627370496, -1022) + end + end + + mantissa = (mantissa / 4503599627370496) + 1 + + return sign and -math.ldexp(mantissa, exponent - 1023) or math.ldexp(mantissa, exponent - 1023) + end + + -- All read functions below here are Roblox specific datatypes. + + local function readBrickColor() + assert(pointer + 16 <= bitCount, "BitBuffer.readBrickColor cannot read past the end of the stream") + + return BrickColor.new(readUInt16()) + end + + local function readColor3() + assert(pointer + 24 <= bitCount, "BitBuffer.readColor3 cannot read past the end of the stream") + + return Color3.fromRGB(readByte(), readByte(), readByte()) + end + + local function readCFrame() + assert(pointer + 8 <= bitCount, "BitBuffer.readCFrame cannot read past the end of the stream") + + local id = readByte() + + if id == 0 then + assert(pointer + 384 <= bitCount, "BitBuffer.readCFrame cannot read past the end of the stream") -- 4*12 bytes = 383 bits + + -- stylua: ignore + return CFrame.new( + readFloat32(), readFloat32(), readFloat32(), + readFloat32(), readFloat32(), readFloat32(), + readFloat32(), readFloat32(), readFloat32(), + readFloat32(), readFloat32(), readFloat32() + ) + else + assert(pointer + 96 <= bitCount, "BitBuffer.readCFrame cannot read past the end of the stream") -- 4*3 bytes = 96 bits + + local rightVector = NORMAL_ID_VECTORS[math.floor(id / 6)] + local upVector = NORMAL_ID_VECTORS[id % 6] + local lookVector = rightVector:Cross(upVector) + + -- CFrame's full-matrix constructor takes right/up/look vectors as columns... + -- stylua: ignore + return CFrame.new( + readFloat32(), readFloat32(), readFloat32(), + rightVector.X, upVector.X, lookVector.X, + rightVector.Y, upVector.Y, lookVector.Y, + rightVector.Z, upVector.Z, lookVector.Z + ) + end + end + + local function readVector3() + assert(pointer + 96 <= bitCount, "BitBuffer.readVector3 cannot read past the end of the stream") + + return Vector3.new(readFloat32(), readFloat32(), readFloat32()) + end + + local function readVector2() + assert(pointer + 64 <= bitCount, "BitBuffer.readVector2 cannot read past the end of the stream") + + return Vector2.new(readFloat32(), readFloat32()) + end + + local function readUDim2() + assert(pointer + 128 <= bitCount, "BitBuffer.readUDim2 cannot read past the end of the stream") + + return UDim2.new(readFloat32(), readInt32(), readFloat32(), readInt32()) + end + + local function readUDim() + assert(pointer + 64 <= bitCount, "BitBuffer.readUDim cannot read past the end of the stream") + + return UDim.new(readFloat32(), readInt32()) + end + + local function readRay() + assert(pointer + 192 <= bitCount, "BitBuffer.readRay cannot read past the end of the stream") + + return Ray.new( + Vector3.new(readFloat32(), readFloat32(), readFloat32()), + Vector3.new(readFloat32(), readFloat32(), readFloat32()) + ) + end + + local function readRect() + assert(pointer + 128 <= bitCount, "BitBuffer.readRect cannot read past the end of the stream") + + return Rect.new(readFloat32(), readFloat32(), readFloat32(), readFloat32()) + end + + local function readRegion3() + assert(pointer + 192 <= bitCount, "BitBuffer.readRegion3 cannot read past the end of the stream") + + return Region3.new( + Vector3.new(readFloat32(), readFloat32(), readFloat32()), + Vector3.new(readFloat32(), readFloat32(), readFloat32()) + ) + end + + local function readEnum() + assert(pointer + 8 <= bitCount, "BitBuffer.readEnum cannot read past the end of the stream") + + local name = readTerminatedString() -- This might expose an error from readString to the end-user but it's not worth the hassle to fix. + + assert(pointer + 16 <= bitCount, "BitBuffer.readEnum cannot read past the end of the stream") + + local value = readUInt16() -- Again, optimistically assuming no Roblox Enum value will ever pass 65,535 + + -- Catching a potential error only to throw it with different formatting seems... Superfluous. + -- Open an issue on github if you feel otherwise. + for _, v in ipairs(Enum[name]:GetEnumItems()) do + if v.Value == value then + return v + end + end + + error( + "BitBuffer.readEnum could not get value: `" + .. tostring(value) + .. "` is not a valid member of `" + .. name + .. "`", + 2 + ) + end + + local function readNumberRange() + assert(pointer + 64 <= bitCount, "BitBuffer.readNumberRange cannot read past the end of the stream") + + return NumberRange.new(readFloat32(), readFloat32()) + end + + local function readNumberSequence() + assert(pointer + 32 <= bitCount, "BitBuffer.readNumberSequence cannot read past the end of the stream") + + local keypointCount = readUInt32() + + assert(pointer + keypointCount * 96, "BitBuffer.readColorSequence cannot read past the end of the stream") + + local keypoints = table.create(keypointCount) + + -- As it turns out, creating a NumberSequence with a negative value as its first argument (in the first and second constructor) + -- creates NumberSequenceKeypoints with negative envelopes. The envelope is read and saved properly, as you would expect, + -- but you can't create a NumberSequence with a negative envelope if you're using a table of keypoints (which is happening here). + -- If you're confused, run this snippet: NumberSequence.new(NumberSequence.new(-1).Keypoints) + -- As a result, there has to be some branching logic in this function. + -- ColorSequences don't have envelopes so it's not necessary for them. + + for i = 1, keypointCount do + local time, value, envelope = readFloat32(), readFloat32(), readFloat32() + if value < 0 then + envelope = nil + end + keypoints[i] = NumberSequenceKeypoint.new(time, value, envelope) + end + + return NumberSequence.new(keypoints) + end + + local function readColorSequence() + assert(pointer + 32 <= bitCount, "BitBuffer.readColorSequence cannot read past the end of the stream") + + local keypointCount = readUInt32() + + assert(pointer + keypointCount * 56, "BitBuffer.readColorSequence cannot read past the end of the stream") + + local keypoints = table.create(keypointCount) + + for i = 1, keypointCount do + keypoints[i] = ColorSequenceKeypoint.new(readFloat32(), Color3.fromRGB(readByte(), readByte(), readByte())) + end + + return ColorSequence.new(keypoints) + end + + return { + dumpBinary = dumpBinary, + dumpString = dumpString, + dumpHex = dumpHex, + dumpBase64 = dumpBase64, + exportChunk = exportChunk, + exportBase64Chunk = exportBase64Chunk, + exportHexChunk = exportHexChunk, + + crc32 = crc32, + getLength = getLength, + getByteLength = getByteLength, + getPointer = getPointer, + setPointer = setPointer, + setPointerFromEnd = setPointerFromEnd, + getPointerByte = getPointerByte, + setPointerByte = setPointerByte, + setPointerByteFromEnd = setPointerByteFromEnd, + isFinished = isFinished, + + writeBits = writeBits, + writeByte = writeByte, + writeBytesFast = writeBytesFast, + writeUnsigned = writeUnsigned, + writeSigned = writeSigned, + writeFloat = writeFloat, + writeBase64 = writeBase64, + writeString = writeString, + writeTerminatedString = writeTerminatedString, + writeSetLengthString = writeSetLengthString, + writeField = writeField, + + writeUInt8 = writeUInt8, + writeUInt16 = writeUInt16, + writeUInt32 = writeUInt32, + writeInt8 = writeInt8, + writeInt16 = writeInt16, + writeInt32 = writeInt32, + + writeFloat16 = writeFloat16, + writeFloat32 = writeFloat32, + writeFloat64 = writeFloat64, + + writeBrickColor = writeBrickColor, + writeColor3 = writeColor3, + writeCFrame = writeCFrame, + writeVector3 = writeVector3, + writeVector2 = writeVector2, + writeUDim2 = writeUDim2, + writeUDim = writeUDim, + writeRay = writeRay, + writeRect = writeRect, + writeRegion3 = writeRegion3, + writeEnum = writeEnum, + writeNumberRange = writeNumberRange, + writeNumberSequence = writeNumberSequence, + writeColorSequence = writeColorSequence, + + readBits = readBits, + readByte = readByte, + readUnsigned = readUnsigned, + readSigned = readSigned, + readFloat = readFloat, + readString = readString, + readTerminatedString = readTerminatedString, + readSetLengthString = readSetLengthString, + readField = readField, + readBytesFast = readBytesFast, + skipStrayBits = skipStrayBits, + + readUInt8 = readUInt8, + readUInt16 = readUInt16, + readUInt32 = readUInt32, + readInt8 = readInt8, + readInt16 = readInt16, + readInt32 = readInt32, + + readFloat16 = readFloat16, + readFloat32 = readFloat32, + readFloat64 = readFloat64, + + readBrickColor = readBrickColor, + readColor3 = readColor3, + readCFrame = readCFrame, + readVector3 = readVector3, + readVector2 = readVector2, + readUDim2 = readUDim2, + readUDim = readUDim, + readRay = readRay, + readRect = readRect, + readRegion3 = readRegion3, + readEnum = readEnum, + readNumberRange = readNumberRange, + readNumberSequence = readNumberSequence, + readColorSequence = readColorSequence, + } +end + +return bitBuffer \ No newline at end of file diff --git a/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/Node.lua b/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/Node.lua new file mode 100644 index 0000000..9e0434a --- /dev/null +++ b/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/Node.lua @@ -0,0 +1,38 @@ +local Node = {} +Node.__index = Node + +function Node.new(data) + local node = setmetatable({}, Node) + node.data = data + node.left = nil + node.right = nil + return node +end + +-- return an iterator that traverses the tree in order +function Node:inorder() + local stack = {} + local current = {self, ""} + table.insert(stack, current) + + return function() + while current[1].left do + local parent = current + current = {parent[1].left, parent[2] .. "0"} + table.insert(stack, current) + end + + if #stack > 0 then + local node = table.remove(stack) + + if node[1].right then + local parent = node + current = {parent[1].right, parent[2] .. "1"} + table.insert(stack, current) + end + return node[1], node[2] + end + end +end + +return Node \ No newline at end of file diff --git a/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/PriorityQueue.lua b/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/PriorityQueue.lua new file mode 100644 index 0000000..c8a7604 --- /dev/null +++ b/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/PriorityQueue.lua @@ -0,0 +1,232 @@ +--[[ + + PriorityQueue - v1.0.1 - public domain Lua priority queue + implemented with indirect binary heap + no warranty implied; use at your own risk + + based on binaryheap library (github.com/iskolbin/binaryheap) + + author: Ilya Kolbin (iskolbin@gmail.com) + url: github.com/iskolbin/priorityqueue + + See documentation in README file. + + COMPATIBILITY + + Lua 5.1, 5.2, 5.3, LuaJIT 1, 2 + + LICENSE + + This software is dual-licensed to the public domain and under the following + license: you are granted a perpetual, irrevocable license to copy, modify, + publish, and distribute this file as you see fit. + +--]] + +local floor, setmetatable = math.floor, setmetatable + +local function siftup( self, from ) + local items, priorities, indices, higherpriority = self, self._priorities, self._indices, self._higherpriority + local index = from + local parent = floor( index / 2 ) + while index > 1 and higherpriority( priorities[index], priorities[parent] ) do + priorities[index], priorities[parent] = priorities[parent], priorities[index] + items[index], items[parent] = items[parent], items[index] + indices[items[index]], indices[items[parent]] = index, parent + index = parent + parent = floor( index / 2 ) + end + return index +end + +local function siftdown( self, limit ) + local items, priorities, indices, higherpriority, size = self, self._priorities, self._indices, self._higherpriority, self._size + for index = limit, 1, -1 do + local left = index + index + local right = left + 1 + while left <= size do + local smaller = left + if right <= size and higherpriority( priorities[right], priorities[left] ) then + smaller = right + end + if higherpriority( priorities[smaller], priorities[index] ) then + items[index], items[smaller] = items[smaller], items[index] + priorities[index], priorities[smaller] = priorities[smaller], priorities[index] + indices[items[index]], indices[items[smaller]] = index, smaller + else + break + end + index = smaller + left = index + index + right = left + 1 + end + end +end + +local PriorityQueueMt + +local PriorityQueue = {} + +local function minishigher( a, b ) + return a < b +end + +local function maxishigher( a, b ) + return a > b +end + +function PriorityQueue.new( priority_or_array ) + local t = type( priority_or_array ) + local higherpriority = minishigher + + if t == 'table' then + higherpriority = priority_or_array.higherpriority or higherpriority + elseif t == 'function' or t == 'string' then + higherpriority = priority_or_array + elseif t ~= 'nil' then + local msg = 'Wrong argument type to PriorityQueue.new, it must be table or function or string, has: %q' + error( msg:format( t )) + end + + if type( higherpriority ) == 'string' then + if higherpriority == 'min' then + higherpriority = minishigher + elseif higherpriority == 'max' then + higherpriority = maxishigher + else + local msg = 'Wrong string argument to PriorityQueue.new, it must be "min" or "max", has: %q' + error( msg:format( tostring( higherpriority ))) + end + end + + local self = setmetatable( { + _priorities = {}, + _indices = {}, + _size = 0, + _higherpriority = higherpriority or minishigher + }, PriorityQueueMt ) + + if t == 'table' then + self:batchenq( priority_or_array ) + end + + return self +end + +function PriorityQueue:enqueue( item, priority ) + local items, priorities, indices = self, self._priorities, self._indices + if indices[item] ~= nil then + error( 'Item ' .. tostring(indices[item]) .. ' is already in the heap' ) + end + local size = self._size + 1 + self._size = size + items[size], priorities[size], indices[item] = item, priority, size + siftup( self, size ) + return self +end + +function PriorityQueue:remove( item ) + local index = self._indices[item] + if index ~= nil then + local size = self._size + local items, priorities, indices = self, self._priorities, self._indices + indices[item] = nil + if size == index then + items[size], priorities[size] = nil, nil + self._size = size - 1 + else + local lastitem = items[size] + items[index], priorities[index] = items[size], priorities[size] + items[size], priorities[size] = nil, nil + indices[lastitem] = index + size = size - 1 + self._size = size + if size > 1 then + siftdown( self, siftup( self, index )) + end + end + return true + else + return false + end +end + +function PriorityQueue:contains( item ) + return self._indices[item] ~= nil +end + +function PriorityQueue:update( item, priority ) + local ok = self:remove( item ) + if ok then + self:enqueue( item, priority ) + return true + else + return false + end +end + +function PriorityQueue:dequeue() + local size = self._size + + assert( size > 0, 'Heap is empty' ) + + local items, priorities, indices = self, self._priorities, self._indices + local item, priority = items[1], priorities[1] + indices[item] = nil + + if size > 1 then + local newitem = items[size] + items[1], priorities[1] = newitem, priorities[size] + items[size], priorities[size] = nil, nil + indices[newitem] = 1 + size = size - 1 + self._size = size + siftdown( self, 1 ) + else + items[1], priorities[1] = nil, nil + self._size = 0 + end + + return item, priority +end + +function PriorityQueue:peek() + return self[1], self._priorities[1] +end + +function PriorityQueue:len() + return self._size +end + +function PriorityQueue:empty() + return self._size <= 0 +end + +function PriorityQueue:batchenq( iparray ) + local items, priorities, indices = self, self._priorities, self._indices + local size = self._size + for i = 1, #iparray, 2 do + local item, priority = iparray[i], iparray[i+1] + if indices[item] ~= nil then + error( 'Item ' .. tostring(indices[item]) .. ' is already in the heap' ) + end + size = size + 1 + items[size], priorities[size] = item, priority + indices[item] = size + end + self._size = size + if size > 1 then + siftdown( self, floor( size / 2 )) + end +end + +PriorityQueueMt = { + __index = PriorityQueue, + __len = PriorityQueue.len, +} + +return setmetatable( PriorityQueue, { + __call = function( _, ... ) + return PriorityQueue.new( ... ) + end +} ) \ No newline at end of file diff --git a/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/init.lua b/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/init.lua new file mode 100644 index 0000000..a18422f --- /dev/null +++ b/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/Huffman/init.lua @@ -0,0 +1,114 @@ +--Huffman.lua +--iiau, Sat May 18 2024 +--Implementation of huffman coding algorithm for use in Roblox + +local Huffman = {} +local PriorityQueue = require(script.PriorityQueue) +local Node = require(script.Node) +local BitBuffer = require(script.BitBuffer) + +local CHUNK_SIZE = 256 + +--thanks to https://stackoverflow.com/a/32220398 for helping me with this +local function to_bin(n) + local t = {} + for _ = 1, 32 do + n = bit32.rrotate(n, -1) + table.insert(t, bit32.band(n, 1)) + end + return table.concat(t) +end + +-- Encode a string to huffman coded string. Limitation is that the data should not be more than 16777215 bytes. +-- @param data The string to encode +-- @return The encoded string +Huffman.encode = function(data: string) : string + assert(#data > 0, "Data must not be empty") + local buffer = BitBuffer() + + -- get the frequency of each character in the string + local freq, dict, size = {}, {}, 0 + for c in data:gmatch(".") do + freq[c] = (freq[c] or 0) + 1 + end + for _ in pairs(freq) do + size += 1 + end + + local q = PriorityQueue.new 'min' + for k: string, v: number in pairs(freq) do + local leaf = Node.new(string.byte(k)) + q:enqueue(leaf, v) + end + + while q:len() > 1 do + local left, freq_l = q:dequeue() + local right, freq_r = q:dequeue() + local parent = Node.new() + parent.left = left + parent.right = right + + q:enqueue(parent, freq_l + freq_r) + end + local tree = q:dequeue() + buffer.writeUInt8(size-1) + buffer.writeUnsigned(24, #data) + for node, bits: string in tree:inorder() do + if not node.data then + continue + end + local number = tonumber(bits, 2) + local bit_array = string.split(bits, "") + for i = 1, #bit_array do + bit_array[i] = tonumber(bit_array[i]) + end + + dict[string.char(node.data)] = bit_array + buffer.writeUInt8(node.data) -- char + buffer.writeUnsigned(5, #bits) -- number of bits + buffer.writeUnsigned(#bits, number) -- bits + end + for c in data:gmatch(".") do + buffer.writeBits(table.unpack(dict[c])) + end + + -- to avoid the dreaded too many results to unpack error + local chunks = {} + for _, chunk in buffer.exportChunk(CHUNK_SIZE) do + table.insert(chunks, chunk) + end + return table.concat(chunks) +end + +-- Decode a string from huffman coded string +-- @param data The string to decode +-- @return The decoded string +Huffman.decode = function(data: string) : string + assert(#data > 0, "Data must not be empty") + local buffer = BitBuffer(data) + + local dict_size = buffer.readUInt8()+1 + local len_data = buffer.readUnsigned(24) + local dict, read = {}, 0 + + for i = 1, dict_size do + local char = buffer.readUInt8() + local digits = buffer.readUnsigned(5) + local bits = buffer.readUnsigned(digits) + dict[to_bin(bits):sub(-digits)] = char + end + local decoded = {} + local bits = "" + while read < len_data do + bits ..= buffer.readBits(1)[1] + local char = dict[bits] + if char then + table.insert(decoded, string.char(char)) + bits = "" + read += 1 + end + end + return table.concat(decoded) +end + +return Huffman \ No newline at end of file diff --git a/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/LZW.lua b/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/LZW.lua new file mode 100644 index 0000000..e97c029 --- /dev/null +++ b/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/LZW.lua @@ -0,0 +1,170 @@ +-- Module by 1waffle1 and boatbomber, optimized and fixed by iiau +-- https://devforum.roblox.com/t/text-compression/163637/37 + +local dictionary = {} -- key to len + +do -- populate dictionary + local length = 0 + for i = 32, 127 do + if i ~= 34 and i ~= 92 then + local c = string.char(i) + dictionary[c] = length + dictionary[length] = c + length = length + 1 + end + end +end + +local escapemap_126, escapemap_127 = {}, {} +local unescapemap_126, unescapemap_127 = {}, {} + +local blacklisted_126 = { 34, 92 } +for i = 126, 180 do + table.insert(blacklisted_126, i) +end + +do -- Populate escape map + -- represents the numbers 0-31, 34, 92, 126, 127 (36 characters) + -- and 128-180 (52 characters) + -- https://devforum.roblox.com/t/text-compression/163637/5 + for i = 0, 31 + #blacklisted_126 do + local b = blacklisted_126[i - 31] + local s = i + 32 + + -- Note: 126 and 127 are magic numbers + local c = string.char(b or i) + local e = string.char(s + (s >= 34 and 1 or 0) + (s >= 91 and 1 or 0)) + + escapemap_126[c] = e + unescapemap_126[e] = c + end + + for i = 1, 255 - 180 do + local c = string.char(i + 180) + local s = i + 34 + local e = string.char(s + (s >= 92 and 1 or 0)) + + escapemap_127[c] = e + unescapemap_127[e] = c + end +end + +local function escape(s) + -- escape the control characters 0-31, double quote 34, backslash 92, Tilde 126, and DEL 127 (36 chars) + -- escape characters 128-180 (53 chars) + return string.gsub(string.gsub(s, '[%c"\\\126-\180]', function(c) + return "\126" .. escapemap_126[c] + end), '[\181-\255]', function(c) + return "\127" .. escapemap_127[c] + end) +end +local function unescape(s) + return string.gsub(string.gsub(s, "\127(.)", function(e) + return unescapemap_127[e] + end), "\126(.)", function(e) + return unescapemap_126[e] + end) +end + +local b93Cache = {} +local function tobase93(n) + local value = b93Cache[n] + if value then + return value + end + + local c = n + value = "" + repeat + local remainder = n % 93 + value = dictionary[remainder] .. value + n = (n - remainder) / 93 + until n == 0 + + b93Cache[c] = value + return value +end + +local b10Cache = {} +local function tobase10(value) + local n = b10Cache[value] + if n then + return n + end + + n = 0 + for i = 1, #value do + n = n + math.pow(93, i - 1) * dictionary[string.sub(value, -i, -i)] + end + + b10Cache[value] = n + return n +end + +local function compress(text) + assert(type(text) == "string", "bad argument #1 to 'compress' (string expected, got " .. typeof(text) .. ")") + local dictionaryCopy = table.clone(dictionary) + local key, sequence, size = "", {}, #dictionary + + local width, spans, span = 1, {}, 0 + local function listkey(k) + local value = tobase93(dictionaryCopy[k]) + local valueLength = #value + if valueLength > width then + width, span, spans[width] = valueLength, 0, span + end + table.insert(sequence, string.rep(" ", width - valueLength) .. value) + span += 1 + end + text = escape(text) + for i = 1, #text do + local c = string.sub(text, i, i) + local new = key .. c + if dictionaryCopy[new] then + key = new + else + listkey(key) + key = c + size += 1 + dictionaryCopy[new] = size + dictionaryCopy[size] = new + end + end + listkey(key) + spans[width] = span + return table.concat(spans, ",") .. "|" .. table.concat(sequence) +end + +local function decompress(text) + assert(type(text) == "string", "bad argument #1 to 'decompress' (string expected, got " .. typeof(text) .. ")") + local dictionaryCopy = table.clone(dictionary) + local sequence, spans, content = {}, string.match(text, "(.-)|(.*)") + local groups, start = {}, 1 + for span in string.gmatch(spans, "%d+") do + local width = #groups + 1 + groups[width] = string.sub(content, start, start + span * width - 1) + start = start + span * width + end + local previous + + for width, group in ipairs(groups) do + for value in string.gmatch(group, string.rep(".", width)) do + local entry = dictionaryCopy[tobase10(value)] + if previous then + if entry then + table.insert(dictionaryCopy, previous .. string.sub(entry, 1, 1)) + else + entry = previous .. string.sub(previous, 1, 1) + table.insert(dictionaryCopy, entry) + end + table.insert(sequence, entry) + else + sequence[1] = entry + end + previous = entry + end + end + return unescape(table.concat(sequence)) +end + +return { compress = compress, decompress = decompress } \ No newline at end of file diff --git a/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/init.lua b/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/init.lua new file mode 100644 index 0000000..37cab5b --- /dev/null +++ b/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/Deflate/init.lua @@ -0,0 +1,19 @@ +--Deflate.lua +--iiau, Sat May 18 2024 + +local Deflate = {} + +local LZW = require(script.LZW) +local Huffman = require(script.Huffman) + +Deflate.encode = function(data: string) : string + data = LZW.compress(data) + return Huffman.encode(data) +end + +Deflate.decode = function(data: string) : string + data = Huffman.decode(data) + return LZW.decompress(data) +end + +return Deflate \ No newline at end of file diff --git a/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/init.lua b/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/init.lua new file mode 100644 index 0000000..731a7c8 --- /dev/null +++ b/src/ServerScriptService/Actor/ServerChunkManager/TerrainGen/init.lua @@ -0,0 +1,70 @@ +local TerrainGen = {} + +local deflate = require("./TerrainGen/Deflate") + +local DSS = game:GetService("DataStoreService") +local WORLDNAME = "DEFAULT" +local WORLDID = "b73bb5a6-297d-4352-b637-daec7e8c8f3e" +local Store = DSS:GetDataStore("BlockscraftWorldV1", WORLDID) + +local ChunkManager = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared").ChunkManager) +local Chunk = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared").ChunkManager.Chunk) + +TerrainGen.ServerChunkCache = {} :: {[typeof("")]: typeof(Chunk.new(0,0,0))} + +-- Load a chunk from the DataStore or generate it if not found +function TerrainGen:GetChunk(x, y, z) + local key = `{x},{y},{z}` + if TerrainGen.ServerChunkCache[key] then + return TerrainGen.ServerChunkCache[key] + end + + -- Generate a new chunk if it doesn't exist + local chunk = Chunk.new(x, y, z) + if y == 1 then + for cx = 1, 8 do + for cz = 1, 8 do + --local perlin = math.noise(((x*8)+cx)/100,((z*8)+cz)/100) + chunk:CreateBlock(cx, 1, cz, { id = 1, state = {} }) + --chunk:CreateBlock(x, 2, z, { id = 1, state = {} }) + end + end + end + if y == 0 then + for cx = 1, 8 do + for cy = 1, 8 do + for cz = 1, 8 do + --local perlin = math.noise(((x*8)+cx)/100,((z*8)+cz)/100) + chunk:CreateBlock(cx, cy, cz, { id = 2, state = {} }) + --chunk:CreateBlock(x, 2, z, { id = 1, state = {} }) + end + end + end + end + + TerrainGen.ServerChunkCache[key] = chunk + return chunk +end + +-- Fake Chunk +function TerrainGen:GetFakeChunk(x, y, z) + + -- Generate a new chunk if it doesn't exist + local chunk = Chunk.new(x, y, z) + for cy = 1,8 do + for cx = 1, 8 do + for cz = 1, 8 do + --local perlin = math.noise(((x*8)+cx)/100,((z*8)+cz)/100) + chunk:CreateBlock(cx, cy, cz, { id = -2, state = {} }) + --chunk:CreateBlock(x, 2, z, { id = 1, state = {} }) + end + end + end + + return chunk +end + + +TerrainGen.CM = ChunkManager + +return TerrainGen diff --git a/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua b/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua new file mode 100644 index 0000000..18f291e --- /dev/null +++ b/src/ServerScriptService/Actor/ServerChunkManager/init.server.lua @@ -0,0 +1,189 @@ +print("Hello world!") + +task.synchronize() + +local ReplicatedStorage = game:GetService("ReplicatedStorage") + + +local Shared = ReplicatedStorage:WaitForChild("Shared") +local ModsFolder = ReplicatedStorage:WaitForChild("Mods") + +local Util = require(Shared.Util) +local TG = require("./ServerChunkManager/TerrainGen") + +do + local workspaceModFolder = game:GetService("Workspace"):WaitForChild("mods") + + for _,v in pairs(workspaceModFolder:GetChildren()) do + v.Parent = ModsFolder + end + workspaceModFolder:Destroy() +end + +local ML = require(Shared.ModLoader) +ML.loadModsS() + +do + local bv = Instance.new("BoolValue") + bv.Name = "MLLoaded" + bv.Value = true + bv.Parent = ReplicatedStorage:WaitForChild("Objects") +end + +local MAX_CHUNK_DIST = 200 + +local FakeChunk = TG:GetFakeChunk(-5,-5,-5) + +task.synchronize() + +ReplicatedStorage.Tick.OnServerEvent:Connect(function(player: Player, v: string) + if TG.ServerChunkCache[v] then + pcall(function() + TG.ServerChunkCache[v].inhabitedTime = tick() + end) + end +end) + +ReplicatedStorage.RecieveChunkPacket.OnServerInvoke = function(plr: Player, x: number, y: number, z: number) + -- validate xyz type and limit + if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then + return {} + end + + if math.abs(x) > MAX_CHUNK_DIST or math.abs(y) > MAX_CHUNK_DIST or math.abs(z) > MAX_CHUNK_DIST then + return FakeChunk.data + end + + task.desynchronize() + local chunk = TG:GetChunk(x, y, z) + local chunkdata = chunk.data + task.synchronize() + + return chunkdata + +end + +local tickRemote = ReplicatedStorage.Tick +local remotes = ReplicatedStorage:WaitForChild("Remotes") +local placeRemote = remotes:WaitForChild("PlaceBlock") +local breakRemote = remotes:WaitForChild("BreakBlock") +local blocksFolder = ReplicatedStorage:WaitForChild("Blocks") +local function propogate(a, cx, cy, cz, x, y, z, bd) + task.synchronize() + tickRemote:FireAllClients(a, cx, cy, cz, x, y, z, bd) + task.desynchronize() +end + +local MAX_REACH = 24 +local blockIdMap = {} + +local function rebuildBlockIdMap() + table.clear(blockIdMap) + for _, block in ipairs(blocksFolder:GetChildren()) do + local id = block:GetAttribute("n") + if id ~= nil then + blockIdMap[id] = id + blockIdMap[tostring(id)] = id + end + end +end + +rebuildBlockIdMap() +blocksFolder.ChildAdded:Connect(rebuildBlockIdMap) +blocksFolder.ChildRemoved:Connect(rebuildBlockIdMap) + +local function getPlayerPosition(player: Player): Vector3? + local character = player.Character + if not character then + return nil + end + local root = character:FindFirstChild("HumanoidRootPart") + if not root then + return nil + end + return root.Position +end + +local function isWithinReach(player: Player, cx: number, cy: number, cz: number, x: number, y: number, z: number): boolean + local playerPos = getPlayerPosition(player) + if not playerPos then + return false + end + local blockPos = Util.ChunkPosToCFrame(Vector3.new(cx, cy, cz), Vector3.new(x, y, z)).Position + return (blockPos - playerPos).Magnitude <= MAX_REACH +end + +local function resolveBlockId(blockId: any): string | number | nil + return blockIdMap[blockId] +end + +local function getServerChunk(cx: number, cy: number, cz: number) + task.desynchronize() + local chunk = TG:GetChunk(cx, cy, cz) + task.synchronize() + return chunk +end + +placeRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z, blockId) + --print("place",player, cx, cy, cz, x, y, z, blockData) + + if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then + return + end + if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then + return + end + if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then + return + end + if math.abs(cx) > MAX_CHUNK_DIST or math.abs(cy) > MAX_CHUNK_DIST or math.abs(cz) > MAX_CHUNK_DIST then + --return + end + if not isWithinReach(player, cx, cy, cz, x, y, z) then + return + end + local resolvedId = resolveBlockId(blockId) + if not resolvedId then + return + end + + local chunk = getServerChunk(cx, cy, cz) + if chunk:GetBlockAt(x, y, z) then + return + end + local data = { + id = resolvedId, + state = {} + } + chunk:CreateBlock(x, y, z, data) + propogate("B_C", cx, cy, cz, x, y, z, data) +end) + +breakRemote.OnServerEvent:Connect(function(player, cx, cy, cz, x, y, z) + --print("del",player, cx, cy, cz, x, y, z) + + if typeof(cx) ~= "number" or typeof(cy) ~= "number" or typeof(cz) ~= "number" then + return + end + if typeof(x) ~= "number" or typeof(y) ~= "number" or typeof(z) ~= "number" then + return + end + if x < 1 or x > 8 or y < 1 or y > 8 or z < 1 or z > 8 then + return + end + if math.abs(cx) > MAX_CHUNK_DIST or math.abs(cy) > MAX_CHUNK_DIST or math.abs(cz) > MAX_CHUNK_DIST then + return + end + if not isWithinReach(player, cx, cy, cz, x, y, z) then + return + end + + local chunk = getServerChunk(cx, cy, cz) + if not chunk:GetBlockAt(x, y, z) then + return + end + chunk:RemoveBlock(x, y, z) + propogate("B_D", cx, cy, cz, x, y, z, 0) +end) + +task.desynchronize() diff --git a/src/ServerScriptService/Actor/init.meta.json b/src/ServerScriptService/Actor/init.meta.json new file mode 100644 index 0000000..6af4553 --- /dev/null +++ b/src/ServerScriptService/Actor/init.meta.json @@ -0,0 +1,4 @@ +{ + "className": "Actor", + "ignoreUnknownInstances": true +} \ No newline at end of file diff --git a/src/StarterGui/Crosshair/LocalScript.client.lua b/src/StarterGui/Crosshair/LocalScript.client.lua new file mode 100644 index 0000000..92b726b --- /dev/null +++ b/src/StarterGui/Crosshair/LocalScript.client.lua @@ -0,0 +1,22 @@ +-- force first person mode on the person's camera + +if not game:IsLoaded() then + game.Loaded:Wait() +end + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local UIS = game:GetService("UserInputService") + +ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded") + +game:GetService("Players").LocalPlayer.CameraMode = Enum.CameraMode.LockFirstPerson +UIS.MouseIconEnabled = false + +UIS.InputEnded:Connect(function(k) + if k.KeyCode == Enum.KeyCode.M then + local v = not script.Parent.DummyButton.Modal + UIS.MouseIconEnabled = v + script.Parent.CrosshairLabel.Visible = not v + script.Parent.DummyButton.Modal = v + end +end) \ No newline at end of file diff --git a/src/StarterGui/Crosshair/init.meta.json b/src/StarterGui/Crosshair/init.meta.json new file mode 100644 index 0000000..118e2ca --- /dev/null +++ b/src/StarterGui/Crosshair/init.meta.json @@ -0,0 +1,4 @@ +{ + "className": "ScreenGui", + "ignoreUnknownInstances": true +} \ No newline at end of file diff --git a/src/StarterGui/Game_UI/LocalScript.client.lua b/src/StarterGui/Game_UI/LocalScript.client.lua new file mode 100644 index 0000000..0b92109 --- /dev/null +++ b/src/StarterGui/Game_UI/LocalScript.client.lua @@ -0,0 +1,62 @@ +if not game:IsLoaded() then + game.Loaded:Wait() +end + +local ui = script.Parent + +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded") + +local cd = ReplicatedStorage.Objects.ChunkDebug:Clone() +local sky = ReplicatedStorage.Objects.Sky:Clone() +local base = ReplicatedStorage.Objects.FakeBaseplate:Clone() + + +cd.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") +sky.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") +base.Parent = game:GetService("Workspace"):FindFirstChildOfClass("Terrain") + + +game:GetService("RunService").RenderStepped:Connect(function(dt) + local fps = math.round(1/dt) + pcall(function() + -- pos in chunks of 32 studs of char + local pos = game:GetService("Players").LocalPlayer.Character:GetPivot() + local chunk = { + x = math.round(pos.X/32), + y = math.round(pos.Y/32), + z = math.round(pos.Z/32) + } + + if math.abs(chunk.x) == 0 then chunk.x = 0 end + if math.abs(chunk.y) == 0 then chunk.y = 0 end + if math.abs(chunk.z) == 0 then chunk.z = 0 end + + local bpos = { + x = math.round(pos.X/4), + y = math.round(pos.Y/4), + z = math.round(pos.Z/4) + } + + if math.abs(bpos.x) == 0 then bpos.x = 0 end + if math.abs(bpos.y) == 0 then bpos.y = 0 end + if math.abs(bpos.z) == 0 then bpos.z = 0 end + + sky.CFrame = pos + ui.DebugUpperText.Text = `Chunk {chunk.x} {chunk.y} {chunk.z}\nPos {bpos.x} {bpos.y} {bpos.z}\n{fps} FPS` + + cd:PivotTo(CFrame.new( + chunk.x*32, + chunk.y*32, + chunk.z*32 + )) + + base.CFrame = CFrame.new( + chunk.x*32, + -24, + chunk.z*32 + ) + + end) +end) diff --git a/src/StarterGui/Game_UI/init.meta.json b/src/StarterGui/Game_UI/init.meta.json new file mode 100644 index 0000000..118e2ca --- /dev/null +++ b/src/StarterGui/Game_UI/init.meta.json @@ -0,0 +1,4 @@ +{ + "className": "ScreenGui", + "ignoreUnknownInstances": true +} \ No newline at end of file diff --git a/src/StarterPlayer/StarterPlayerScripts/Actor/ActorInit.client.lua b/src/StarterPlayer/StarterPlayerScripts/Actor/ActorInit.client.lua new file mode 100644 index 0000000..deccdd2 --- /dev/null +++ b/src/StarterPlayer/StarterPlayerScripts/Actor/ActorInit.client.lua @@ -0,0 +1,26 @@ +if not game:IsLoaded() then + game.Loaded:Wait() +end + +pcall(function() + task.synchronize() + game:GetService("Workspace"):WaitForChild("$blockscraft_server",5):Destroy() +end) + +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded") + +local ML = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ModLoader")) + +ML.loadModsC() + +do + local PM = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("PlacementManager")) + PM:Init() +end + +do + local CM = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ChunkManager")) + CM:Init() +end diff --git a/src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua b/src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua new file mode 100644 index 0000000..2af7785 --- /dev/null +++ b/src/StarterPlayer/StarterPlayerScripts/Actor/BlockInteraction.client.lua @@ -0,0 +1,92 @@ +if not game:IsLoaded() then + game.Loaded:Wait() +end + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local UIS = game:GetService("UserInputService") + +ReplicatedStorage:WaitForChild("Objects"):WaitForChild("MLLoaded") + +local blocksFolder = ReplicatedStorage:WaitForChild("Blocks") +local PM = require(ReplicatedStorage.Shared.PlacementManager) + +local HOTBAR_SIZE = 9 +local hotbar = table.create(HOTBAR_SIZE) +local selectedSlot = 1 + +local keyToSlot = { + [Enum.KeyCode.One] = 1, + [Enum.KeyCode.Two] = 2, + [Enum.KeyCode.Three] = 3, + [Enum.KeyCode.Four] = 4, + [Enum.KeyCode.Five] = 5, + [Enum.KeyCode.Six] = 6, + [Enum.KeyCode.Seven] = 7, + [Enum.KeyCode.Eight] = 8, + [Enum.KeyCode.Nine] = 9, +} + +local function rebuildHotbar() + local ids = {} + for _, block in ipairs(blocksFolder:GetChildren()) do + local id = block:GetAttribute("n") + if id ~= nil then + table.insert(ids, tostring(id)) + end + end + + table.sort(ids) + for i = 1, HOTBAR_SIZE do + hotbar[i] = ids[i] or "" + end + selectedSlot = math.clamp(selectedSlot, 1, HOTBAR_SIZE) +end + +local function getSelectedBlockId(): string? + local id = hotbar[selectedSlot] + if id == "" then + return nil + end + return id +end + +local function setSelectedSlot(slot: number) + if slot < 1 or slot > HOTBAR_SIZE then + return + end + selectedSlot = slot +end + +rebuildHotbar() +blocksFolder.ChildAdded:Connect(rebuildHotbar) +blocksFolder.ChildRemoved:Connect(rebuildHotbar) + +UIS.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean) + if gameProcessedEvent then + return + end + + local slot = keyToSlot[input.KeyCode] + if slot then + setSelectedSlot(slot) + return + end + + if input.UserInputType == Enum.UserInputType.MouseButton1 then + local mouseBlock = PM:GetBlockAtMouse() + if not mouseBlock then + return + end + PM:BreakBlock(mouseBlock.chunk.X, mouseBlock.chunk.Y, mouseBlock.chunk.Z, mouseBlock.block.X, mouseBlock.block.Y, mouseBlock.block.Z) + elseif input.UserInputType == Enum.UserInputType.MouseButton2 then + local mouseBlock = PM:GetPlacementAtMouse() + if not mouseBlock then + return + end + local blockId = getSelectedBlockId() + if not blockId then + return + end + PM:PlaceBlock(mouseBlock.chunk.X, mouseBlock.chunk.Y, mouseBlock.chunk.Z, mouseBlock.block.X, mouseBlock.block.Y, mouseBlock.block.Z, blockId) + end +end) diff --git a/src/StarterPlayer/StarterPlayerScripts/Actor/init.meta.json b/src/StarterPlayer/StarterPlayerScripts/Actor/init.meta.json new file mode 100644 index 0000000..6af4553 --- /dev/null +++ b/src/StarterPlayer/StarterPlayerScripts/Actor/init.meta.json @@ -0,0 +1,4 @@ +{ + "className": "Actor", + "ignoreUnknownInstances": true +} \ No newline at end of file diff --git a/src/Workspace/mods/init.meta.json b/src/Workspace/mods/init.meta.json new file mode 100644 index 0000000..1025b06 --- /dev/null +++ b/src/Workspace/mods/init.meta.json @@ -0,0 +1,3 @@ +{ + "ignoreUnknownInstances": true +} \ No newline at end of file diff --git a/src/Workspace/mods/mc/init.lua b/src/Workspace/mods/mc/init.lua new file mode 100644 index 0000000..100fdc8 --- /dev/null +++ b/src/Workspace/mods/mc/init.lua @@ -0,0 +1,23 @@ +local mod = { + name = "Blockscraft", + description = "Base Blockscraft blocks", + ns = "mc", + author = { "ocbwoy3" } +} + +local rep = game:GetService("ReplicatedStorage"):WaitForChild("Blocks") +local upd = game:GetService("ReplicatedStorage"):WaitForChild("BlockUpdateOperations") + +function mod.init() + for a,b in pairs(script.blocks:GetChildren()) do + local upop = b:FindFirstChild("BlockUpdateOperation") + if upop then + upop.Name = b:GetAttribute("n") + upop:SetAttribute("n",b:GetAttribute("n")) + upop.Parent = upd + end + b:Clone().Parent = rep + end +end + +return mod \ No newline at end of file diff --git a/src/Workspace/mods/mc/init.meta.json b/src/Workspace/mods/mc/init.meta.json new file mode 100644 index 0000000..1025b06 --- /dev/null +++ b/src/Workspace/mods/mc/init.meta.json @@ -0,0 +1,3 @@ +{ + "ignoreUnknownInstances": true +} \ No newline at end of file diff --git a/src/Workspace/mods/mc/upds/BlockUpdateOperation.lua b/src/Workspace/mods/mc/upds/BlockUpdateOperation.lua new file mode 100644 index 0000000..7f0f2f4 --- /dev/null +++ b/src/Workspace/mods/mc/upds/BlockUpdateOperation.lua @@ -0,0 +1,3 @@ +return function(p: typeof(script.Parent.Parent.blocks["mc:grass_block"])) + p.block.Grass.Color = Color3.new(p:GetAttribute("x"),p:GetAttribute("x"),p:GetAttribute("x")) +end \ No newline at end of file diff --git a/src/Workspace/mods/mc/upds/init.meta.json b/src/Workspace/mods/mc/upds/init.meta.json new file mode 100644 index 0000000..1025b06 --- /dev/null +++ b/src/Workspace/mods/mc/upds/init.meta.json @@ -0,0 +1,3 @@ +{ + "ignoreUnknownInstances": true +} \ No newline at end of file