Saving/loading terrain inside data store

is there anyway to do this? i tried to use read and write voxels but that took forever to process just a very tiny section of terrain

i know about the terrain save/load plugin, that stores a terrain region instance which cannot be saved in a data store so this cant work

2 Likes

What are the requirements for saving the terrain? Do you need to save every voxel, or do you just need to save a heightmap representation? Is the terrain able to be manipulated by the player during the game, or could you simply save a seed that could be used to regenerate the same region at join time?

1 Like

i need to save it just like the terrain save/load plugin
so every voxel with their material so it can load it in the same way it was created in studio

1 Like

You’ll need to write some system for reading the voxel information into a data structure capable of being saved in data stores as well as a way to reconstruct the terrain from this format. Depending on the size of the region(s) of terrain, you might need to keep in mind the limits that can be saved per data store key.

As far as a data structure, I recommend a buffer. When reading from terrain to prepare for save, create a buffer:

local chunkData = buffer.create(2 * x * y * z)
  • 2 is a constant assuming we will use 2 bytes per voxel
  • x, y, and z are the dimensions of the terrain region

Occupancy per voxel retuns a number [0-1] - this can be multiplied by 255 for a range within an unsigned integer.

Material per voxel returns an Enum.Material, however the Value property of the EnumItem isn’t necessarily within the unsigned integer range (despite there being less than 256 Materials). You can implement a simple hash to associate an unsigned integer to each Enum.Material you need to support:

matToUnsigned = {
   [Enum.Material.Air] = 0,
   [Enum.Material.Grass] = 1,
   [Enum.Material.Rock] = 2,
   ... --Add more key/values to your needs. Values must be unique
}

Using the information per voxel, you can write each voxel to the buffer:

--psuedo:
chunkOffset = 0
foreach voxel in CHUNK:
   vOcc = 255 * voxel.Occupancy
   vMat = matToUnsigned[voxel.Material]
   buffer.writeu8(chunkData, offset, vOcc)
   buffer.writeu8(chunkData, offset + 1, vMat)
   chunkOffset += 2
  • chunkOffset is used above to indicate you would need some consistent way of iterating over the voxels in the CHUNK

Once your buffer has been filled with terrain data, you can save this to a data store by getting the string representation of the buffer: buffer.tostring(chunkData). Loading terrain can use the value retrieve from the data store to create a buffer: buffer.fromstring(value) which could then be used to reconstruct the terrain in game.

EDIT: This assumes the use of Read/WriteVoxels Terrain methods, not the newer Read/WriteVoxelChannels. For more information see this announcement.

1 Like

this is what i currently have, issue is the materials and occupancies lists have to be 3d lists when passing into WriteVoxels() but how would i know how they should be arranged

the original materials list is set up like this

[1] = {[1] = {materials}, [2] = {materials}, [3] = {materials}, [4] = {materials}},
[2] = {[1] = {materials}, [2] = {materials}, [3] = {materials}, [4] = {materials}},
[3] = {[1] = {materials}, [2] = {materials}, [3] = {materials}, [4] = {materials}},
[4] = {[1] = {materials}, [2] = {materials}, [3] = {materials}, [4] = {materials}},
Size = Vector3(4, 4, 4)

my loaded materials list is a 1d array of some order ranging from 1-64

local RESOLUTION = 2
local MATERIAL_TO_INT = {
	[Enum.Material.Air] = 0,
	[Enum.Material.Grass] = 1
}
local INT_TO_MATERIAL = {
	[0] = Enum.Material.Air,
	[1] = Enum.Material.Grass
}

local region = Region3.new(Vector3.one * -10, Vector3.one * 10)
local materials, occupancies = workspace.Terrain:ReadVoxels(region, RESOLUTION + 2)
local size = materials.Size

print(materials)
print(occupancies)

local chunkData = buffer.create(RESOLUTION * size.x * size.y * size.z)
local chunkOffset = 0

for x = 1, size.X, 1 do
	for y = 1, size.Y, 1 do
		for z = 1, size.Z, 1 do
			local occupancy = occupancies[x][y][z] * 255
			local material = MATERIAL_TO_INT[materials[x][y][z]]
			buffer.writeu8(chunkData, chunkOffset, occupancy)
			buffer.writeu8(chunkData, chunkOffset + 1, material)
			chunkOffset += 2
		end
	end
end

print('saved')
task.wait(2)
print('loading')

local saved = buffer.tostring(chunkData)

workspace.Terrain:Clear()

local loaded = buffer.fromstring(saved)
local materials, occupancies = {}, {}
local size = buffer.len(chunkData)
for offset = 0, size - 1, 2 do
	local occupancy = buffer.readu8(loaded, offset) / 255
	local material = INT_TO_MATERIAL[buffer.readu8(loaded, offset + 1)]
	table.insert(materials, material)
	table.insert(occupancies, occupancy)
end

print(materials)
print(occupancies)

--workspace.Terrain:WriteVoxels(region, RESOLUTION + 2, materials, occupancies)

update: i figured it out

local RESOLUTION = 2
local MATERIAL_TO_INT = {
	[Enum.Material.Air] = 0,
	[Enum.Material.Grass] = 1
}
local INT_TO_MATERIAL = {
	[0] = Enum.Material.Air,
	[1] = Enum.Material.Grass
}

local function deepClone(tbl: {any})
	local tCopy = table.clone(tbl)
	for k, v in tCopy do
		if type(v) == "table" then
			tCopy[k] = deepClone(v)
		end
	end
	return tCopy
end

local region = Region3.new(Vector3.one * -10, Vector3.one * 10)
local materials, occupancies = workspace.Terrain:ReadVoxels(region, RESOLUTION + 2)
local size = materials.Size

print(materials)
print(occupancies)

local chunkData = buffer.create(RESOLUTION * size.x * size.y * size.z)
local chunkOffset = 0

for x = 1, size.X, 1 do
	for y = 1, size.Y, 1 do
		for z = 1, size.Z, 1 do
			local occupancy = occupancies[x][y][z] * 255
			local material = MATERIAL_TO_INT[materials[x][y][z]]
			buffer.writeu8(chunkData, chunkOffset, occupancy)
			buffer.writeu8(chunkData, chunkOffset + 1, material)
			chunkOffset += 2
		end
	end
end

print('saved')
task.wait(2)
print('loading')

local saved = buffer.tostring(chunkData)

workspace.Terrain:Clear()

local loaded = buffer.fromstring(saved)
local materials = table.create(size.X, table.create(size.Y, table.create(size.Z, nil)))
local occupancies = deepClone(materials)

local offset = 0
for x = 1, size.X, 1 do
	for y = 1, size.Y, 1 do
		for z = 1, size.Z, 1 do
			local occupancy = buffer.readu8(loaded, offset) / 255
			local material = INT_TO_MATERIAL[buffer.readu8(loaded, offset + 1)]
			
			materials[x][y][z] = material
			occupancies[x][y][z] = occupancy
			offset += 2
		end
	end
end

print(materials)
print(occupancies)

workspace.Terrain:WriteVoxels(region, RESOLUTION + 2, materials, occupancies)
1 Like

im now trying to actually save this in a data store and im getting the error
“Cannot store Array in data store. Data stores can only accept valid UTF-8 characters.”

but you can clearly save arrays in a data store because thats exactly what i do right before trying to save the buffer data, so it could be the values

i have a list of stringified buffers to save in a data store (a list because the terrain could be larger than the max read voxel size of 4194304 voxels^3)

when i print the terrain data i get this
image

it goes on like that for a while

edit: you can save buffers directly in a datastore so theres no need to convert it to a string, thats what i did