cellestial/arena.lua

786 lines
22 KiB
Lua

local adv_chat = minetest.global_exists("adv_chat") and adv_chat
local speedup = cellestial.conf.speedup
local mapcache = cellestial.conf.mapcache
local c_cell = minetest.get_content_id("cellestial:cell")
local c_border = minetest.get_content_id("cellestial:border")
local c_air = minetest.CONTENT_AIR
local area_store = AreaStore()
local area_store_path = minetest.get_worldpath() .. "/data/cellestial.dat"
function load_store()
if modlib.file.exists(area_store_path) then
area_store:from_file(area_store_path)
end
end
load_store()
local delta = 0
function store_store()
if delta > 0 then
area_store:to_file(area_store_path)
delta = 0
end
end
modlib.minetest.register_globalstep(60, function()
if delta > 10 then
store_store()
end
end)
modlib.minetest.register_globalstep(600, store_store)
minetest.register_on_shutdown(store_store)
arenas = {}
function unload_arenas()
for id, arena in pairs(arenas) do
local owner_online = false
for _, owner in pairs(arena.meta.owners) do
if minetest.get_player_by_name(owner) then
owner_online = true
break
end
end
if not owner_online then
arenas[id] = nil
end
end
end
modlib.minetest.register_globalstep(60, unload_arenas)
simulating = {}
function calculate_offsets(voxelarea)
--[[
Lua API: "Y stride and z stride of a flat array"
+x +1
-x -1
+y +ystride
-y -ystride
+z +zstride
-z -zstride
]]
local ystride, zstride = voxelarea.ystride, voxelarea.zstride
local offsets = {}
for x = -1, 1, 1 do
for y = -ystride, ystride, ystride do
for z = -zstride, zstride, zstride do
local offset = x + y + z
if offset ~= 0 then
table.insert(offsets, offset)
end
end
end
end
return offsets
end
function initialize_cells(self)
local cells = {}
self.cells = cells
for index, data in pairs(self.area) do
if data == c_cell then
cells[index] = true
end
end
end
function read_from_map(self)
self.voxelmanip = minetest.get_voxel_manip(self.min, self.max)
local emin, emax = self.voxelmanip:read_from_map(self.min, self.max)
self.voxelarea = VoxelArea:new { MinEdge = emin, MaxEdge = emax }
self.offsets = calculate_offsets(self.voxelarea)
self.area = self.voxelmanip:get_data()
end
function overlaps(min, max)
local areas = area_store:get_areas_in_area(min, max, true, true, false)
if not modlib.table.is_empty(areas) then
return true
end
return false
end
function update(self)
read_from_map(self)
if speedup then
initialize_cells(self)
calculate_neighbors(self)
end
remove_area(self)
end
function create_base(min, max)
local obj = { min = min, max = max }
update(obj)
return setmetatable(obj, { __index = getfenv(1), __call = getfenv(1) })
end
function create_role(self)
local role = "#" .. self.id
if adv_chat and not adv_chat.roles[role] then
adv_chat.register_role(role, { title = self.meta.name, color = cellestial.colors.cell.edge })
for _, owner in pairs(self.meta.owners) do
if minetest.get_player_by_name(owner) then
adv_chat.add_role(owner, role)
end
end
end
end
function new(min, max, meta)
local obj = create_base(min, max)
if not obj then
return obj
end
meta.name = meta.name or cellestial.conf.arena_defaults.name
obj.meta = meta
obj.id = assert(store(obj))
create_role(obj)
modlib.table.foreach_value(meta.owners, modlib.func.curry(add_owner_to_meta, obj))
arenas[obj.id] = obj
obj:reset()
store_store()
return obj
end
function deserialize(self, data)
self.meta = minetest.parse_json(data)
end
function load(id, min, max, data)
local obj = create_base(min, max)
obj.id = id
deserialize(obj, data)
arenas[id] = obj
create_role(obj)
return obj
end
function create_from_area(area)
return load(area.id, area.min, area.max, area.data)
end
function owner_info(self)
return table.concat(self.meta.owners, ", ")
end
function info(self)
local dim = vector.add(get_dim(self), 1)
return ('Arena #%s "%s" by %s from (%s, %s, %s) to (%s, %s, %s) - %s wide, %s tall and %s long'):format(
self.id,
self.meta.name,
owner_info(self),
self.min.x,
self.min.y,
self.min.z,
self.max.x,
self.max.y,
self.max.z,
dim.x,
dim.y,
dim.z
)
end
function formspec_table_info(self)
local dim = vector.add(get_dim(self), 1)
return table.concat(modlib.table.map({
cellestial.colors.cell.fill,
"#" .. self.id,
cellestial.colors.cell.edge,
self.meta.name,
"#FFFFFF",
owner_info(self),
table.concat({ self.min.x, self.min.y, self.min.z }, ", ") .. " - " .. table.concat({ self.max.x, self.max.y, self.max.z }, ", ") ..
" (" .. table.concat({ dim.x, dim.y, dim.z }, ", ") .. ")"
}, minetest.formspec_escape), ",")
end
function serialize(self)
return minetest.write_json(self.meta)
end
function store(self)
delta = delta + 1
if self.id then
area_store:remove_area(self.id)
end
return area_store:insert_area(self.min, self.max, serialize(self), self.id)
end
function is_owner(self, other_owner)
return modlib.table.contains(self.meta.owners, other_owner)
end
function get_position(self, name)
if cellestial.is_cellestial(name) then
return 1
end
return is_owner(self, name)
end
function serialize_ids(ids)
return table.concat(ids, ",")
end
function store_ids(meta, ids)
meta:set_string("cellestial_arena_ids", serialize_ids(ids))
end
function deserialize_ids(text)
return modlib.table.map(modlib.text.split(text, ","), tonumber)
end
function load_ids(meta)
local ids = meta:get_string("cellestial_arena_ids")
return deserialize_ids(ids)
end
function owner_action(func)
return function(self, name)
local player = minetest.get_player_by_name(name)
if not player then
return
end
local meta = player:get_meta()
local ids = load_ids(meta)
local index = modlib.table.binary_search(ids, self.id)
local err = func(self, ids, index)
if err ~= nil then
return err
end
store_ids(meta, ids)
return true
end
end
add_owner_to_meta = owner_action(
function(self, ids, index)
if index > 0 then
return false
end
table.insert(ids, -index, self.id)
end
)
function add_owner(self, name, index)
local success = add_owner_to_meta(self, name)
if success == nil then
return
end
if adv_chat then
adv_chat.add_role(name, "#" .. self.id)
end
if not modlib.table.contains(self.meta.owners) then
table.insert(self.meta.owners, index or (#self.meta.owners + 1), name)
end
arena:store()
return success
end
remove_owner_from_meta = owner_action(
function(self, ids, index)
if index < 1 then
return false
end
table.remove(ids, index)
end
)
function remove_owner(self, name)
local success = remove_owner_from_meta(self, name)
if success == nil then
return
end
if adv_chat then
adv_chat.remove_role(name, "#" .. self.id)
end
local owner_index = modlib.table.contains(self.meta.owners, name)
if owner_index then
table.remove(self.meta.owners, owner_index)
end
arena:store()
end
function set_owners(self, owners)
local owner_set = modlib.table.set(owners)
local self_owner_set = modlib.table.set(self.owners)
local to_be_added = modlib.table.difference(owner_set, self_owner_set)
local to_be_removed = modlib.table.difference(self_owner_set, owner_set)
modlib.table.foreach_key(to_be_added, modlib.func.curry(add_owner, self))
modlib.table.foreach_key(to_be_removed, modlib.func.curry(add_owner, self))
arena:store()
end
function get_dim(self)
return vector.subtract(self.max, self.min)
end
function get_area(self)
if mapcache then
return self.area
end
read_from_map(self)
self.area = self.voxelmanip:get_data()
return self.area
end
function get_area_temp(self)
if mapcache then
return self.area
end
return self.voxelmanip:get_data()
end
function remove_area(self)
if not mapcache then
self.area = nil
end
end
function set_area(self, min, dim)
local new_min = min or self.min
local new_max = self.max
if dim then
new_max = vector.add(new_min, dim)
end
local areas = area_store:get_areas_in_area(new_min, new_max, true, true)
areas[self.id] = nil
if modlib.table.is_empty(areas) then
self.min = new_min
self.max = new_max
return true
end
return false
end
function set_borders(self)
local min, max = self.min, self.max
for coord = 1, 6 do
local coords = { min.x, min.y, min.z, max.x, max.y, max.z }
if coord > 3 then
coords[coord] = coords[coord - 3]
else
coords[coord] = coords[coord + 3]
end
for index in self.voxelarea:iter(unpack(coords)) do
self.area[index] = c_border
end
end
end
function resize(self, dim)
local min = self.min
local old_max = self.max
if not set_area(self, nil, dim) then
return false
end
local max = self.max
local max_max = {}
for coord, value in pairs(max) do
max_max[coord] = math.max(value, old_max[coord])
end
local voxelmanip = minetest.get_voxel_manip(self.min, max_max)
local emin, emax = voxelmanip:read_from_map(self.min, max_max)
local voxelarea = VoxelArea:new{ MinEdge = emin, MaxEdge = emax }
local area = voxelmanip:get_data()
-- clear added area
for coord, v_coord in ipairs{"x", "y", "z"} do
local coords = { min.x, min.y, min.z, max_max.x, max_max.y, max_max.z }
local old = old_max[v_coord]
if old < coords[coord+3] then
coords[coord] = old
else
coords[coord] = max[v_coord]
coords[coord + 3] = old
end
for index in voxelarea:iter(unpack(coords)) do
area[index] = c_air
end
end
voxelmanip:set_data(area)
voxelmanip:write_to_map()
read_from_map(self)
set_borders(self)
write_to_map(self)
update(self)
store(self)
return true
end
function move(self, min)
local old_min, old_max = self.min, self.max
local voxelmanip = minetest.get_voxel_manip(old_min, old_max)
local emin, emax = voxelmanip:read_from_map(old_min, old_max)
local voxelarea = VoxelArea:new{ MinEdge = emin, MaxEdge = emax }
local previous_data = voxelarea:iterp(old_min, old_max)
if not set_area(self, min, get_dim(self)) then
return false
end
local area = voxelmanip:get_data()
local area_copy = modlib.table.copy(area)
for index in voxelarea:iterp(old_min, old_max) do
area[index] = c_air
end
voxelmanip:set_data(area)
voxelmanip:write_to_map()
read_from_map(self)
for index in self.voxelarea:iterp(self.min, self.max) do
local previous_data_index = previous_data()
self.area[index] = area_copy[previous_data_index]
end
write_to_map(self)
remove_area(self)
store(self)
return true
end
function get(pos)
local areas = area_store:get_areas_for_pos(pos, true, true)
local id = next(areas)
if not id then
return
end
if next(areas, id) then
return
end
if arenas[id] then
return arenas[id]
end
local area = areas[id]
area.id = id
return create_from_area(area)
end
local guaranteed_max = 128
local cutoff_min_iteration = 4
local cutoff_factor = 1.25
-- uses a monte-carlo like iterative tree level search
function create_free(meta, origin, dim)
dim = dim or modlib.table.copy(cellestial.conf.arena_defaults.dimension)
local visited_ids = {}
local current_level = { origin or modlib.table.copy(cellestial.conf.arena_defaults.search_origin) }
local iteration = 1
local found_min
local area_found = false
repeat
local new_level = {}
local function process_level()
for _, min in pairs(current_level) do
local areas = area_store:get_areas_in_area(min, vector.add(min, dim), true, true, false)
if modlib.table.is_empty(areas) then
found_min = min
area_found = true
return
end
for id, area in pairs(modlib.table.shuffle(areas)) do
if not visited_ids[id] then
visited_ids[id] = true
end
for _, coord in pairs(modlib.table.shuffle({ "x", "y", "z" })) do
for _, new_value in pairs(modlib.table.shuffle({ area.min[coord] - dim[coord] - 1, area.max[coord] + 1 })) do
local new_min = modlib.table.copy(area.min)
new_min[coord] = new_value
if iteration <= cutoff_min_iteration or math.random() < 1 / math.pow(cutoff_factor, iteration - cutoff_min_iteration) then
table.insert(new_level, new_min)
if #new_level >= guaranteed_max then
return
end
end
end
end
end
end
end
process_level()
modlib.table.shuffle(new_level)
current_level = new_level
iteration = iteration + 1
until area_found
local arena = new(found_min, vector.add(found_min, dim), meta)
return arena
end
function get_by_id(id)
if arenas[id] then
return arenas[id]
end
local area = area_store:get_area(id, true, true)
if not area then
return
end
area.id = id
return create_from_area(area)
end
function get_by_player(player)
return get(player:get_pos())
end
function get_by_name(name)
local player = minetest.get_player_by_name(name)
if not player then
return
end
return get_by_player(player)
end
function list_ids_by_name(name)
local player = minetest.get_player_by_name(name)
if not player then
return
end
local arena_ids = load_ids(player:get_meta())
return arena_ids
end
function list_by_name(name)
local ids = list_ids_by_name(name)
if not ids then
return ids
end
return modlib.table.map(ids, get_by_id)
end
function remove(self)
arenas[(type(self) == "table" and self.id) or self] = nil
end
function get_cell(self, pos)
local index = self.voxelarea:indexp(pos)
if speedup then
return self.cells[index] == true
end
if self.area then
return self.area[index] == c_cell
end
return minetest.get_node(pos).name == "cellestial:cell"
end
if speedup then
function __set_cell(self, index, cell)
local cell_or_nil = (cell or nil)
if self.cells[index] == cell_or_nil then
return true
end
self.cells[index] = cell_or_nil
local neighbors = self.neighbors
if cell then
neighbors[index] = neighbors[index] or 0
end
local delta = (cell and 1) or -1
-- This won't give out of bounds because it only iterates the inside of the voxelarea (content)
for _, offset in pairs(self.offsets) do
local newindex = index + offset
neighbors[newindex] = (neighbors[newindex] or 0) + delta
end
end
end
-- does everything except setting the node
function _set_cell(self, pos, cell)
local index = self.voxelarea:indexp(pos)
if speedup then
if __set_cell(self, index, cell) then
return
end
else
if get_cell(self, pos) == (cell or false) then
return
end
end
if self.area then
self.area[index] = (cell and c_cell) or c_air
end
return true
end
function set_cell(self, pos, cell)
if _set_cell(self, pos, cell) then
minetest.set_node(pos, { name = (cell and "cellestial:cell") or "air" })
end
end
function calculate_neighbors(self)
local cells = self.cells
local offsets = self.offsets
local neighbors = {}
self.neighbors = neighbors
for index, _ in pairs(cells) do
neighbors[index] = neighbors[index] or 0
-- This won't give out of bounds because it only iterates the inside of the voxelarea (content)
for _, offset in pairs(offsets) do
local new_index = index + offset
neighbors[new_index] = (neighbors[new_index] or 0) + 1
end
end
end
function apply_rules(self, rules)
local cells, area = self.cells, get_area(self)
local birth = rules.birth
local death = rules.death
local delta_cells = {}
for index, amount in pairs(self.neighbors) do
if cells[index] then
if death[amount] and area[index] == c_cell then
delta_cells[index] = false
end
elseif birth[amount] and area[index] == c_air then
delta_cells[index] = true
end
end
if birth[0] then
for index in iter_content(self) do
if not cells[index] then
delta_cells[index] = true
end
end
end
for index, cell in pairs(delta_cells) do
__set_cell(self, index, cell)
self.area[index] = (cell and c_cell) or c_air
end
end
function write_to_map(self)
local vm = self.voxelmanip
vm:set_data(self.area)
vm:write_to_map()
end
if speedup then
function next_step(self, rules)
apply_rules(self, rules)
write_to_map(self)
remove_area(self)
end
else
function next_step(self, rules)
local offsets = self.offsets
local birth = rules.birth
local death = rules.death
read_from_map(self)
local vm = self.voxelmanip
local data = vm:get_data()
local new_data = {}
for index, c_id in ipairs(data) do
new_data[index] = c_id
end
self.area = new_data
for index in iter_content(self) do
local c_id = data[index]
local amount = 0
-- This won't give out of bounds because it only iterates the inside of the voxelarea (content)
for _, offset in pairs(offsets) do
if data[index + offset] == c_cell then
amount = amount + 1
end
end
if c_id == c_cell then
if death[amount] then
c_id = c_air
end
elseif c_id == c_air and birth[amount] then
c_id = c_cell
end
new_data[index] = c_id
end
write_to_map(self)
end
end
function iter_content(self)
return self.voxelarea:iter(self.min.x + 1, self.min.y + 1, self.min.z + 1, self.max.x - 1, self.max.y - 1, self.max.z - 1)
end
function is_content(self, pos)
return pos.x >= self.min.x + 1 and pos.y >= self.min.y + 1 and pos.z >= self.min.z + 1
and pos.x <= self.max.x - 1 and pos.y <= self.max.y - 1 and pos.z <= self.max.z - 1
end
function _clear(self)
for index in iter_content(self) do
self.area[index] = c_air
end
if speedup then
self.cells = {}
self.neighbors = {}
end
end
function clear(self)
get_area(self)
_clear(self)
write_to_map(self)
remove_area(self)
end
function reset(self)
local min, max = self.min, self.max
get_area(self)
_clear(self)
set_borders(self)
local light_data = self.voxelmanip:get_light_data()
for index in self.voxelarea:iter(min.x, min.y, min.z, max.x, max.y, max.z) do
light_data[index] = minetest.LIGHT_MAX
end
self.voxelmanip:set_light_data(light_data)
write_to_map(self)
remove_area(self)
end
function randomize(self, threshold)
self.area = get_area(self)
self.cells = {}
for index in iter_content(self) do
if math.random() < threshold then
self.cells[index] = true
self.area[index] = c_cell
else
self.area[index] = c_air
end
end
calculate_neighbors(self)
write_to_map(self)
remove_area(self)
end
function next_steps(self, steps, rules)
for _ = 1, steps do
next_step(self, rules)
end
end
function start(self, steps_per_second, rules)
simulating[self.id] = { arena = self, steps_per_second = steps_per_second, outstanding_steps = 0, rules = rules }
end
function stop(self)
simulating[self.id] = nil
end
function simulate(self, steps_per_second, rules)
if simulating[self.id] then
return stop(self)
end
return start(self, steps_per_second, rules)
end
function teleport(self, player)
local area = get_area(self)
for index in iter_content(self) do
local c_id = area[index]
if c_id == c_air then
-- move to place with air
player:set_pos(vector.add(self.voxelarea:position(index), 0.5))
return true
end
end
player:set_pos(vector.add(self.min, vector.divide(vector.subtract(self.max, self.min), 2))) -- move to center
return false
end
minetest.register_globalstep(
function(dtime)
for _, sim in pairs(simulating) do
local outstanding_steps = sim.outstanding_steps + dtime * sim.steps_per_second
local steps = math.floor(outstanding_steps)
sim.arena:next_steps(steps, sim.rules)
sim.outstanding_steps = outstanding_steps - steps
end
end
)