11 Commits

11 changed files with 173 additions and 65 deletions

View File

@ -1,14 +1,20 @@
# Map Generator with Rivers
`mapgen_rivers v1.0` by Gaël de Sailly.
`mapgen_rivers v1.0.1` by Gaël de Sailly.
Semi-procedural map generator for Minetest 5.x. It aims to create realistic and nice-looking landscapes for the game, focused on river networks. It is based on algorithms modelling water flow and river erosion at a broad scale, similar to some used by researchers in Earth Sciences. It is taking some inspiration from [Fastscape](https://github.com/fastscape-lem/fastscape).
Its main particularity compared to conventional Minetest mapgens is that rivers that flow strictly downhill, and combine together to form wider rivers, until they reach the sea. Another notable feature is the possibility of large lakes above sea level.
![Screenshot](https://user-images.githubusercontent.com/6905002/98825953-6289d980-2435-11eb-9e0b-704a95663ce0.png)
![Screenshot](https://content.minetest.net/uploads/fff09f2269.png)
It used to be composed of a Python script doing pre-generation, and a Lua mod reading the pre-generation output and generating the map. The code has been rewritten in full Lua for version 1.0 (July 2021), and is now usable out-of-the-box as any other Minetest mod.
# Author and license
License: GNU LGPLv3.0
Code: Gaël de Sailly
Flow routing algorithm concept (in `terrainlib/rivermapper.lua`): Cordonnier, G., Bovy, B., & Braun, J. (2019). A versatile, linear complexity algorithm for flow routing in topographies with depressions. Earth Surface Dynamics, 7(2), 549-562.
# Requirements
Mod dependencies: `default` required, and [`biomegen`](https://github.com/Gael-de-Sailly/biomegen) optional (provides biome system).

View File

@ -1,3 +1,6 @@
local sqrt, abs = math.sqrt, math.abs
local unpk = unpack
local function distance_to_segment(x1, y1, x2, y2, x, y)
-- get the distance between point (x,y) and segment (x1,y1)-(x2,y2)
local a = (x1-x2)^2 + (y1-y2)^2 -- square of distance
@ -5,13 +8,13 @@ local function distance_to_segment(x1, y1, x2, y2, x, y)
local c = (x2-x)^2 + (y2-y)^2
if a + b < c then
-- The closest point of the segment is the extremity 1
return math.sqrt(b)
return sqrt(b)
elseif a + c < b then
-- The closest point of the segment is the extremity 2
return math.sqrt(c)
return sqrt(c)
else
-- The closest point is on the segment
return math.abs(x1 * (y2-y) + x2 * (y-y1) + x * (y1-y2)) / math.sqrt(a)
return abs(x1 * (y2-y) + x2 * (y-y1) + x * (y1-y2)) / sqrt(a)
end
end
@ -19,8 +22,8 @@ local function transform_quadri(X, Y, x, y)
-- To index points in an irregular quadrilateral, giving x and y between 0 (one edge) and 1 (opposite edge)
-- X, Y 4-vectors giving the coordinates of the 4 vertices
-- x, y position to index.
local x1, x2, x3, x4 = unpack(X)
local y1, y2, y3, y4 = unpack(Y)
local x1, x2, x3, x4 = unpk(X)
local y1, y2, y3, y4 = unpk(Y)
-- Compare distance to 2 opposite edges, they give the X coordinate
local d23 = distance_to_segment(x2,y2,x3,y3,x,y)

View File

@ -6,7 +6,11 @@ local transform_quadri = dofile(modpath .. 'geometry.lua')
local sea_level = mapgen_rivers.settings.sea_level
local riverbed_slope = mapgen_rivers.settings.riverbed_slope * mapgen_rivers.settings.blocksize
local MAP_BOTTOM = -31000
local out_elev = mapgen_rivers.settings.margin_elev
-- Localize for performance
local floor, min, max = math.floor, math.min, math.max
local unpk = unpack
-- Linear interpolation
local function interp(v00, v01, v11, v10, xf, zf)
@ -30,11 +34,11 @@ local function heightmaps(minp, maxp)
if poly then
local xf, zf = transform_quadri(poly.x, poly.z, x, z)
local i00, i01, i11, i10 = unpack(poly.i)
local i00, i01, i11, i10 = unpk(poly.i)
-- Load river width on 4 edges and corners
local r_west, r_north, r_east, r_south = unpack(poly.rivers)
local c_NW, c_NE, c_SE, c_SW = unpack(poly.river_corners)
local r_west, r_north, r_east, r_south = unpk(poly.rivers)
local c_NW, c_NE, c_SE, c_SW = unpk(poly.river_corners)
-- Calculate the depth factor for each edge and corner.
-- Depth factor:
@ -64,10 +68,10 @@ local function heightmaps(minp, maxp)
-- Transform the coordinates to have xf and zf = 0 or 1 in rivers (to avoid rivers having lateral slope and to accomodate the surrounding smoothly)
if imax == 0 then
local x0 = math.max(r_west, c_NW-zf, zf-c_SW)
local x1 = math.min(r_east, c_NE+zf, c_SE-zf)
local z0 = math.max(r_north, c_NW-xf, xf-c_NE)
local z1 = math.min(r_south, c_SW+xf, c_SE-xf)
local x0 = max(r_west, c_NW-zf, zf-c_SW)
local x1 = min(r_east, c_NE+zf, c_SE-zf)
local z0 = max(r_north, c_NW-xf, xf-c_NE)
local z1 = min(r_south, c_SW+xf, c_SE-xf)
xf = (xf-x0) / (x1-x0)
zf = (zf-z0) / (z1-z0)
elseif imax == 1 then
@ -90,7 +94,7 @@ local function heightmaps(minp, maxp)
-- Determine elevation by interpolation
local vdem = poly.dem
local terrain_height = math.floor(0.5+interp(
local terrain_height = floor(0.5+interp(
vdem[1],
vdem[2],
vdem[3],
@ -115,17 +119,17 @@ local function heightmaps(minp, maxp)
lake_id = 1
end
end
local lake_height = math.max(math.floor(poly.lake[lake_id]), terrain_height)
local lake_height = max(floor(poly.lake[lake_id]), terrain_height)
if imax > 0 and depth_factor_max > 0 then
terrain_height = math.min(math.max(lake_height, sea_level) - math.floor(1+depth_factor_max*riverbed_slope), terrain_height)
terrain_height = min(max(lake_height, sea_level) - floor(1+depth_factor_max*riverbed_slope), terrain_height)
end
terrain_height_map[i] = terrain_height
lake_height_map[i] = lake_height
else
terrain_height_map[i] = MAP_BOTTOM
lake_height_map[i] = MAP_BOTTOM
terrain_height_map[i] = out_elev
lake_height_map[i] = out_elev
end
i = i + 1
end

View File

@ -4,6 +4,11 @@ local modpath = minetest.get_modpath(minetest.get_current_modname()) .. '/'
mapgen_rivers.modpath = modpath
mapgen_rivers.world_data_path = minetest.get_worldpath() .. '/river_data/'
if minetest.get_mapgen_setting("mg_name") ~= "singlenode" then
minetest.set_mapgen_setting("mg_name", "singlenode", true)
minetest.log("warning", "[mapgen_rivers] Mapgen set to singlenode")
end
dofile(modpath .. 'settings.lua')
local sea_level = mapgen_rivers.settings.sea_level
@ -27,6 +32,9 @@ local function interp(v00, v01, v11, v10, xf, zf)
return v1*zf + v0*(1-zf)
end
-- Localize for performance
local floor, min = math.floor, math.min
local data = {}
local noise_x_obj, noise_z_obj, noise_distort_obj, noise_heat_obj, noise_heat_blend_obj
@ -43,7 +51,7 @@ local sumtime2 = 0
local ngen = 0
local function generate(minp, maxp, seed)
print(("[mapgen_rivers] Generating from %s to %s"):format(minetest.pos_to_string(minp), minetest.pos_to_string(maxp)))
minetest.log("info", ("[mapgen_rivers] Generating from %s to %s"):format(minetest.pos_to_string(minp), minetest.pos_to_string(maxp)))
local chulens = {
x = maxp.x-minp.x+1,
@ -106,8 +114,8 @@ local function generate(minp, maxp, seed)
end
end
local pminp = {x=math.floor(xmin), z=math.floor(zmin)}
local pmaxp = {x=math.floor(xmax)+1, z=math.floor(zmax)+1}
local pminp = {x=floor(xmin), z=floor(zmin)}
local pmaxp = {x=floor(xmax)+1, z=floor(zmax)+1}
incr = pmaxp.x-pminp.x+1
i_origin = 1 - pminp.z*incr - pminp.x
terrain_map, lake_map = heightmaps(pminp, pmaxp)
@ -115,6 +123,30 @@ local function generate(minp, maxp, seed)
terrain_map, lake_map = heightmaps(minp, maxp)
end
-- Check that there is at least one position that reaches min y
if minp.y > sea_level then
local y0 = minp.y
local is_empty = true
for i=1, #terrain_map do
if terrain_map[i] >= y0 or lake_map[i] >= y0 then
is_empty = false
break
end
end
-- If not, skip chunk
if is_empty then
local t = os.clock() - t0
ngen = ngen + 1
sumtime = sumtime + t
sumtime2 = sumtime2 + t*t
minetest.log("verbose", "[mapgen_rivers] Skipping empty chunk (fully above ground level)")
minetest.log("verbose", ("[mapgen_rivers] Done in %5.3f s"):format(t))
return
end
end
local c_stone = minetest.get_content_id("default:stone")
local c_dirt = minetest.get_content_id("default:dirt")
local c_lawn = minetest.get_content_id("default:dirt_with_grass")
@ -140,7 +172,7 @@ local function generate(minp, maxp, seed)
for z = minp.z, maxp.z do
for x = minp.x, maxp.x do
local ivm = a:index(x, minp.y, z)
local ivm = a:index(x, maxp.y+1, z)
local ground_above = false
local temperature
if use_biomes then
@ -156,8 +188,8 @@ local function generate(minp, maxp, seed)
if use_distort then
local xn = noise_x_map[nid]
local zn = noise_z_map[nid]
local x0 = math.floor(xn)
local z0 = math.floor(zn)
local x0 = floor(xn)
local z0 = floor(zn)
local i0 = i_origin + z0*incr + x0
local i1 = i0+1
@ -165,13 +197,12 @@ local function generate(minp, maxp, seed)
local i3 = i2-1
terrain = interp(terrain_map[i0], terrain_map[i1], terrain_map[i2], terrain_map[i3], xn-x0, zn-z0)
lake = math.min(lake_map[i0], lake_map[i1], lake_map[i2], lake_map[i3])
lake = min(lake_map[i0], lake_map[i1], lake_map[i2], lake_map[i3])
end
if y <= maxp.y then
local is_lake = lake > terrain
local ivm = a:index(x, y, z)
if y <= terrain then
if not use_biomes or y <= terrain-1 or ground_above then
data[ivm] = c_stone
@ -200,7 +231,7 @@ local function generate(minp, maxp, seed)
ground_above = y <= terrain
ivm = ivm + ystride
ivm = ivm - ystride
if use_distort then
nid = nid + incrY
end
@ -228,18 +259,17 @@ local function generate(minp, maxp, seed)
vm:calc_lighting()
vm:update_liquids()
vm:write_to_map()
local t1 = os.clock()
local t = t1-t0
local t = os.clock()-t0
ngen = ngen + 1
sumtime = sumtime + t
sumtime2 = sumtime2 + t*t
print(("[mapgen_rivers] Done in %5.3f s"):format(t))
minetest.log("verbose", ("[mapgen_rivers] Done in %5.3f s"):format(t))
end
minetest.register_on_generated(generate)
minetest.register_on_shutdown(function()
local avg = sumtime / ngen
local std = math.sqrt(sumtime2/ngen - avg*avg)
print(("[mapgen_rivers] Mapgen statistics:\n- Mapgen calls: %4d\n- Mean time: %5.3f s\n- Standard deviation: %5.3f s"):format(ngen, avg, std))
minetest.log("action", ("[mapgen_rivers] Mapgen statistics:\n- Mapgen calls: %4d\n- Mean time: %5.3f s\n- Standard deviation: %5.3f s"):format(ngen, avg, std))
end)

View File

@ -1,12 +1,15 @@
local worldpath = mapgen_rivers.world_data_path
local floor = math.floor
local sbyte, schar = string.byte, string.char
local unpk = unpack
function mapgen_rivers.load_map(filename, bytes, signed, size, converter)
local file = io.open(worldpath .. filename, 'rb')
local data = file:read('*all')
if #data < bytes*size then
data = minetest.decompress(data)
end
local sbyte = string.byte
local map = {}
@ -35,8 +38,6 @@ function mapgen_rivers.load_map(filename, bytes, signed, size, converter)
return map
end
local sbyte = string.byte
local loader_mt = {
__index = function(loader, i)
local file = loader.file
@ -75,9 +76,6 @@ end
function mapgen_rivers.write_map(filename, data, bytes)
local size = #data
local file = io.open(worldpath .. filename, 'wb')
local mfloor = math.floor
local schar = string.char
local upack = unpack
local bytelist = {}
for j=1, bytes do
@ -85,15 +83,15 @@ function mapgen_rivers.write_map(filename, data, bytes)
end
for i=1, size do
local n = mfloor(data[i])
local n = floor(data[i])
data[i] = n
for j=bytes, 2, -1 do
bytelist[j] = n % 256
n = mfloor(n / 256)
n = floor(n / 256)
end
bytelist[1] = n % 256
file:write(schar(upack(bytelist)))
file:write(schar(unpk(bytelist)))
end
file:close()

View File

@ -73,7 +73,7 @@ for name, np in pairs(mapgen_rivers.noise_params) do
if lac > 1 then
local omax = math.floor(math.log(math.min(np.spread.x, np.spread.y, np.spread.z)) / math.log(lac))+1
if np.octaves > omax then
print("[mapgen_rivers] Noise " .. name .. ": 'octaves' reduced to " .. omax)
minetest.log("warning", "[mapgen_rivers] Noise " .. name .. ": 'octaves' reduced to " .. omax)
np.octaves = omax
end
end

View File

@ -33,7 +33,7 @@ if first_mapgen then
-- Generate a map!!
local pregenerate = dofile(mapgen_rivers.modpath .. '/pregenerate.lua')
minetest.register_on_mods_loaded(function()
print('[mapgen_rivers] Generating grid')
minetest.log("action", '[mapgen_rivers] Generating grid, this may take a while...')
pregenerate(load_all)
if load_all then
@ -58,9 +58,9 @@ if not (first_mapgen and load_all) then
minetest.register_on_mods_loaded(function()
if load_all then
print('[mapgen_rivers] Loading full grid')
minetest.log("action", '[mapgen_rivers] Loading full grid')
else
print('[mapgen_rivers] Loading grid as interactive loaders')
minetest.log("action", '[mapgen_rivers] Loading grid as interactive loaders')
end
local grid = mapgen_rivers.grid
@ -90,16 +90,19 @@ if mapgen_rivers.settings.center then
map_offset.z = blocksize*Z/2
end
-- Localize for performance
local floor, ceil, min, max, abs = math.floor, math.ceil, math.min, math.max, math.abs
local min_catchment = mapgen_rivers.settings.min_catchment / (blocksize*blocksize)
local wpower = mapgen_rivers.settings.river_widening_power
local wfactor = 1/(2*blocksize * min_catchment^wpower)
local function river_width(flow)
flow = math.abs(flow)
flow = abs(flow)
if flow < min_catchment then
return 0
end
return math.min(wfactor * flow ^ wpower, 1)
return min(wfactor * flow ^ wpower, 1)
end
local noise_heat -- Need a large-scale noise here so no heat blend
@ -138,8 +141,8 @@ local function make_polygons(minp, maxp)
local polygons = {}
-- Determine the minimum and maximum coordinates of the polygons that could be on the chunk, knowing that they have an average size of 'blocksize' and a maximal offset of 0.5 blocksize.
local xpmin, xpmax = math.max(math.floor((minp.x+map_offset.x)/blocksize - 0.5), 0), math.min(math.ceil((maxp.x+map_offset.x)/blocksize + 0.5), X-2)
local zpmin, zpmax = math.max(math.floor((minp.z+map_offset.z)/blocksize - 0.5), 0), math.min(math.ceil((maxp.z+map_offset.z)/blocksize + 0.5), Z-2)
local xpmin, xpmax = max(floor((minp.x+map_offset.x)/blocksize - 0.5), 0), min(ceil((maxp.x+map_offset.x)/blocksize + 0.5), X-2)
local zpmin, zpmax = max(floor((minp.z+map_offset.z)/blocksize - 0.5), 0), min(ceil((maxp.z+map_offset.z)/blocksize + 0.5), Z-2)
-- Iterate over the polygons
for xp = xpmin, xpmax do
@ -165,8 +168,8 @@ local function make_polygons(minp, maxp)
local bounds = {} -- Will be a list of the intercepts of polygon edges for every Z position (scanline algorithm)
-- Calculate the min and max Z positions
local zmin = math.max(math.floor(math.min(unpack(poly_z)))+1, minp.z)
local zmax = math.min(math.floor(math.max(unpack(poly_z))), maxp.z)
local zmin = max(floor(min(unpack(poly_z)))+1, minp.z)
local zmax = min(floor(max(unpack(poly_z))), maxp.z)
-- And initialize the arrays
for z=zmin, zmax do
bounds[z] = {}
@ -176,14 +179,14 @@ local function make_polygons(minp, maxp)
for i2=1, 4 do -- Loop on 4 edges
local z1, z2 = poly_z[i1], poly_z[i2]
-- Calculate the integer Z positions over which this edge spans
local lzmin = math.floor(math.min(z1, z2))+1
local lzmax = math.floor(math.max(z1, z2))
local lzmin = floor(min(z1, z2))+1
local lzmax = floor(max(z1, z2))
if lzmin <= lzmax then -- If there is at least one position in it
local x1, x2 = poly_x[i1], poly_x[i2]
-- Calculate coefficient of the equation defining the edge: X=aZ+b
local a = (x1-x2) / (z1-z2)
local b = (x1 - a*z1)
for z=math.max(lzmin, minp.z), math.min(lzmax, maxp.z) do
for z=max(lzmin, minp.z), min(lzmax, maxp.z) do
-- For every Z position involved, add the intercepted X position in the table
table.insert(bounds[z], a*z+b)
end
@ -194,11 +197,11 @@ local function make_polygons(minp, maxp)
-- Now sort the bounds list
local zlist = bounds[z]
table.sort(zlist)
local c = math.floor(#zlist/2)
local c = floor(#zlist/2)
for l=1, c do
-- Take pairs of X coordinates: all positions between them belong to the polygon.
local xmin = math.max(math.floor(zlist[l*2-1])+1, minp.x)
local xmax = math.min(math.floor(zlist[l*2]), maxp.x)
local xmin = max(floor(zlist[l*2-1])+1, minp.x)
local xmax = min(floor(zlist[l*2]), maxp.x)
local i = (z-minp.z) * chulens + (xmin-minp.x) + 1
for x=xmin, xmax do
-- Fill the map at these places
@ -220,16 +223,16 @@ local function make_polygons(minp, maxp)
local riverD = river_width(rivers[iD])
if glaciers then -- Widen the river
if get_temperature(poly_x[1], poly_dem[1], poly_z[1]) < 0 then
riverA = math.min(riverA*glacier_factor, 1)
riverA = min(riverA*glacier_factor, 1)
end
if get_temperature(poly_x[2], poly_dem[2], poly_z[2]) < 0 then
riverB = math.min(riverB*glacier_factor, 1)
riverB = min(riverB*glacier_factor, 1)
end
if get_temperature(poly_x[3], poly_dem[3], poly_z[3]) < 0 then
riverC = math.min(riverC*glacier_factor, 1)
riverC = min(riverC*glacier_factor, 1)
end
if get_temperature(poly_x[4], poly_dem[4], poly_z[4]) < 0 then
riverD = math.min(riverD*glacier_factor, 1)
riverD = min(riverD*glacier_factor, 1)
end
end

View File

@ -13,6 +13,38 @@ local time_step = mapgen_rivers.settings.evol_time_step
local niter = math.ceil(time/time_step)
time_step = time / niter
local use_margin = mapgen_rivers.settings.margin
local margin_width = mapgen_rivers.settings.margin_width / blocksize
local margin_elev = mapgen_rivers.settings.margin_elev
local function margin(dem, width, elev)
local X, Y = dem.X, dem.Y
for i=1, width do
local c1 = ((i-1)/width) ^ 0.5
local c2 = (1-c1) * elev
local index = (i-1)*X + 1
for x=1, X do
dem[index] = dem[index] * c1 + c2
index = index + 1
end
index = i
for y=1, Y do
dem[index] = dem[index] * c1 + c2
index = index + X
end
index = X*(Y-i) + 1
for x=1, X do
dem[index] = dem[index] * c1 + c2
index = index + 1
end
index = X-i + 1
for y=1, Y do
dem[index] = dem[index] * c1 + c2
index = index + X
end
end
end
local function pregenerate(keep_loaded)
local grid = mapgen_rivers.grid
local size = grid.size
@ -26,6 +58,10 @@ local function pregenerate(keep_loaded)
dem.X = size.x
dem.Y = size.y
if use_margin then
margin(dem, margin_width, margin_elev)
end
local model = EvolutionModel(evol_params)
model.dem = dem
local ref_dem = model:define_isostasy(dem)
@ -33,7 +69,7 @@ local function pregenerate(keep_loaded)
local tectonic_step = tectonic_speed * time_step
collectgarbage()
for i=1, niter do
print("[mapgen_rivers] Iteration " .. i .. " of " .. niter)
minetest.log("info", "[mapgen_rivers] Iteration " .. i .. " of " .. niter)
model:diffuse(time_step)
model:flow()
@ -41,6 +77,9 @@ local function pregenerate(keep_loaded)
if i < niter then
if tectonic_step ~= 0 then
nobj_base:get_3d_map_flat({x=0, y=tectonic_step*i, z=0}, ref_dem)
if use_margin then
margin(ref_dem, margin_width, margin_elev)
end
end
model:isostasy()
end

View File

@ -1,7 +1,7 @@
local mtsettings = minetest.settings
local mgrsettings = Settings(minetest.get_worldpath() .. '/mapgen_rivers.conf')
mapgen_rivers.version = "1.0"
mapgen_rivers.version = "1.0.1"
local previous_version_mt = mtsettings:get("mapgen_rivers_version") or "0.0"
local previous_version_mgr = mgrsettings:get("version") or "0.0"
@ -74,6 +74,9 @@ mapgen_rivers.settings = {
grid_x_size = def_setting('grid_x_size', 'number', 1000),
grid_z_size = def_setting('grid_z_size', 'number', 1000),
margin = def_setting('margin', 'bool', true),
margin_width = def_setting('margin_width', 'number', 2000),
margin_elev = def_setting('margin_elev', 'number', -200),
evol_params = {
K = def_setting('river_erosion_coef', 'number', 0.5),
m = def_setting('river_erosion_power', 'number', 0.4),

View File

@ -17,13 +17,23 @@ mapgen_rivers_grid_x_size (Grid X size) int 1000 50 5000
# Actual size of the map is grid_z_size * blocksize
mapgen_rivers_grid_z_size (Grid Z size) int 1000 50 5000
# If margin is enabled, elevation becomes closer to a fixed value when approaching
# the edges of the map.
mapgen_rivers_margin (Margin) bool true
# Width of the transition at map borders, in nodes
mapgen_rivers_margin_width (Margin width) float 2000.0 0.0 15000.0
# Elevation toward which to converge at map borders
mapgen_rivers_margin_elev (Margin elevation) float -200.0 -31000.0 31000.0
# Minimal catchment area for a river to be drawn, in square nodes
# Lower value means bigger river density
mapgen_rivers_min_catchment (Minimal catchment area) float 3600.0 100.0 1000000.0
# Coefficient describing how rivers widen when merging.
# Riwer width is a power law W = a*D^p. D is river flow and p is this parameter.
# Higher value means a river needs to receive more tributaries to grow in width.
# Higher value means that a river will grow more when receiving a tributary.
# Note that a river can never exceed 2*blocksize.
mapgen_rivers_river_widening_power (River widening power) float 0.5 0.0 1.0

View File

@ -1,5 +1,17 @@
-- rivermapper.lua
-- This file provide functions to construct the river tree from an elevation model.
-- Based on a research paper:
--
-- Cordonnier, G., Bovy, B., and Braun, J.:
-- A versatile, linear complexity algorithm for flow routing in topographies with depressions,
-- Earth Surf. Dynam., 7, 549562, https://doi.org/10.5194/esurf-7-549-2019, 2019.
--
-- Big thanks to them for releasing this paper under a free license ! :)
-- The algorithm here makes use of most of the paper's concepts, including the Planar Boruvka algorithm.
-- Only flow_local and accumulate_flow are custom algorithms.
local function flow_local_semirandom(plist)
local sum = 0
for i=1, #plist do