local ChunksUtil = {}

local ReplicatedStorage = game:GetService('ReplicatedStorage')

local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)

local CHUNK_SIZE: number = ChunkSettings['CHUNK_SIZE']
local BLOCK_SIZE: number = ChunkSettings['BLOCK_SIZE']

-- PUBLIC

-- Converts [chunk_x][chunk_z][x][z][y] position to a Vector3(x, y, z)
function ChunksUtil.chunkToWorldPosition(chunkX: number, chunkZ: number, chunkY: number, x: number, z: number, y: number): Vector3
	local worldX = (chunkX * CHUNK_SIZE + x) * BLOCK_SIZE
	local worldZ = (chunkZ * CHUNK_SIZE + z) * BLOCK_SIZE
	local worldY = (chunkY * CHUNK_SIZE + y) * BLOCK_SIZE
	return Vector3.new(worldX, worldY, worldZ)
end

-- Returns the buffer position of blockId from relative chunk position
function ChunksUtil.chunkToBufferPosition(x: number, z: number, y: number): number
	return (x - 1) * CHUNK_SIZE + (z - 1) * CHUNK_SIZE * CHUNK_SIZE + (y - 1)
end

-- Converts Vector3(x, y, z) position to a [chunk_x][chunk_z][x][z][y]
function ChunksUtil.worldToChunkPosition(coordinate : Vector3): {number}
	local chunkX =  ((coordinate.X - BLOCK_SIZE) / BLOCK_SIZE) // CHUNK_SIZE
	local chunkZ = ((coordinate.Z - BLOCK_SIZE) / BLOCK_SIZE) // CHUNK_SIZE
	local chunkY = ((coordinate.Y - BLOCK_SIZE) / BLOCK_SIZE) // CHUNK_SIZE
	local x = coordinate.X / BLOCK_SIZE - (chunkX * CHUNK_SIZE)
	local z = coordinate.Z / BLOCK_SIZE - (chunkZ * CHUNK_SIZE)
	local y = coordinate.Y / BLOCK_SIZE - (chunkY * CHUNK_SIZE)
	return {chunkX, chunkZ, chunkY, x, z, y}
end

-- Returns (-1, 1)
function ChunksUtil.simpleNoise(x: number, y: number, seed: number): number
	return math.noise(x / 20, y / 20, seed)
end

--[[
	Octaves = 2,
	Lacunarity = 5, -- > 1
	Persistence = .2, -- > 0, < 1
	Scale = 40,
]]
function ChunksUtil.continentalnessNoise(x: number, y: number, seed: number): number
	local value = 0
	local amplitude = 1
	local x1 = x
	local y1 = y

	for i = 1, 2, 1 do
		value += math.noise(x1 / 160, y1 / 160, seed) * amplitude

		y1 *= 5
		x1 *= 5

		amplitude *= .2
	end
	return value
end

--[[
	Octaves = 2,
	Lacunarity = 4, -- > 1
	Persistence = .2, -- > 0, < 1
	Scale = 50,
]]
function ChunksUtil.erosionNoise(x: number, y: number, seed: number): number
	local value = 0
	local amplitude = 1
	local x1 = x
	local y1 = y

	for i = 1, 2, 1 do
		value += math.noise(x1 / 200, y1 / 200, seed) * amplitude

		y1 *= 4
		x1 *= 4

		amplitude *= .2
	end
	return value
end

function ChunksUtil.peaksAndValleysNoise(x: number, y: number, seed: number): number
	local value = 0
	local amplitude = 1
	local x1 = x
	local y1 = y

	for i = 1, 2, 1 do
		value += math.noise(x1 / 100, y1 / 100, seed) * amplitude

		y1 *= 4
		x1 *= 4

		amplitude *= .2
	end
	return value
end

-- Fills in table contents with chunkxz + xyz indices
function ChunksUtil.fillChunksTable(tbl: {}, ...): ()
	local data = {...}

	while #data > 0 do
		if not tbl[data[1]] then
			tbl[data[1]] = {}
		end
		tbl = tbl[data[1]]
		table.remove(data, 1)
	end
end

return ChunksUtil


local itemsData = {
	Air = {
		isTransparent = true,
	},

	Water = {
		isTransparent = true,
	},

	Stone = {
		Hardness = 20,
		PreferredTool = 'Pickaxe',
		DropItem = 'Cobblestone',
		Type = 'Block',
	},

	Cobblestone = {
		Hardness = 20,
		PreferredTool = 'Pickaxe',
		Type = 'Block',
	},

	['Grass Block'] = {
		Hardness = 6,
		PreferredTool = 'Shovel',
		DropItem = 'Dirt',
		Type = 'Block',
	},

	Dirt = {
		Hardness = 5,
		PreferredTool = 'Shovel',
		Type = 'Block',
	},

	['Oak Wood Planks'] = {
		Hardness = 20,
		PreferredTool = 'Axe',
		Type = 'Block',
	},

	Bedrock = {},

	Sand = {
		Hardness = 8,
		PreferredTool = 'Shovel',
		Type = 'Block',
	},

	['Oak Log'] = {
		Hardness = 20,
		PreferredTool = 'Axe',
		Type = 'Block',
	},

	['Oak Leaves'] = {
		Hardness = 2,
		Type = 'Block',
		isTransparent = true,
	},

	Glass = {
		isTransparent = true,
	},

	['Coal Ore'] = {
		Hardness = 30,
		PreferredTool = 'Pickaxe',
		DropItem = 'Coal',
		Type = 'Block',
	},

	['Gold Ore'] = {
		Hardness = 30,
		PreferredTool = 'Pickaxe',
		DropItem = 'Raw Gold',
		Type = 'Block',
	},

	['Iron Ore'] = {
		Hardness = 30,
		PreferredTool = 'Pickaxe',
		DropItem = 'Raw Iron',
		Type = 'Block',
	},

	['Diamond Ore'] = {
		Hardness = 30,
		PreferredTool = 'Pickaxe',
		DropItem = 'Diamond',
		Type = 'Block',
	},

	['Wooden Pickaxe'] = {
		Type = 'Pickaxe',
	},

	Dandelion = {
		Hardness = 0,
		Type = 'Plant',
	},

	Grass = {
		Hardness = 0,
		Type = 'Plant',
	},

	['Oxeye Daisy'] = {
		Hardness = 0,
		Type = 'Plant',
	},

	Poppy = {
		Hardness = 0,
		Type = 'Plant',
	},

	Clay = {
		Hardness = 6,
		PreferredTool = 'Shovel',
		Type = 'Block',
	},

	Gravel = {
		Hardness = 6,
		PreferredTool = 'Shovel',
		Type = 'Block',
	},

	['Raw Iron'] = {
		Type = 'Ore'
	},

	['Diamond'] = {
		Type = 'Ore'
	},

	['Coal'] = {
		Type = 'Ore'
	},

	['Iron Ingot'] = {
		Type = 'Ore'
	},

	['Wooden Sword'] = {
		Type = 'Sword',
	},

	['Stone Sword'] = {
		Type = 'Sword',
	},

	['Iron Sword'] = {
		Type = 'Sword',
	},

	['Diamond Sword'] = {
		Type = 'Sword',
	},

	['Stone Pickaxe'] = {
		Type = 'Pickaxe',
	},

	['Iron Pickaxe'] = {
		Type = 'Pickaxe',
	},

	['Diamond Pickaxe'] = {
		Type = 'Pickaxe',
	},

	['Wooden Axe'] = {
		Type = 'Axe',
	},

	['Stone Axe'] = {
		Type = 'Axe',
	},

	['Iron Axe'] = {
		Type = 'Axe',
	},

	['Diamond Axe'] = {
		Type = 'Axe',
	},

	['Wooden Shovel'] = {
		Type = 'Shovel',
	},

	['Stone Shovel'] = {
		Type = 'Shovel',
	},

	['Iron Shovel'] = {
		Type = 'Shovel',
	},

	['Diamond Shovel'] = {
		Type = 'Shovel',
	},
}

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local ItemsIDs = require(ReplicatedStorage.Shared.ItemsIDs)

function _init()

	for itemID, itemName in ipairs(ItemsIDs) do

		if not itemsData[itemName] then
			itemsData[itemName] = {}
		end

		itemsData[itemName]['ID'] = itemID
	end
end

-- Init
_init()

return itemsData


return {

	'Air',
	'Water',

	-- Everyday blocks
	'Dirt',
	'Grass Block',
	'Stone',
	'Cobblestone',
	'Oak Wood Planks',
	'Oak Leaves',
	'Oak Log',
	'Bedrock',
	'Sand',
	'Glass',

	-- Ore blocks
	'Coal Ore',
	'Gold Ore',
	'Iron Ore',
	'Diamond Ore',

	-- Tools
	'Wooden Pickaxe',
	'Wooden Axe',
	'Wooden Shovel',
	'Wooden Sword',

	'Stone Pickaxe',
	'Stone Axe',
	'Stone Shovel',
	'Stone Sword',

	'Iron Pickaxe',
	'Iron Axe',
	'Iron Shovel',
	'Iron Sword',

	'Diamond Pickaxe',
	'Diamond Axe',
	'Diamond Shovel',
	'Diamond Sword',

	'Coal',
	'Gold Ingot',
	'Raw Iron',
	'Iron Ingot',
	'Diamond',

	-- Flowers
	'Grass',
	'Dandelion',
	'Poppy',
	'Oxeye Daisy',

	-- Misc
	'Oak Sapling',
	'Apple'
}


--!native
local actor: Actor = script:GetActor()

local ReplicatedStorage = game:GetService('ReplicatedStorage')
local StarterPlayer = game:GetService('StarterPlayer')

local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)
local BiomeSelector = require(StarterPlayer.StarterPlayerScripts.Modules.Chunks.Loading.Terrain.Biomes.BiomeSelector)

local BLOCK_SIZE: number = ChunkSettings['BLOCK_SIZE']
local CHUNK_SIZE: number = ChunkSettings['CHUNK_SIZE']
local CHUNK_DISTANCE: number = ChunkSettings['CHUNK_DISTANCE']

local ChunkLoadedBiome: BindableEvent = StarterPlayer.StarterPlayerScripts.Events.ChunkLoadedBiome

-- FUNCTIONS

function loadChunk(chunkX: number, chunkZ: number, chunkY: number, chunkBlocks: buffer, neighbouringChunks: {}, SEED: number): ()
	local neighbouringChunksModifications = BiomeSelector.generateBiome(chunkX, chunkZ, chunkY, chunkBlocks, neighbouringChunks, SEED)

	ChunkLoadedBiome:Fire(chunkX, chunkZ, chunkY, chunkBlocks, neighbouringChunks)
	task.synchronize()
	actor:Destroy()
	script:Destroy()
end

actor:BindToMessageParallel('beginLoading', loadChunk)


--!native
local actor: Actor = script:GetActor()

local ReplicatedStorage = game:GetService('ReplicatedStorage')
local StarterPlayer = game:GetService('StarterPlayer')

local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)
local ItemsData = require(ReplicatedStorage.Shared.ItemsData)
local TerrainShapeGenerator = require(StarterPlayer.StarterPlayerScripts.Modules.Chunks.Loading.Terrain.TerrainShapeGenerator)

local BLOCK_SIZE: number = ChunkSettings['BLOCK_SIZE']
local CHUNK_SIZE: number = ChunkSettings['CHUNK_SIZE']
local CHUNK_DISTANCE: number = ChunkSettings['CHUNK_DISTANCE']

local AIR_ID: number = ItemsData['Air']['ID']

local ChunkLoadedTerrain: BindableEvent = StarterPlayer.StarterPlayerScripts.Events.ChunkLoadedTerrain

-- FUNCTIONS

function createAirBuffer(): buffer
	local chunkBlocks = buffer.create(CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE + 2) -- Offset of 2 when checking for neighbours
	buffer.fill(chunkBlocks, 0, AIR_ID)
	return chunkBlocks
end

function loadChunk(chunkX: number, chunkZ: number, chunkY: number, SEED: number): ()
	local chunkBlocks = createAirBuffer()
	TerrainShapeGenerator.generate(chunkX, chunkZ, chunkY, chunkBlocks, SEED)

	ChunkLoadedTerrain:Fire(chunkX, chunkZ, chunkY, chunkBlocks)
	task.synchronize()
	actor:Destroy()
	script:Destroy()
end

actor:BindToMessageParallel('beginLoading', loadChunk)


local Players = game:GetService('Players')
local Modules = Players.LocalPlayer.PlayerScripts:WaitForChild('Modules')

require(Modules.Chunks.ChunksManager)
--require(Modules.Lighting)

task.delay(.1, function()
	require(Modules.Break)
	require(Modules.CollisionsManager)
end)


local Break = {}

local Players = game:GetService('Players')
local ReplicatedStorage = game:GetService('ReplicatedStorage')
local UserInputService = game:GetService('UserInputService')

local Player: Player = Players.LocalPlayer
local PlayerScripts = Player.PlayerScripts

local Modules = PlayerScripts:WaitForChild('Modules')

local ChunksData = require(Modules.Chunks.Data.ChunksData)
local ChunkRenderer = require(Modules.Chunks.Rendering.ChunkRenderer)
local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)
local ChunksUtil = require(ReplicatedStorage.Shared.ChunksUtil)
local ItemsData = require(ReplicatedStorage.Shared.ItemsData)

local mouse: Mouse = Player:GetMouse()
local camera: Camera = workspace.CurrentCamera

local BLOCK_SIZE: number = ChunkSettings['BLOCK_SIZE']
local CHUNK_SIZE: number = ChunkSettings['CHUNK_SIZE']

local AIR_ID: number = ItemsData['Air']['ID']

-- PRIVATE

local function roundToNearest(value: number, snap: number): number
	return math.round(value / snap) * snap
end

local function handleInputBegan(input: InputObject, typing: boolean): ()
	if
		typing
		or input.UserInputType ~= Enum.UserInputType.MouseButton1
		or not Player.Character
	then
		return
	end

	local rayOrigin = camera.CFrame.Position
	local rayDirection = mouse.UnitRay.Direction

	local lastRoundedPosition = nil

	-- Keep raycasting with increments
	for i = 1, 150 do
		local location = rayOrigin + rayDirection * (i / 10)
		local roundedPosition = Vector3.new(
			roundToNearest(location.X, BLOCK_SIZE),
			roundToNearest(location.Y, BLOCK_SIZE),
			roundToNearest(location.Z, BLOCK_SIZE)
		)

		if roundedPosition == lastRoundedPosition then
			continue
		end

		local chunkPosition = ChunksUtil.worldToChunkPosition(roundedPosition)
		local chunkX = chunkPosition[1]
		local chunkZ = chunkPosition[2]
		local chunkY = chunkPosition[3]
		local x = chunkPosition[4]
		local z = chunkPosition[5]
		local y = math.clamp(chunkPosition[6], 0, CHUNK_SIZE)

		local bufferPosition = ChunksUtil.chunkToBufferPosition(x, z, y)

		local blockId = buffer.readu8(ChunksData[chunkX][chunkZ][chunkY]['buffer'], bufferPosition)

		if blockId ~= AIR_ID then
			buffer.writeu8(ChunksData[chunkX][chunkZ][chunkY]['buffer'], bufferPosition, AIR_ID)
			local toRerender = {{chunkX, chunkZ, chunkY}}

			if x == 1 then
				table.insert(toRerender, {chunkX - 1, chunkZ, chunkY})
			elseif x == CHUNK_SIZE then
				table.insert(toRerender, {chunkX + 1, chunkZ, chunkY})
			end
			if z == 1 then
				table.insert(toRerender, {chunkX, chunkZ - 1, chunkY})
			elseif z == CHUNK_SIZE then
				table.insert(toRerender, {chunkX, chunkZ + 1, chunkY})
			end
			if y == 1 then
				table.insert(toRerender, {chunkX, chunkZ, chunkY - 1})
			elseif y == CHUNK_SIZE then
				table.insert(toRerender, {chunkX, chunkZ, chunkY + 1})
			end

			for _, chunk in toRerender do
				ChunkRenderer.unrenderChunk(chunk[1], chunk[2], chunk[3])
				ChunkRenderer.renderChunk(chunk[1], chunk[2], chunk[3])
			end
			return
		end

		lastRoundedPosition = roundedPosition
	end
end

-- EVENTS

UserInputService.InputBegan:Connect(handleInputBegan)

return Break


--!native
local ChunkLoading = {}

local Players = game:GetService('Players')
local ReplicatedStorage = game:GetService('ReplicatedStorage')
local RunService = game:GetService('RunService')

local Player: Player = Players.LocalPlayer
local Character: Model = Player.Character or Player.CharacterAdded:Wait()
local HumanoidRootPart: Part = Character:WaitForChild('HumanoidRootPart') :: Part
local PlayerScripts = Player.PlayerScripts

local Modules = PlayerScripts:WaitForChild('Modules')
local Events = PlayerScripts:WaitForChild('Events')

local ChunkRenderer = require(Modules.Chunks.Rendering.ChunkRenderer)
local ChunksData = require(Modules.Chunks.Data.ChunksData)
local ChunkGenerationStages = require(Modules.Chunks.Data.ChunkGenerationStages)
local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)
local ChunksUtil = require(ReplicatedStorage.Shared.ChunksUtil)
local ItemsData = require(ReplicatedStorage.Shared.ItemsData)

local ParallelScripts = PlayerScripts:WaitForChild('ParallelScripts')

local BiomeLoaderParallel: LocalScript = ParallelScripts.BiomeLoaderParallel
local TerrainLoaderParallel: LocalScript = ParallelScripts.TerrainLoaderParallel

local ClientSettings = PlayerScripts:WaitForChild('ClientSettings')

local BLOCK_SIZE: number = ChunkSettings['BLOCK_SIZE']
local CHUNK_SIZE: number = ChunkSettings['CHUNK_SIZE']
local CHUNK_DISTANCE: number = ChunkSettings['CHUNK_DISTANCE']
local MAX_CHUNK_DISTANCE: number = ChunkSettings['MAX_CHUNK_DISTANCE']
local MAX_CHUNK_LOADING_ACTORS: number = ChunkSettings['MAX_CHUNK_LOADING_ACTORS']

local LOAD_OFFSET: number = 2

local CHUNKS_TO_RENDER_PER_UPDATE: number = 2
local CHUNKS_TO_LOAD_TERRAIN_PER_UPDATE: number = 7

local FRAMES_BETWEEN_RENDERS: number = 1
local FRAMES_BETWEEN_LOADS: number = 4

local AIR_ID: number = ItemsData['Air']['ID']
local SEED: number = 500

local framesSinceLastRender: number = 0
local framesSinceLastLoad: number = 0

local chunkLoadingActors: number = ClientSettings:GetAttribute('ChunkLoadingActors')
local chunkLoadingActorsHalf: number = chunkLoadingActors // 2 -- Amount of biome and terrain actors seperately

local terrainActors = table.create(chunkLoadingActorsHalf)
local biomeActors = table.create(chunkLoadingActorsHalf)

local ActorsFolder = Instance.new('Folder')
ActorsFolder.Name = 'Actors'
ActorsFolder.Parent = ParallelScripts

-- Create actors for loadChunk()
for i = 1, MAX_CHUNK_LOADING_ACTORS // 2 do
	local terrainActor = Instance.new('Actor')
	terrainActor.Name = 'TerrainActor' .. i
	local terrainScript = TerrainLoaderParallel:Clone()
	terrainScript.Parent = terrainActor
	terrainActor.Parent = ActorsFolder

	local biomeActor = Instance.new('Actor')
	biomeActor.Name = 'BiomeActor' .. i
	local biomeScript = BiomeLoaderParallel:Clone()
	biomeScript.Parent = biomeActor
	biomeActor.Parent = ActorsFolder

	table.insert(terrainActors, terrainActor)
	table.insert(biomeActors, biomeActor)
end

-- PRIVATE

local function onChunkLoadingActorsChanged(): ()
	local newValue = ClientSettings:GetAttribute('ChunkLoadingActors')
	ClientSettings:SetAttribute('ChunkLoadingActors', math.clamp(math.floor(newValue), 2, MAX_CHUNK_LOADING_ACTORS))
	newValue = ClientSettings:GetAttribute('ChunkLoadingActors')

	chunkLoadingActors = newValue
	chunkLoadingActorsHalf = newValue // 2
end

local function renderChunks(toRender: {}): ()
	local chunksRendered: number = 0
	for _, chunk in toRender do
		local chunkX = chunk[1]
		local chunkZ = chunk[2]
		local chunkY = chunk[3]

		ChunksData[chunkX][chunkZ][chunkY]['isRendered'] = true
		ChunkRenderer.renderChunk(chunkX, chunkZ, chunkY)

		chunksRendered += 1
		if chunksRendered > CHUNKS_TO_RENDER_PER_UPDATE then
			return
		end
	end
end

local function unrenderChunks(toUnrender: {}): ()
	local chunksUnrendered: number = 0
	for i = #toUnrender, 1, -1 do
		local chunk = toUnrender[i]
		local chunkX = chunk[1]
		local chunkZ = chunk[2]
		local chunkY = chunk[3]

		ChunkRenderer.unrenderChunk(chunkX, chunkZ, chunkY)
		ChunksData[chunkX][chunkZ][chunkY]['isRendered'] = false

		chunksUnrendered += 1
		if chunksUnrendered > CHUNKS_TO_RENDER_PER_UPDATE then
			return
		end
	end
end

-- Calls loadChunk() on every chunk to load
local function loadChunks(toLoad: {}): ()
	local chunksTerrainLoaded: number = 0

	local chunksToLoadTerrain = {}
	local chunksToLoadBiome = {}

	for _, chunk in toLoad do
		local chunkX: number = chunk[1]
		local chunkZ: number = chunk[2]
		local chunkY: number = chunk[3]

		if ChunksData[chunkX][chunkZ][chunkY]['stage'] == ChunkGenerationStages['none'] then
			if chunksTerrainLoaded > CHUNKS_TO_LOAD_TERRAIN_PER_UPDATE then
				continue
			end
			ChunksData[chunkX][chunkZ][chunkY]['isLoading'] = true

			table.insert(chunksToLoadTerrain, chunk)

			chunksTerrainLoaded += 1

		elseif ChunksData[chunkX][chunkZ][chunkY]['stage'] == ChunkGenerationStages['terrain'] then
			if -- Neighbours' terrain must be loaded
				not ChunksData[chunkX][chunkZ][chunkY - 1]
				or ChunksData[chunkX][chunkZ][chunkY - 1]['stage'] == ChunkGenerationStages['none']
			then
				continue
			end
			ChunksData[chunkX][chunkZ][chunkY]['isLoading'] = true

			table.insert(chunksToLoadBiome, chunk)
		end
	end

	task.defer(function()
		for _, chunk in chunksToLoadTerrain do
			local chunkX: number = chunk[1]
			local chunkZ: number = chunk[2]
			local chunkY: number = chunk[3]

			terrainActors[math.random(1, chunkLoadingActorsHalf)]:SendMessage('beginLoading', chunkX, chunkZ, chunkY, SEED)
		end

		for _, chunk in chunksToLoadBiome do
			local chunkX: number = chunk[1]
			local chunkZ: number = chunk[2]
			local chunkY: number = chunk[3]

			local chunkBlocks: buffer = ChunksData[chunkX][chunkZ][chunkY]['buffer']
			local neighbouringChunks = {
				[chunkX .. 'x' .. chunkZ .. 'x' .. chunkY - 1] = ChunksData[chunkX][chunkZ][chunkY - 1]['buffer']
			}

			biomeActors[math.random(1, chunkLoadingActorsHalf)]:SendMessage('beginLoading', chunkX, chunkZ, chunkY, chunkBlocks, neighbouringChunks, SEED)
		end
	end)
end

-- Returns chunks to render
local function findChunksToRender(): {}
	local characterPosition: Vector3 = HumanoidRootPart.Position
	local characterPositionChunkX: number = characterPosition.X // (CHUNK_SIZE * BLOCK_SIZE)
	local characterPositionChunkZ: number = characterPosition.Z // (CHUNK_SIZE * BLOCK_SIZE)
	local characterPositionChunkY: number = characterPosition.Y // (CHUNK_SIZE * BLOCK_SIZE)

	local toRender = {}
	for chunkX = characterPositionChunkX - CHUNK_DISTANCE, characterPositionChunkX + CHUNK_DISTANCE do
		local chunkXDistance = (chunkX - characterPositionChunkX) ^ 2
		for chunkZ = characterPositionChunkZ - CHUNK_DISTANCE, characterPositionChunkZ + CHUNK_DISTANCE do
			local chunkZDistance = (chunkZ - characterPositionChunkZ) ^ 2
			for chunkY = characterPositionChunkY - CHUNK_DISTANCE, characterPositionChunkY + CHUNK_DISTANCE do
				-- All neighbours must be fully loaded
				local distance = chunkXDistance + chunkZDistance + (chunkY - characterPositionChunkY) ^ 2
				if
					distance > CHUNK_DISTANCE * CHUNK_DISTANCE
					or not ChunksData[chunkX]
					or not ChunksData[chunkX][chunkZ]
					or not ChunksData[chunkX][chunkZ][chunkY]
					or ChunksData[chunkX][chunkZ][chunkY]['isRendered'] == true
					or ChunksData[chunkX][chunkZ][chunkY]['stage'] ~= ChunkGenerationStages['done']
					or not ChunksData[chunkX + 1]
					or not ChunksData[chunkX + 1][chunkZ]
					or not ChunksData[chunkX + 1][chunkZ][chunkY]
					or ChunksData[chunkX + 1][chunkZ][chunkY]['stage'] ~= ChunkGenerationStages['done']
					or not ChunksData[chunkX - 1]
					or not ChunksData[chunkX - 1][chunkZ]
					or not ChunksData[chunkX - 1][chunkZ][chunkY]
					or ChunksData[chunkX - 1][chunkZ][chunkY]['stage'] ~= ChunkGenerationStages['done']
					or not ChunksData[chunkX][chunkZ + 1]
					or not ChunksData[chunkX][chunkZ + 1][chunkY]
					or ChunksData[chunkX][chunkZ + 1][chunkY]['stage'] ~= ChunkGenerationStages['done']
					or not ChunksData[chunkX][chunkZ - 1]
					or not ChunksData[chunkX][chunkZ - 1][chunkY]
					or ChunksData[chunkX][chunkZ - 1][chunkY]['stage'] ~= ChunkGenerationStages['done']
					or not ChunksData[chunkX][chunkZ][chunkY + 1]
					or ChunksData[chunkX][chunkZ][chunkY + 1]['stage'] ~= ChunkGenerationStages['done']
					or not ChunksData[chunkX][chunkZ][chunkY - 1]
					or ChunksData[chunkX][chunkZ][chunkY - 1]['stage'] ~= ChunkGenerationStages['done']
				then
					continue
				end
				local isAllAir: boolean = true
				local chunkBlocks: buffer = ChunksData[chunkX][chunkZ][chunkY]['buffer']
				for i = 1, CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE do
					if buffer.readu8(chunkBlocks, i) ~= AIR_ID then
						isAllAir = false
						break
					end
				end
				if isAllAir then
					continue
				end

				table.insert(toRender, {chunkX, chunkZ, chunkY})
			end
		end
	end
	return toRender
end

-- Returns chunks to unrender
local function findChunksToUnrender(): {} --TODO not good has issues
	local characterPosition: Vector3 = HumanoidRootPart.Position
	local characterPositionChunkX: number = characterPosition.X // (CHUNK_SIZE * BLOCK_SIZE)
	local characterPositionChunkZ: number = characterPosition.Z // (CHUNK_SIZE * BLOCK_SIZE)
	local characterPositionChunkY: number = characterPosition.Y // (CHUNK_SIZE * BLOCK_SIZE)

	local safeChunks = {}

	local radius = CHUNK_DISTANCE + 1

	for chunkX = characterPositionChunkX - radius, characterPositionChunkX + radius do
		safeChunks[chunkX] = {}
		local chunkXDistance = (chunkX - characterPositionChunkX) ^ 2

		for chunkZ = characterPositionChunkZ - radius, characterPositionChunkZ + radius do
			safeChunks[chunkX][chunkZ] = {}
			local chunkZDistance = (chunkZ - characterPositionChunkZ) ^ 2

			for chunkY = characterPositionChunkY - radius, characterPositionChunkY + radius do
				local distance = chunkXDistance + chunkZDistance + (chunkY - characterPositionChunkY) ^ 2
				if distance <= radius * radius then
					safeChunks[chunkX][chunkZ][chunkY] = true
				end
			end
		end
	end

	local toUnrender = {}
	-- Find what loaded chunks aren't in shouldBeRendered
	for chunkX in ChunksData do
		for chunkZ in ChunksData[chunkX] do
			for chunkY in ChunksData[chunkX][chunkZ] do
				if not ChunksData[chunkX][chunkZ][chunkY]['isRendered'] then
					continue
				end
				if
					not safeChunks[chunkX]
					or not safeChunks[chunkX][chunkZ]
					or not safeChunks[chunkX][chunkZ][chunkY]
				then
					table.insert(toUnrender, {chunkX, chunkZ, chunkY})
				end
			end
		end
	end
	return toUnrender
end

-- Returns the closest chunks to the character
local function findChunksToLoad(): {}
	local characterPosition: Vector3 = HumanoidRootPart.Position
	local characterPositionChunkX: number = characterPosition.X // (CHUNK_SIZE * BLOCK_SIZE)
	local characterPositionChunkZ: number = characterPosition.Z // (CHUNK_SIZE * BLOCK_SIZE)
	local characterPositionChunkY: number = characterPosition.Y // (CHUNK_SIZE * BLOCK_SIZE)

	local toLoad = {}
	local radius = CHUNK_DISTANCE + LOAD_OFFSET

	for chunkX = characterPositionChunkX - radius, characterPositionChunkX + radius do
		local chunkXDistance = (chunkX - characterPositionChunkX) ^ 2
		for chunkZ = characterPositionChunkZ - radius, characterPositionChunkZ + radius do
			local chunkZDistance = (chunkZ - characterPositionChunkZ) ^ 2
			for chunkY = characterPositionChunkY - radius, characterPositionChunkY + radius do

				local distance = chunkXDistance + chunkZDistance + (chunkY - characterPositionChunkY) ^ 2
				if
					ChunksData[chunkX]
					and ChunksData[chunkX][chunkZ]
					and ChunksData[chunkX][chunkZ][chunkY]
				then
					local chunkData = ChunksData[chunkX][chunkZ][chunkY]
					if
						chunkData['stage'] == ChunkGenerationStages['done']
						or chunkData['isLoading']
						or (chunkData['stage'] == ChunkGenerationStages['terrain']
						and distance > (radius - 1) * (radius - 1))
					then
						continue
					end
				end

				if
					math.abs(chunkX) > MAX_CHUNK_DISTANCE
					or math.abs(chunkY) > MAX_CHUNK_DISTANCE
					or math.abs(chunkZ) > MAX_CHUNK_DISTANCE
					or distance > radius * radius
				then
					continue
				end

				table.insert(toLoad, {chunkX, chunkZ, chunkY})
				if
					not ChunksData[chunkX]
					or not ChunksData[chunkX][chunkZ]
					or not ChunksData[chunkX][chunkZ][chunkY]
				then
					ChunksUtil.fillChunksTable(ChunksData, chunkX, chunkZ, chunkY)
					ChunksData[chunkX][chunkZ][chunkY]['stage'] = ChunkGenerationStages['none']
				end
			end
		end
	end
	return toLoad
end

-- Returns chunks not within character's range
local function unloadChunks(chunksToLoad: {}): ()
	local characterPosition: Vector3 = HumanoidRootPart.Position
	local characterPositionChunkX: number = characterPosition.X // (CHUNK_SIZE * BLOCK_SIZE)
	local characterPositionChunkZ: number = characterPosition.Z // (CHUNK_SIZE * BLOCK_SIZE)
	local characterPositionChunkY: number = characterPosition.Y // (CHUNK_SIZE * BLOCK_SIZE)

	local shouldBeLoaded = {}

	-- Find what chunks should be loaded
	local radius = CHUNK_DISTANCE + LOAD_OFFSET + 1

	for chunkX = characterPositionChunkX - radius, characterPositionChunkX + radius do
		shouldBeLoaded[chunkX] = {}
		local chunkXDistance = (chunkX - characterPositionChunkX) ^ 2

		for chunkZ = characterPositionChunkZ - radius, characterPositionChunkZ + radius do
			shouldBeLoaded[chunkX][chunkZ] = {}
			local chunkZDistance = (chunkZ - characterPositionChunkZ) ^ 2

			for chunkY = characterPositionChunkY - radius, characterPositionChunkY + radius do
				local distance = chunkXDistance + chunkZDistance + (chunkY - characterPositionChunkY) ^ 2
				if distance <= radius * radius then
					shouldBeLoaded[chunkX][chunkZ][chunkY] = true
				end
			end
		end
	end

	-- Find what loaded chunks aren't in shouldBeLoaded
	for chunkX in ChunksData do
		for chunkZ in ChunksData[chunkX] do
			for chunkY in ChunksData[chunkX][chunkZ] do
				if
					not shouldBeLoaded[chunkX]
					or not shouldBeLoaded[chunkX][chunkZ]
					or not shouldBeLoaded[chunkX][chunkZ][chunkY]
				then
					ChunksData[chunkX][chunkZ][chunkY] = nil
				end
			end
		end
	end
end

-- Reorders table of chunks based on distance from player
local function reorderChunkPriority(chunks: {}): {}
	local characterPosition: Vector3 = HumanoidRootPart.Position
	local characterPositionChunkX: number = characterPosition.X // (CHUNK_SIZE * BLOCK_SIZE)
	local characterPositionChunkZ: number = characterPosition.Z // (CHUNK_SIZE * BLOCK_SIZE)
	local characterPositionChunkY: number = characterPosition.Y // (CHUNK_SIZE * BLOCK_SIZE)

	table.sort(chunks, function(a, b)
		local aDistance = (a[1] - characterPositionChunkX) ^ 2 + (a[2] - characterPositionChunkZ) ^ 2 + (a[3] - characterPositionChunkY) ^ 2
		local bDistance = (b[1] - characterPositionChunkX) ^ 2 + (b[2] - characterPositionChunkZ) ^ 2 + (b[3] - characterPositionChunkY) ^ 2
		return aDistance < bDistance
	end)
	return chunks
end

local function handleFrame(): ()
	framesSinceLastLoad += 1
	framesSinceLastRender += 1
	if not HumanoidRootPart then
		return
	end

	if framesSinceLastLoad >= FRAMES_BETWEEN_LOADS then
		local chunksToLoad = findChunksToLoad()
		reorderChunkPriority(chunksToLoad)
		--print(chunksToLoad)
		--[[local chunk = chunksToLoad[1]
		local x = chunk[1]
		local z = chunk[2]
		local y = chunk[3]

		print(x, z, y, ChunksData[x][z][y])]]
		loadChunks(chunksToLoad)

		unloadChunks()
		framesSinceLastLoad = 0
		return
	end

	if framesSinceLastRender >= FRAMES_BETWEEN_RENDERS then
		local chunksToRender = findChunksToRender()
		reorderChunkPriority(chunksToRender)
		renderChunks(chunksToRender)

		local ChunksToUnrender = findChunksToUnrender()
		reorderChunkPriority(ChunksToUnrender)
		unrenderChunks(ChunksToUnrender)
		framesSinceLastRender = 0
	end
end

local function handleChunkLoadedTerrain(chunkX: number, chunkZ: number, chunkY: number, chunkBlocks: buffer)
	ChunksData[chunkX][chunkZ][chunkY]['buffer'] = chunkBlocks
	ChunksData[chunkX][chunkZ][chunkY]['stage'] = ChunkGenerationStages['terrain']
	ChunksData[chunkX][chunkZ][chunkY]['isLoading'] = false
end

local function handleChunkLoadedBiome(chunkX: number, chunkZ: number, chunkY: number, chunkBlocks: buffer, neighbouringChunks: {})
	ChunksData[chunkX][chunkZ][chunkY]['buffer'] = chunkBlocks
	ChunksData[chunkX][chunkZ][chunkY]['stage'] = ChunkGenerationStages['done']
	ChunksData[chunkX][chunkZ][chunkY]['isLoading'] = false

	for neighbouringChunkString, neighbouringChunkNewBuffer  in neighbouringChunks do
		local neighbouringChunkPosition = string.split(neighbouringChunkString, 'x')
		local neighbouringChunkX = tonumber(neighbouringChunkPosition[1])
		local neighbouringChunkZ = tonumber(neighbouringChunkPosition[2])
		local neighbouringChunkY = tonumber(neighbouringChunkPosition[3])

		local neighbouringChunkOldBuffer = ChunksData[neighbouringChunkX][neighbouringChunkZ][neighbouringChunkY]['buffer']

		for x = 1, CHUNK_SIZE do
			local bufferPositionX: number = (x - 1) * CHUNK_SIZE
			for z = 1, CHUNK_SIZE do
				local bufferPositionZ: number = (z - 1) * CHUNK_SIZE * CHUNK_SIZE
				for y = 1, CHUNK_SIZE do
					local oldBlockId: number = buffer.readu8(neighbouringChunkOldBuffer, bufferPositionX + bufferPositionZ + (y - 1))
					local newBlockId: number = buffer.readu8(neighbouringChunkNewBuffer, bufferPositionX + bufferPositionZ + (y - 1))
					if oldBlockId == newBlockId then
						continue
					end
					buffer.writeu8(neighbouringChunkOldBuffer, bufferPositionX + bufferPositionZ + (y - 1), newBlockId)
				end
			end
		end
	end
end

-- EVENTS

RunService.Heartbeat:Connect(handleFrame)

Events.ChunkLoadedTerrain.Event:Connect(handleChunkLoadedTerrain)
Events.ChunkLoadedBiome.Event:Connect(handleChunkLoadedBiome)

ClientSettings:GetAttributeChangedSignal('ChunkLoadingActors'):Connect(onChunkLoadingActorsChanged)

return ChunkLoading


-- Represents what stage has been completed by a chunk
return {
	['none'] = 0,
	['terrain'] = 1,
	['biome'] = 2,
	['done'] = 3
}


--!native
local BedrockGeneration = {}

local ReplicatedStorage = game:GetService('ReplicatedStorage')

local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)
local ItemsData = require(ReplicatedStorage.Shared.ItemsData)

local CHUNK_SIZE: number = ChunkSettings['CHUNK_SIZE']
local MAX_CHUNK_DISTANCE: number = ChunkSettings['MAX_CHUNK_DISTANCE']

local BEDROCK_ID: number = ItemsData['Bedrock']['ID']

-- PUBLIC

function BedrockGeneration.generate(chunkBlocks: {}, chunkY: number, random: Random): ({}, Random)
	if chunkY ~= -MAX_CHUNK_DISTANCE then
		return
	end
	for x = 1, CHUNK_SIZE do
		for z = 1, CHUNK_SIZE do
			if random:NextInteger(1, 2) == 2 then
				chunkBlocks[x][z][CHUNK_SIZE - 1] = BEDROCK_ID
			else
				chunkBlocks[x][z][CHUNK_SIZE] = BEDROCK_ID
			end
		end
	end
	return chunkBlocks, random
end

return BedrockGeneration


local BiomeSelector = {}

local ReplicatedStorage = game:GetService('ReplicatedStorage')
local StarterPlayer = game:GetService('StarterPlayer')

local SplineMaps = require(StarterPlayer.StarterPlayerScripts.Modules.Chunks.Loading.Terrain.SplineMaps)
local BiomesData = require(StarterPlayer.StarterPlayerScripts.Modules.Chunks.Loading.Terrain.Biomes.BiomesData)
local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)
local ChunksUtil = require(ReplicatedStorage.Shared.ChunksUtil)
local ItemsData = require(ReplicatedStorage.Shared.ItemsData)

local CHUNK_SIZE = ChunkSettings['CHUNK_SIZE']

-- PRIVATE

function getBiomeGenerator(worldPositionX: number, worldPositionZ: number, chunkY: number, SEED: number): ModuleScript
	local temperature: number = math.clamp(ChunksUtil.simpleNoise(worldPositionX, worldPositionZ, SEED) * 2, -2, 2)
	local humidity: number = math.clamp(ChunksUtil.simpleNoise(worldPositionX, worldPositionZ, SEED + 1) * 2, -2, 2)

	return BiomesData[1].biomeGenerator
	-- Get surface level biome
	--[[for _, biomeData in BiomesData do
		if
			temperature < biomeData.temperature.min
			or temperature > biomeData.temperature.max
			or humidity < biomeData.humidity.min
			or humidity > biomeData.humidity.max
		then
			continue
		end
		return biomeData.biomeGenerator
	end]]
end

-- PUBLIC

function BiomeSelector.generateBiome(chunkX: number, chunkZ: number, chunkY: number, chunkBlocks: buffer, neighbouringChunks: {}, SEED: number): ()
	for x = 1, CHUNK_SIZE do
		local worldPositionX: number = chunkX * CHUNK_SIZE + x

		for z = 1, CHUNK_SIZE do
			local worldPositionZ: number = chunkZ * CHUNK_SIZE + z

			local biomeGenerator: ModuleScript = getBiomeGenerator(worldPositionX, worldPositionZ, chunkY, SEED)
			local neighbouringChunksModifications = biomeGenerator.generate(chunkBlocks, neighbouringChunks, chunkX, chunkZ, chunkY, x, z)
		end
	end
end

return BiomeSelector


local Players = game:GetService('Players')

local Player: Player = Players.LocalPlayer
local PlayerScripts = Player.PlayerScripts

local Modules = PlayerScripts:WaitForChild('Modules')

local ForestGenerator = require(Modules.Chunks.Loading.Terrain.Biomes.Forest.ForestGenerator)
local DesertGenerator = require(Modules.Chunks.Loading.Terrain.Biomes.Desert.DesertGenerator)
local JungleGenerator = require(Modules.Chunks.Loading.Terrain.Biomes.Jungle.JungleGenerator)
local TundraGenerator = require(Modules.Chunks.Loading.Terrain.Biomes.Tundra.TundraGenerator)

return {
	{
		temperature = {
			min = -1,
			max = 1
		},
		humidity = {
			min = -2,
			max = 0
		},
		biomeGenerator = ForestGenerator
	},
	{
		temperature = {
			min = 1,
			max = 2
		},
		humidity = {
			min = -2,
			max = 2
		},
		biomeGenerator = DesertGenerator
	},
	{
		temperature = {
			min = -1,
			max = 1
		},
		humidity = {
			min = 0,
			max = 2
		},
		biomeGenerator = JungleGenerator
	},
	{
		temperature = {
			min = -2,
			max = -1
		},
		humidity = {
			min = -2,
			max = 2
		},
		biomeGenerator = TundraGenerator
	},
}


local DesertGenerator = {}

function DesertGenerator.generate(chunkBlocks: buffer)

end

return DesertGenerator


local DecorationsGenerator = {}

local StarterPlayer = game:GetService("StarterPlayer")

local TreesGenerator = require(StarterPlayer.StarterPlayerScripts.Modules.Chunks.Loading.Terrain.Decorations.Trees.TreesGenerator)
--local DandelionGenerator = require(StarterPlayer.StarterPlayerScripts.Modules.Chunks.Loading.Terrain.Decorations.DandelionGenerator)
--local GrassGenerator = require(StarterPlayer.StarterPlayerScripts.Modules.Chunks.Loading.Terrain.Decorations.GrassGenerator)
--local PoppyGenerator = require(StarterPlayer.StarterPlayerScripts.Modules.Chunks.Loading.Terrain.Decorations.PoppyGenerator)
--local OxeyeDaisyGenerator = require(StarterPlayer.StarterPlayerScripts.Modules.Chunks.Loading.Terrain.Decorations.OxeyeDaisyGenerator)

-- PUBLIC

function DecorationsGenerator.generate(chunkBlocks: buffer, surfaceY: {}, SEED: number, chunkX: number, chunkZ: number): ({}, {})
	--local chunkKey = .5 * (chunkX + chunkZ) * (chunkX + chunkZ + 1) + chunkZ

	TreesGenerator.generate(chunkBlocks, surfaceY, chunkX, chunkZ)
	--GrassGenerator.generate(chunkBlocks, surfaceY, SEED, random, chunkX, chunkZ)
	--DandelionGenerator.generate(chunkBlocks, surfaceY, SEED, random, chunkX, chunkZ)
	--PoppyGenerator.generate(chunkBlocks, surfaceY, SEED, random, chunkX, chunkZ)
	--OxeyeDaisyGenerator.generate(chunkBlocks, surfaceY, SEED, random, chunkX, chunkZ)

	return chunkBlocks
end

return DecorationsGenerator


local Tree = {}

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local ItemsData = require(ReplicatedStorage.Shared.ItemsData)

local OAK_LOG_ID = ItemsData['Oak Log']['ID']
local OAK_LEAVES_ID = ItemsData['Oak Leaves']['ID']

local MIN_LOGS = 3
local MAX_LOGS = 5

local LOGS_INSIDE_TREE = 2

-- PRIVATE

-- Stores oak logs positions in table
function generateLogs(height: number): {}
	local tree = {}
	for y = 1, height do
		table.insert(tree, {0, 0, y, OAK_LOG_ID})
	end
	return tree
end

-- Stores oak leaves positions in table
function generateLeavesLayers(tree: {}, random: Random, height: number): {}
	-- Store first layer
	local layer = 1
	for x = -2, 2 do
		for z = -2, 2 do
			-- Skip center
			if x == 0 and z == 0 then
				continue
			end
			if math.abs(x) == 2 and math.abs(z) == 2 then
				if random:NextInteger(1, 4) <= 1 then
					continue
				end
			end
			table.insert(tree, {x, z, layer + height - LOGS_INSIDE_TREE, OAK_LEAVES_ID})
		end
	end
	layer += 1

	-- Store second layer
	for x = -2, 2 do
		for z = -2, 2 do
			if x == 0 and z == 0 then -- Skip center
				continue
			end
			if math.abs(x) == 2 and math.abs(z) == 2 then
				if random:NextInteger(1, 4) <= 1 then
					continue
				end
			end
			table.insert(tree, {x, z, layer + height - LOGS_INSIDE_TREE, OAK_LEAVES_ID})
		end
	end
	layer += 1

	-- Store third layer
	for x = -1, 1 do
		for z = -1, 1 do
			table.insert(tree, {x, z, layer + height - LOGS_INSIDE_TREE, OAK_LEAVES_ID})
		end
	end
	layer += 1

	-- Store fourth layer
	for x = -1, 1 do
		for z = -1, 1 do
			if math.abs(x) == 1 and math.abs(z) == 1 then
				continue
			end
			table.insert(tree, {x, z, layer + height - LOGS_INSIDE_TREE, OAK_LEAVES_ID})
		end
	end

	return tree
end

-- PUBLIC

function Tree.getRandomTree(random: Random): {}
	local height = random:NextInteger(MIN_LOGS, MAX_LOGS)
	return generateLeavesLayers(generateLogs(height), random, height)
end

return Tree


local TreeGeneration = {}

local ReplicatedStorage = game:GetService('ReplicatedStorage')
local StarterPlayer = game:GetService("StarterPlayer")

local Modules = StarterPlayer.StarterPlayerScripts.Modules

local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)
local ChunksUtil = require(ReplicatedStorage.Shared.ChunksUtil)
local Tree = require(Modules.Chunks.Loading.Terrain.Decorations.Trees.Tree)

local BLOCK_SIZE = ChunkSettings['BLOCK_SIZE']
local CHUNK_SIZE = ChunkSettings['CHUNK_SIZE']

local PERCENT_CHANCE_FOR_TREE = 2

-- PRIVATE

function createEmptyPreloadedBlocks(preloadedBlocks: {}, chunkX: number, chunkZ: number): ({number: {number: {number: {number: number}}}})

	if not preloadedBlocks[chunkX] then
		preloadedBlocks[chunkX] = {}
	end
	if not preloadedBlocks[chunkX][chunkZ] then
		preloadedBlocks[chunkX][chunkZ] = {}
	end

	for x = 1, CHUNK_SIZE do
		preloadedBlocks[chunkX][chunkZ][x] = {}

		for z = 1, CHUNK_SIZE do
			preloadedBlocks[chunkX][chunkZ][x][z] = {}
		end
	end

	return preloadedBlocks
end

-- PUBLIC

function TreeGeneration.generate(chunkBlocks: buffer, surfaceY: {}, random: Random, chunkX: number, chunkZ: number): ({}, {})
	for x = 1, CHUNK_SIZE do
		for z = 1, CHUNK_SIZE do
			for _, y in surfaceY[x][z] do

				local currentValue = random:NextInteger(0, 200)
				if currentValue > PERCENT_CHANCE_FOR_TREE then
					continue
				end

				local treeBlocks = Tree.getRandomTree(random)

				for _, blockData in treeBlocks do

					local relativeTreeX = blockData[1]
					local relativeTreeZ = blockData[2]
					local relativeTreeY = blockData[3]
					local blockID = blockData[4]

					local blockX = relativeTreeX + x
					local blockZ = relativeTreeZ + z
					local blockY = y + relativeTreeY

					local worldPosition = Vector3.new(
						(chunkX * CHUNK_SIZE + blockX) * BLOCK_SIZE,
						blockY * BLOCK_SIZE,
						(chunkZ * CHUNK_SIZE + blockZ) * BLOCK_SIZE
					)

					local chunkPosition = ChunksUtil.worldToChunkPosition(worldPosition)

					local leavesChunkX = chunkPosition[1]
					local leavesChunkZ = chunkPosition[2]
					local leavesX = chunkPosition[3]
					local leavesZ = chunkPosition[4]
					local leavesY = chunkPosition[5]

					if -- Block is NOT out of bounds
						leavesChunkX == chunkX
						and leavesChunkZ == chunkZ
					then
						chunkBlocks[blockX][blockZ][blockY] = blockID
						continue
					end

					-- Block IS out of bounds

					-- If chunk not in preloaded -> add chunk to preloaded
					--[[if
						not preloadedBlocks[leavesChunkX]
						or not preloadedBlocks[leavesChunkX][leavesChunkZ]
					then
						preloadedBlocks = createEmptyPreloadedBlocks(preloadedBlocks, leavesChunkX, leavesChunkZ)
					end

					preloadedBlocks[leavesChunkX][leavesChunkZ][leavesX][leavesZ][leavesY] = blockID]]
				end
			end
		end
	end

	return chunkBlocks
end

return TreeGeneration


local DandelionGeneration = {}

local ReplicatedStorage = game:GetService('ReplicatedStorage')

local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)
local ChunksUtil = require(ReplicatedStorage.Shared.ChunksUtil)
local ItemsData = require(ReplicatedStorage.Shared.ItemsData)

local CHUNK_SIZE = ChunkSettings['CHUNK_SIZE']

local AIR_ID = ItemsData['Air']['ID']
local DANDELION_ID = ItemsData['Dandelion']['ID']

-- PUBLIC

-- Returns chunk blocks table including dandelions
function DandelionGeneration.generate(chunkBlocks: {}, surfaceY: {}, SEED: number, random: Random, chunkX: number, chunkZ: number): ({})

	SEED += 1

	for x = 1, CHUNK_SIZE do
		for z = 1, CHUNK_SIZE do

			for _, y in surfaceY[x][z] do

				if chunkBlocks[x][z][y + 1] ~= AIR_ID then
					continue
				 end

				local xPos = chunkX * CHUNK_SIZE + x
				local zPos = chunkZ * CHUNK_SIZE + z

				local currentChance = ChunksUtil.simpleNoise(xPos, zPos, SEED) -- Returns [-1, 1]
				local currentValue = random:NextInteger(-45, 1) -- Controls frequency

				if currentChance < currentValue then
					chunkBlocks[x][z][y + 1] = DANDELION_ID
				end
			end
		end
	end

	return chunkBlocks
end

return DandelionGeneration


local GrassGeneration = {}

local ReplicatedStorage = game:GetService('ReplicatedStorage')

local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)
local ChunksUtil = require(ReplicatedStorage.Shared.ChunksUtil)
local ItemsData = require(ReplicatedStorage.Shared.ItemsData)

local CHUNK_SIZE = ChunkSettings['CHUNK_SIZE']

local AIR_ID = ItemsData['Air']['ID']
local GRASS_ID = ItemsData['Grass']['ID']

-- PUBLIC

function GrassGeneration.generate(chunkBlocks: {}, surfaceY: {}, SEED: number, random: Random, chunkX: number, chunkZ: number): ({})

	SEED += 2

	for x = 1, CHUNK_SIZE do
		for z = 1, CHUNK_SIZE do

			for _, y in surfaceY[x][z] do

				if chunkBlocks[x][z][y + 1] ~= AIR_ID then
					continue
				end

				local xPos = chunkX * CHUNK_SIZE + x
				local ZPos = chunkX * CHUNK_SIZE + z

				local currentChance = ChunksUtil.simpleNoise(xPos, ZPos, SEED) -- Returns [-1, 1]
				local currentValue = random:NextInteger(-8, 1) -- Controls frequency

				if currentChance < currentValue then
					chunkBlocks[x][z][y + 1] = GRASS_ID
				end
			end
		end
	end

	return chunkBlocks
end

return GrassGeneration


local OxeyeGeneration = {}

local ReplicatedStorage = game:GetService('ReplicatedStorage')

local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)
local ChunksUtil = require(ReplicatedStorage.Shared.ChunksUtil)
local ItemsData = require(ReplicatedStorage.Shared.ItemsData)

local CHUNK_SIZE = ChunkSettings['CHUNK_SIZE']

local AIR_ID = ItemsData['Air']['ID']
local OXEYE_DAISY_ID = ItemsData['Oxeye Daisy']['ID']

-- PUBLIC

-- Returns chunk blocks table including oxeye daisies
function OxeyeGeneration.generate(chunkBlocks: {}, surfaceY: {}, SEED: number, random: Random, chunkX: number, chunkZ: number): ({})

	SEED += 3

	for x = 1, CHUNK_SIZE do
		for z = 1, CHUNK_SIZE do

			for _, y in surfaceY[x][z] do

				if chunkBlocks[x][z][y + 1] ~= AIR_ID then
					continue
				end

				local xPos = chunkX * CHUNK_SIZE + x
				local zPos = chunkZ * CHUNK_SIZE + z

				local currentChance = ChunksUtil.simpleNoise(xPos, zPos, SEED) -- Returns [-1, 1]
				local currentValue = random:NextInteger(-40, 1) -- Controls frequency

				if currentChance < currentValue then
					chunkBlocks[x][z][y + 1] = OXEYE_DAISY_ID
				end
			end
		end
	end

	return chunkBlocks
end

return OxeyeGeneration


local PoppyGeneration = {}

local ReplicatedStorage = game:GetService('ReplicatedStorage')

local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)
local ChunksUtil = require(ReplicatedStorage.Shared.ChunksUtil)
local ItemsData = require(ReplicatedStorage.Shared.ItemsData)

local CHUNK_SIZE = ChunkSettings['CHUNK_SIZE']

local AIR_ID = ItemsData['Air']['ID']
local POPPY_ID = ItemsData['Poppy']['ID']

-- PUBLIC

-- Returns chunk blocks table including poppies
function PoppyGeneration.generate(chunkBlocks: {}, surfaceY: {}, SEED: number, random: Random, chunkX: number, chunkZ: number): ({})

	SEED += 4

	for x = 1, CHUNK_SIZE do
		for z = 1, CHUNK_SIZE do

			for _, y in surfaceY[x][z] do

				if chunkBlocks[x][z][y + 1] ~= AIR_ID then
					continue
				end

				local xPos = chunkX * CHUNK_SIZE + x
				local zPos = chunkZ * CHUNK_SIZE + z

				local currentChance = ChunksUtil.simpleNoise(xPos, zPos, SEED) -- Returns [-1, 1]
				local currentValue = random:NextInteger(-55, 1) -- Controls frequency

				if currentChance < currentValue then
					chunkBlocks[x][z][y + 1] = POPPY_ID
				end
			end
		end
	end

	return chunkBlocks
end

return PoppyGeneration


local ForestGenerator = {}

local Players = game:GetService('Players')
local ReplicatedStorage = game:GetService('ReplicatedStorage')

local Player: Player = Players.LocalPlayer
local PlayerScripts = Player.PlayerScripts

local Modules = PlayerScripts:WaitForChild('Modules')

local ChunksUtil = require(ReplicatedStorage.Shared.ChunksUtil)
local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)
local ItemsData = require(ReplicatedStorage.Shared.ItemsData)
local GrassLayerGenerator = require(Modules.Chunks.Loading.Terrain.Biomes.Forest.GrassLayerGenerator)
--local TreesGenerator = require(Modules.Chunks.Loading.Terrain.Biomes.Forest.Decorations.Trees.TreesGenerator)

local CHUNK_SIZE: number = ChunkSettings['CHUNK_SIZE']

local AIR_ID: number = ItemsData['Air']['ID']
local GRASS_BLOCK_ID: number = ItemsData['Grass Block']['ID']
local DIRT_ID: number = ItemsData['Dirt']['ID']

-- PUBLIC

function ForestGenerator.generate(chunkBlocks: buffer, neighbouringChunks: {}, chunkX: number, chunkZ: number, chunkY: number, x: number, z: number): {}
	GrassLayerGenerator.generate(chunkBlocks, neighbouringChunks, chunkX, chunkZ, chunkY, x, z)
	--TreesGenerator.generate(chunkBlocks, neighbouringChunks, neighbouringChunksModifications)
	return neighbouringChunks
	--[[if buffer.readu8(chunkBlocks, ChunksUtil.chunkToBufferPosition(x, z, y)) == AIR_ID then
		return
	end

	if y == CHUNK_SIZE then
		local aboveChunk: buffer = neighbouringChunks[chunkX .. chunkZ .. chunkY + 1]
		if buffer.readu8(aboveChunk, ChunksUtil.chunkToBufferPosition(x, z, 1)) == AIR_ID  then
			buffer.writeu8(chunkBlocks, ChunksUtil.chunkToBufferPosition(x, z, y), GRASS_BLOCK_ID)
		end
		return
	end

	if buffer.readu8(chunkBlocks, ChunksUtil.chunkToBufferPosition(x, z, y + 1)) == AIR_ID then
		buffer.writeu8(chunkBlocks, ChunksUtil.chunkToBufferPosition(x, z, y), GRASS_BLOCK_ID)

	elseif buffer.readu8(chunkBlocks, ChunksUtil.chunkToBufferPosition(x, z, y + 1)) == GRASS_BLOCK_ID then
		buffer.writeu8(chunkBlocks, ChunksUtil.chunkToBufferPosition(x, z, y), DIRT_ID)
	end]]
end

return ForestGenerator


local GrassLayerGenerator = {}

local ReplicatedStorage = game:GetService('ReplicatedStorage')

local ChunksUtil = require(ReplicatedStorage.Shared.ChunksUtil)
local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)
local ItemsData = require(ReplicatedStorage.Shared.ItemsData)

local CHUNK_SIZE: number = ChunkSettings['CHUNK_SIZE']

local AIR_ID: number = ItemsData['Air']['ID']
local GRASS_BLOCK_ID: number = ItemsData['Grass Block']['ID']
local DIRT_ID: number = ItemsData['Dirt']['ID']
local STONE_ID: number = ItemsData['Stone']['ID']

local MAX_DIRT: number = 3

-- PUBLIC

function GrassLayerGenerator.generate(chunkBlocks: buffer, neighbouringChunks: {}, chunkX: number, chunkZ: number, chunkY: number, x: number, z: number): ()
	-- Skip air and dirt blocks.
	local didPlaceGrassBlock: boolean = false
	local dirtsPlaced: number = 0

	for y = 1, CHUNK_SIZE - 1 do
		if buffer.readu8(chunkBlocks, ChunksUtil.chunkToBufferPosition(x, z, y)) ~= STONE_ID then
			didPlaceGrassBlock = false
			dirtsPlaced = 0
			continue
		end

		if didPlaceGrassBlock then
			dirtsPlaced += 1
			buffer.writeu8(chunkBlocks, ChunksUtil.chunkToBufferPosition(x, z, y), DIRT_ID)
		else
			if buffer.readu8(chunkBlocks, ChunksUtil.chunkToBufferPosition(x, z, y + 1)) == AIR_ID then
				didPlaceGrassBlock = true
				buffer.writeu8(chunkBlocks, ChunksUtil.chunkToBufferPosition(x, z, y), GRASS_BLOCK_ID)
			end
		end

		if dirtsPlaced >= MAX_DIRT then
			didPlaceGrassBlock = false
			dirtsPlaced = 0
		end
	end

	--local chunkBelowBlocks: buffer = neighbouringChunks[chunkX .. 'x' .. chunkZ .. 'x' .. chunkY - 1]
	--buffer.writeu8(chunkBelowBlocks, ChunksUtil.chunkToBufferPosition(x, z, 5), DIRT_ID)

	-- If bottom blockId == Air -> add grass + dirt sequence in below chunk
	if buffer.readu8(chunkBlocks, ChunksUtil.chunkToBufferPosition(x, z, 1)) == AIR_ID then
		local chunkBelowBlocks: buffer = neighbouringChunks[chunkX .. 'x' .. chunkZ .. 'x' .. chunkY - 1]
		if buffer.readu8(chunkBelowBlocks, ChunksUtil.chunkToBufferPosition(x, z, CHUNK_SIZE), AIR_ID) then
			return
		end
		buffer.writeu8(chunkBelowBlocks, ChunksUtil.chunkToBufferPosition(x, z, CHUNK_SIZE), GRASS_BLOCK_ID)

		for y = 1, MAX_DIRT do
			buffer.writeu8(chunkBelowBlocks, ChunksUtil.chunkToBufferPosition(x, z, CHUNK_SIZE - y), DIRT_ID)
		end
	end
end

return GrassLayerGenerator


local JungleGenerator = {}

function JungleGenerator.generate(chunkBlocks: buffer)

end

return JungleGenerator


local TundraGenerator = {}

function TundraGenerator.generate(chunkBlocks: buffer)

end

return TundraGenerator


--!native
local TerrainShapeGenerator = {}

local ReplicatedStorage = game:GetService('ReplicatedStorage')
local Players = game:GetService('Players')

local Player: Player = Players.LocalPlayer
local PlayerScripts = Player.PlayerScripts

local Modules = PlayerScripts:WaitForChild('Modules')

local SplineMaps = require(Modules.Chunks.Loading.Terrain.SplineMaps)
local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)
local ChunksUtil = require(ReplicatedStorage.Shared.ChunksUtil)
local ItemsData = require(ReplicatedStorage.Shared.ItemsData)

local CHUNK_SIZE: number = ChunkSettings['CHUNK_SIZE']

local STONE_ID: number = ItemsData['Stone']['ID']

local squishFactor: number = 20

-- PUBLIC

function TerrainShapeGenerator.generate(chunkX: number, chunkZ: number, chunkY: number, chunkBlocks: buffer, SEED: number): (buffer)
	local baseHeights = {}
	for x = 1, CHUNK_SIZE do
		local worldPositionX: number = chunkX * CHUNK_SIZE + x
		local bufferPositionX: number = (x - 1) * CHUNK_SIZE
		baseHeights[x] = {}

		for z = 1, CHUNK_SIZE do
			local worldPositionZ: number = chunkZ * CHUNK_SIZE + z
			local bufferPositionZ: number = (z - 1) * CHUNK_SIZE * CHUNK_SIZE

			-- maybe use the more complex noise function with diff parameters for each noise
			local continentalnessPerlin: number = ChunksUtil.continentalnessNoise(worldPositionX, worldPositionZ, SEED - 1)
			local erosionPerlin: number = ChunksUtil.erosionNoise(worldPositionX, worldPositionZ, SEED)
			local peaksAndValleysPerlin: number = ChunksUtil.peaksAndValleysNoise(worldPositionX, worldPositionZ, SEED + 1)

			local continentalnessHeightOffset: number = SplineMaps.getContinentalnessTerrainHeightOffset(continentalnessPerlin)
			local erosionHeightOffset: number = SplineMaps.getErosionTerrainHeightOffset(erosionPerlin)
			local peaksAndValleysHeightOffset: number = SplineMaps.getPeaksAndValleysTerrainHeightOffset(peaksAndValleysPerlin)

			--local continentalnessSquashFactor = SplineMaps.getContinentalnessSquashFactor(continentalnessPerlin)
			--local erosionSquashFactor = SplineMaps.getErosionSquashFactor(erosionPerlin)
			--local peaksAndValleysSquashFactor = SplineMaps.getPeaksAndValleysSquashFactor(peaksAndValleysPerlin)

			local baseHeight: number = continentalnessHeightOffset - erosionHeightOffset + peaksAndValleysHeightOffset
			baseHeights[x][z] = baseHeight

			for y = CHUNK_SIZE, 1, -1 do
				local worldPositionY: number = chunkY * CHUNK_SIZE + y

				local density = math.noise(worldPositionX / 20, worldPositionZ / 20, worldPositionY / 20)
				local densityModifier: number = (baseHeight - worldPositionY) / squishFactor

				if density + densityModifier > 0 then
					buffer.writeu8(chunkBlocks, bufferPositionX + bufferPositionZ + (y - 1), STONE_ID)
				end
			end
		end
	end
	return chunkBlocks
end

return TerrainShapeGenerator


local ChunkRenderer = {}

-- 3333 max blocks, if face culling implemented
-- = 8 * 8 * 52 = 3328 blocks
--local buffer = buffer.create(12288)

local AssetService = game:GetService("AssetService")
local Atlas = AssetService:CreateEditableImageAsync("rbxassetid://17400483416")
local ReplicatedStorage = game:GetService('ReplicatedStorage')
local Players = game:GetService('Players')

local Player: Player = Players.LocalPlayer
local PlayerScripts = Player.PlayerScripts

local Modules = PlayerScripts:WaitForChild('Modules')

local ChunksData = require(Modules.Chunks.Data.ChunksData)
local BlockMeshData = require(Modules.Chunks.Rendering.Data.BlockMeshData)
local PlantMeshData = require(Modules.Chunks.Rendering.Data.PlantMeshData)
local AtlasTextureData = require(Modules.Chunks.Rendering.Data.AtlasTextureData)
local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)
local ChunksUtil = require(ReplicatedStorage.Shared.ChunksUtil)
local ItemsData = require(ReplicatedStorage.Shared.ItemsData)

local CHUNK_SIZE: number = ChunkSettings['CHUNK_SIZE']
local BLOCK_SIZE: number = ChunkSettings['BLOCK_SIZE']

local AIR_ID: number = ItemsData['Air']['ID']

local ATLAS_SIZE: number = 8

local faceOrder = {
	{BlockMeshData.frontVertices, BlockMeshData.frontIndices},
	{BlockMeshData.backVertices, BlockMeshData.backIndices},
	{BlockMeshData.rightVertices, BlockMeshData.rightIndices},
	{BlockMeshData.leftVertices, BlockMeshData.leftIndices},
	{BlockMeshData.topVertices, BlockMeshData.topIndices},
	{BlockMeshData.bottomVertices, BlockMeshData.bottomIndices}
}

local BLOCK_NEIGHBOUR_VECTORS =
	{
		Vector3.new(0, 0, BLOCK_SIZE),
		Vector3.new(0, 0, -BLOCK_SIZE),
		Vector3.new(BLOCK_SIZE, 0, 0),
		Vector3.new(-BLOCK_SIZE, 0, 0),
		Vector3.new(0, BLOCK_SIZE, 0),
		Vector3.new(0, -BLOCK_SIZE, 0),
	}

-- PRIVATE

local function createFace(EditableMesh: EditableMesh, faceVertices, faceIndices, blockId, faceId, x, z, y)
	local vertices = {}
	for _, vertexOffset in faceVertices do
		local vertexPosition = Vector3.new(x + vertexOffset.X, y + vertexOffset.Y, z + vertexOffset.Z)
		local vertexId = EditableMesh:AddVertex(vertexPosition)
		table.insert(vertices, vertexId)
	end
	for i = 1, 4, 3 do
		local vertexId0Position = faceIndices[i]
		local vertexId1Position = faceIndices[i + 1]
		local vertexId2Position = faceIndices[i + 2]
		EditableMesh:AddTriangle(vertices[vertexId0Position], vertices[vertexId1Position], vertices[vertexId2Position])
	end

	-- Find face's texture in atlas -> set vertice's UVs to texture's corner
	local texturePosition = AtlasTextureData[blockId][faceId]
	local x = texturePosition % ATLAS_SIZE - 1
	local y = texturePosition // ATLAS_SIZE
	EditableMesh:SetUV(vertices[1], Vector2.new(x / ATLAS_SIZE, y / ATLAS_SIZE)) -- Top left of image
	EditableMesh:SetUV(vertices[2], Vector2.new((x + 1) / ATLAS_SIZE, y / ATLAS_SIZE)) -- Top right of image
	EditableMesh:SetUV(vertices[3], Vector2.new(x / ATLAS_SIZE, (y + 1) / ATLAS_SIZE)) -- Bottom left of image
	EditableMesh:SetUV(vertices[4], Vector2.new((x + 1) / ATLAS_SIZE, (y + 1) / ATLAS_SIZE) ) --  Bottom right of image
end

local function getShouldSidesRender(chunkX: number, chunkZ: number, chunkY: number, x: number, z: number, y: number): {}
	local shouldSidesRender = {false, false, false, false, false, false}
	local worldPosition = ChunksUtil.chunkToWorldPosition(chunkX, chunkZ, chunkY, x, z, y)

	for i, offsetVector in BLOCK_NEIGHBOUR_VECTORS do
		local neighborWorldPosition = Vector3.new(
			worldPosition.X + offsetVector.X,
			worldPosition.Y + offsetVector.Y,
			worldPosition.Z + offsetVector.Z
		)

		local neighborChunkPosition = ChunksUtil.worldToChunkPosition(neighborWorldPosition)
		local neighborChunkX = neighborChunkPosition[1]
		local neighborChunkZ = neighborChunkPosition[2]
		local neighborChunkY = neighborChunkPosition[3]
		local neighborX = neighborChunkPosition[4]
		local neighborZ = neighborChunkPosition[5]
		local neighborY = neighborChunkPosition[6]

		local bufferPosition = ChunksUtil.chunkToBufferPosition(neighborX, neighborZ, neighborY)

		shouldSidesRender[i] = buffer.readu8(ChunksData[neighborChunkX][neighborChunkZ][neighborChunkY]['buffer'], bufferPosition) == AIR_ID
	end
	return shouldSidesRender
end

-- PUBLIC

function ChunkRenderer.renderChunk(chunkX: number, chunkZ: number, chunkY: number): ()
	local MeshPart = Instance.new('MeshPart')
	MeshPart.Name = `{chunkX}x{chunkZ}x{chunkY}`
	MeshPart.Size = Vector3.new(BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
	MeshPart.CanCollide = false
	MeshPart.Anchored = true

	local EditableMesh = Instance.new('EditableMesh')
	local EditableImage = Atlas:Clone()

	local loadedChunkBlocks: buffer = ChunksData[chunkX][chunkZ][chunkY]['buffer']

	for x = 1, CHUNK_SIZE do
		local bufferPositionX = (x - 1) * CHUNK_SIZE

		for z = 1, CHUNK_SIZE do
			local bufferPositionZ = (z - 1) * CHUNK_SIZE * CHUNK_SIZE

			for y = 1, CHUNK_SIZE do
				local blockId = buffer.readu8(loadedChunkBlocks, bufferPositionX + bufferPositionZ + (y - 1))
				if
					not blockId
					or blockId == AIR_ID
					or not AtlasTextureData[blockId]
				then
					continue
				end

				local shouldSidesRender: {} = getShouldSidesRender(chunkX, chunkZ, chunkY, x, z, y)
				for faceId, shouldRender in shouldSidesRender do
					if not shouldRender then
						continue
					end

					local faceVertices = faceOrder[faceId][1]
					local faceIndices = faceOrder[faceId][2]

					createFace(EditableMesh, faceVertices, faceIndices, blockId, faceId, x, z, y)
				end
			end
		end
	end

	EditableMesh.Parent = MeshPart
	EditableImage.Parent = MeshPart
	MeshPart.Position = Vector3.new(
		chunkX * CHUNK_SIZE * BLOCK_SIZE,
		chunkY * CHUNK_SIZE * BLOCK_SIZE,
		chunkZ * CHUNK_SIZE * BLOCK_SIZE
	)
	MeshPart.Parent = workspace
end

function ChunkRenderer.unrenderChunk(chunkX: number, chunkZ: number, chunkY: number): ()
	local chunk = workspace:FindFirstChild(`{chunkX}x{chunkZ}x{chunkY}`)
	if chunk then
		chunk:Destroy()
	end
end

return ChunkRenderer


local BlockMeshData = {}

BlockMeshData.rightVertices = {
	Vector3.new(.5, .5, .5),
	Vector3.new(.5, .5, -.5),
	Vector3.new(.5, -.5, .5),
	Vector3.new(.5, -.5, -.5)
}
BlockMeshData.rightIndices = {3, 4, 2, 2, 1, 3}

BlockMeshData.leftVertices = {
	Vector3.new(-.5, .5, -.5),
	Vector3.new(-.5, .5, .5),
	Vector3.new(-.5, -.5, -.5),
	Vector3.new(-.5, -.5, .5)
}
BlockMeshData.leftIndices = {3, 4, 2, 2, 1, 3}

BlockMeshData.topVertices = {
	Vector3.new(-.5, .5, -.5),
	Vector3.new(.5, .5, -.5),
	Vector3.new(-.5, .5, .5),
	Vector3.new(.5, .5, .5)
}
BlockMeshData.topIndices = {3, 4, 2, 2, 1, 3}

BlockMeshData.bottomVertices = {
	Vector3.new(-.5, -.5, -.5),
	Vector3.new(.5, -.5, -.5),
	Vector3.new(-.5, -.5, .5),
	Vector3.new(.5, -.5, .5)
}
BlockMeshData.bottomIndices = {2, 4, 3, 3, 1, 2}

BlockMeshData.frontVertices = {
	Vector3.new(-.5, .5, .5),
	Vector3.new(.5, .5, .5),
	Vector3.new(-.5, -.5, .5),
	Vector3.new(.5, -.5, .5),
}
BlockMeshData.frontIndices = {3, 4, 2, 2, 1, 3}

BlockMeshData.backVertices = {
	Vector3.new(-.5, .5, -.5),
	Vector3.new(.5, .5, -.5),
	Vector3.new(-.5, -.5, -.5),
	Vector3.new(.5, -.5, -.5),
}
BlockMeshData.backIndices = {2, 4, 3, 3, 1, 2}

return BlockMeshData


local PlantMeshData = {}

PlantMeshData.leftVertices = {
	Vector3.new(-.5, .5, -.5),
	Vector3.new(.5, .5, .5),
	Vector3.new(-.5, -.5, -.5),
	Vector3.new(.5, -.5, .5),
}
PlantMeshData.leftIndices = {3, 4, 2, 2, 1, 3}

PlantMeshData.rightVertices = {
	Vector3.new(-.5, .5, .5),
	Vector3.new(.5, .5, -.5),
	Vector3.new(-.5, -.5, .5),
	Vector3.new(.5, -.5, -.5),
}
PlantMeshData.rightIndices = {3, 4, 2, 2, 1, 3}

return PlantMeshData


local CollisionsManager = {}

local Players = game:GetService('Players')
local ReplicatedStorage = game:GetService('ReplicatedStorage')
local RunService = game:GetService('RunService')

local Player: Player = Players.LocalPlayer
local Character: Model = Player.Character or Player.CharacterAdded:Wait()
local HumanoidRootPart: Part = Character:WaitForChild('HumanoidRootPart') :: Part
local PlayerScripts = Player.PlayerScripts

local Modules = PlayerScripts:WaitForChild('Modules')

local ChunksData = require(Modules.Chunks.Data.ChunksData)
local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)
local ChunksUtil = require(ReplicatedStorage.Shared.ChunksUtil)
local ItemsData = require(ReplicatedStorage.Shared.ItemsData)

local CHUNK_SIZE: number = ChunkSettings['CHUNK_SIZE']
local BLOCK_SIZE: number = ChunkSettings['BLOCK_SIZE']

local AIR_ID: number = ItemsData['Air']['ID']

local CollisionParts: Folder = workspace.CollisionParts
local collisionParts = {}

local IS_DEBUGGING: boolean = false

for i = 1, 10 do
	local collisionPart: BasePart = Instance.new('Part')
	collisionPart.Transparency = 1
	collisionPart.Material = Enum.Material.Neon
	collisionPart.Color = Color3.fromRGB(52, 255, 0)
	collisionPart.Anchored = true
	collisionPart.CanCollide = true
	collisionPart.Size = Vector3.new(BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
	if IS_DEBUGGING then
		collisionPart.Transparency = .9
		collisionPart.Size = Vector3.new(BLOCK_SIZE + .01, BLOCK_SIZE + .01, BLOCK_SIZE + .01)
	end
	collisionPart.Parent = CollisionParts
	table.insert(collisionParts, collisionPart)
end

local function roundToNearest(value: number, snap: number): number
	return math.round(value / snap) * snap
end

local function updateCollisionParts(): ()
	if not HumanoidRootPart then
		return
	end

	local characterPosition: Vector3 = HumanoidRootPart.Position
	local roundedPosition: Vector3 = Vector3.new(
		roundToNearest(characterPosition.X, BLOCK_SIZE),
		roundToNearest(characterPosition.Y - 1, BLOCK_SIZE),
		roundToNearest(characterPosition.Z, BLOCK_SIZE)
	)

	local surroundingBlockPositions: {Vector3} = {}

	for worldX = roundedPosition.X - BLOCK_SIZE, roundedPosition.X + BLOCK_SIZE, BLOCK_SIZE do
		for worldZ = roundedPosition.Z - BLOCK_SIZE, roundedPosition.Z + BLOCK_SIZE, BLOCK_SIZE do
			for worldY = roundedPosition.Y - BLOCK_SIZE, roundedPosition.Y + BLOCK_SIZE, BLOCK_SIZE do
				local chunkPosition = ChunksUtil.worldToChunkPosition(Vector3.new(worldX, worldY, worldZ))
				local chunkX = chunkPosition[1]
				local chunkZ = chunkPosition[2]
				local chunkY = chunkPosition[3]
				local x = chunkPosition[4]
				local z = chunkPosition[5]
				local y = chunkPosition[6]

				if
					not ChunksData[chunkX]
					or not ChunksData[chunkX][chunkZ]
					or not ChunksData[chunkX][chunkZ][chunkY]
				then
					continue
				end
				local chunkBlocks: buffer = ChunksData[chunkX][chunkZ][chunkY]['buffer']

				local bufferPosition = ChunksUtil.chunkToBufferPosition(x, z, y)
				if buffer.readu8(chunkBlocks, bufferPosition) == AIR_ID then
					continue
				end
				table.insert(surroundingBlockPositions, Vector3.new(worldX, worldY, worldZ))
			end
		end
	end

	table.sort(surroundingBlockPositions, function(a, b)
		return
			(a.X - roundedPosition.X) ^ 2 + (a.Y - roundedPosition.Y) ^ 2 + (a.Z - roundedPosition.Z) ^ 2
			<
			(b.X - roundedPosition.X) ^ 2 + (b.Y - roundedPosition.Y) ^ 2 + (b.Z - roundedPosition.Z) ^ 2
	end)

	for i = 1, #collisionParts do
		if not surroundingBlockPositions[i] then
			collisionParts[i].Position = Vector3.new(0, -1000, 0)
			continue
		end
		collisionParts[i].Position = surroundingBlockPositions[i]
	end
end

RunService.PreRender:Connect(updateCollisionParts)

return CollisionsManager


local LightingController = {}

local Lighting = game:GetService('Lighting')
local ReplicatedStorage = game:GetService('ReplicatedStorage')

local ChunkSettings = require(ReplicatedStorage.Shared.ChunkSettings)

local CHUNK_DISTANCE: number = ChunkSettings['CHUNK_DISTANCE']
local CHUNK_SIZE: number = ChunkSettings['CHUNK_SIZE']
local BLOCK_SIZE: number = ChunkSettings['BLOCK_SIZE']

Lighting.FogEnd = CHUNK_DISTANCE * CHUNK_SIZE * BLOCK_SIZE
Lighting.FogStart = (CHUNK_DISTANCE - 1) * CHUNK_SIZE * BLOCK_SIZE

return LightingController