Initial release 🥳

master
Lars Mueller 2020-03-26 19:54:34 +01:00
commit f04f64aa8b
16 changed files with 2088 additions and 0 deletions

85
Readme.md Normal file
View File

@ -0,0 +1,85 @@
# 3D Cellular Automata (`cellestial`)
An implementation of 3D Cellular Automata for Minetest. Resembles [Flux](https://forum.minetest.net/viewtopic.php?f=15&t=20498).
## About
Cellestial (**cell**-estial) is a mod implementing 3D Cellular Automata in Minetest. Media & code by Lars Mueller aka LMD or appguru(eu). Code licensed under the MIT license, media licensed as CC0.
Part of the Cellestial Series: [`cellestial`](https://github.com/appgurueu/cellestial), [`cellestiall`](https://github.com/appgurueu/cellestiall) and [`cellestial_game`](https://github.com/appgurueu/cellestial_game)
## Symbolic Representation
![Screenshot](screenshot.png)
## Features
* High Performance
* Intuitive Interfaces
* Powerful API
### Links
* [GitHub](https://github.com/appgurueu/cellestial)
* [Discord](https://discordapp.com/invite/ysP74by)
* [ContentDB](https://content.minetest.net/packages/LMD/cellestial)
* [Minetest Forum](https://forum.minetest.net/viewtopic.php?f=9&t=24456)
* ["Candidates for the Game of Life in Three Dimensions" by Carter Bays](http://wpmedia.wolfram.com/uploads/sites/13/2018/02/01-3-1.pdf)
## Instructions
Available in-game through `/cells help`.
## Configuration
Below is the default configuration, located under `<worldpath>/config/cellestial.json`. It is mostly self-explaining. A few notes:
* `r`, `g`, `b`: Red, green and blue color components
* `max_steps`: Maximum steps per item use / second
* `speedup`: Decrease processing time, but cache more
* `mapcache`: Cache the nodes of the area
* `arena_defaults`: Values used for arenas if not provided
```json
{
"colors": {
"cell": {
"edge": {
"r": 0,
"g": 128,
"b": 0
},
"fill": {
"r": 0,
"g": 255,
"b": 0
}
},
"border": {
"edge": {
"r": 51,
"g": 51,
"b": 51
},
"fill": {
"r": 77,
"g": 77,
"b": 77
}
}
},
"max_steps": 10,
"request_duration": 30,
"creative": true,
"place_inside_player": false,
"speedup": true,
"mapcache": false,
"arena_defaults": {
"name": "unnamed",
"dimension": {"x": 80, "y": 80, "z": 80},
"search_origin": {"x": 0, "y": 0, "z": 0},
"steps": 1,
"threshold": 0.5
}
}
```

712
arena.lua Normal file
View File

@ -0,0 +1,712 @@
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)
-- TODO use set_cell and the like for the performant step
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 = 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 = get_dim(self)
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 = get_dim(self)
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
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
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
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))
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
update(self)
return true
end
return false
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
while true do
local new_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
goto area_found
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
goto next_level
end
end
end
end
end
end
:: next_level ::
modlib.table.shuffle(new_level)
current_level = new_level
iteration = iteration + 1
end
:: 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
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
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
local min, max = self.min, self.max
for index in iter_content(self) do
local c_id = data[index]
local amount = 0
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 _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)
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
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)
local rand = math.random(threshold)
local amount = (1 / math.sqrt(2 * math.pi)) * math.exp(-0.5 * rand * rand)
local min, max = self.min, self.max
self.cells = {}
for index in iter_content(self) do
if self.cells[index] then
self.area[index] = c_cell
else
self.area[index] = c_air
end
end
end
function randomize_slow(self, threshold)
local min, max = self.min, self.max
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
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
)

694
chatcommands.lua Normal file
View File

@ -0,0 +1,694 @@
local function register_chatcommand(cmd, desc, func, param)
cmdlib.register_chatcommand(
"cells " .. cmd,
{
description = desc,
params = param,
func = function(name, params)
local arena = arena.get_by_name(name)
if not arena then
return false, "Not inside exactly one arena"
end
if not arena:is_owner(name) and not is_cellestial(name) then
return false, "Not an owner of the current arena"
end
return unpack({ func(arena, name, params) })
end
}
)
end
register_chatcommand(
"clear",
"Clear the current arena",
function(arena)
arena:clear()
return true, "Arena cleared"
end
)
register_chatcommand(
"update",
"Update the current arena",
function(arena)
arena:update()
return true, "Arena updated"
end
)
register_chatcommand(
"randomize",
"Randomize the current arena",
function(arena, _, params)
local threshold = conf.arena_defaults.threshold
if params.threshold then
threshold = tonumber(params.threshold)
if not threshold or threshold < 0 or threshold > 1 then
return false, "Threshold needs to be a number from 0 to 1"
end
end
arena:randomize(threshold)
return true, "Arena randomized"
end,
"[threshold]"
)
register_chatcommand(
"evolve",
"Evolve/simulate the cells inside the current arena",
function(arena, _, params)
local steps = conf.arena_defaults.steps
if params.steps then
steps = tonumber(params.steps)
if not steps or steps <= 0 or steps % 1 ~= 0 then
return false, "Steps need to be a positive integer number"
end
end
arena:next_steps(steps)
return true, "Simulated " .. steps .. " step" .. ((steps > 1 and "s") or "")
end,
"[steps]"
)
register_chatcommand(
"start",
"Start the simulation",
function(arena, _, params)
local steps_per_second = 1
if params.steps_per_second then
steps_per_second = tonumber(params.steps_per_second)
if not steps_per_second or steps_per_second <= 0 then
return false, "Steps per second needs to be > 0"
end
end
arena:start(steps_per_second)
return true, "Started simulation with a speed of " .. steps_per_second .. " steps per second"
end,
"[steps_per_second]"
)
register_chatcommand(
"stop",
"Stop the simulation",
function(arena)
local s = arena:stop()
if not s then
return false, "Simulation not running"
end
return true, "Simulation stopped"
end
)
local assign = {
width_change = "x",
height_change = "y",
length_change = "z",
x_move = "x",
y_move = "y",
z_move = "z"
}
local human_names = {
width_change = "Width Change",
height_change = "Height Change",
length_change = "Length Change",
x_move = "X move",
y_move = "Y move",
z_move = "Z move"
}
local dim_human_names = {
width_change = "Width",
height_change = "Height",
length_change = "Length",
x_move = "X",
y_move = "Y",
z_move = "Z"
}
register_chatcommand(
"resize",
"Resize arena",
function(arena, _, params)
local dimensions = arena:get_dimensions()
for name, val in pairs(params) do
val = tonumber(val)
if not val or val % 1 ~= 0 then
return false, human_names[name] .. " needs to be an integer number"
end
local new_dim = dimensions[assign[name]] + val
if new_dim < 3 then
return false, dim_human_names[name] .. " needs to be at least 3 (as it includes the borders)"
end
dimensions[assign[name]] = new_dim
end
local s = arena:set_area(nil, dimensions)
if s then
return true, "Arena resized to " .. dimensions.x .. ", " .. dimensions.y .. ", " .. dimensions.z
end
return false, "Arena would collide with other arenas if resized"
end,
"<width_change> [height_change] [length_change]"
)
register_chatcommand(
"move",
"Move arena",
function(arena, _, params)
local position = modlib.table.tablecopy(arena.min)
for name, val in pairs(params) do
val = tonumber(val)
if not val or val % 1 ~= 0 then
return false, human_names[name] .. " needs to be an integer number"
end
local new_dim = position[assign[name]] + val
position[assign[name]] = new_dim
end
local s = arena:set_area(position)
if s then
return true, "Arena moved to " .. position.x .. ", " .. position.y .. ", " .. position.z
end
return false, "Arena would collide with other arenas if moved"
end,
"<x_move> [y_move] [z_move]"
)
local function get_id(name, params)
local id = tonumber(params.id)
if not id or id % 1 ~= 0 or id < 0 then
return false, "ID needs to be a non-negative integer number"
end
local arena = arena.get_by_id(id)
if not arena then
return false, "No area with the ID #" .. params.id
end
return true, arena
end
cmdlib.register_chatcommand(
"cells get id",
{
params = "<id>",
description = "Get the arena with the corresponding ID",
func = function(name, params)
local success, arena = get_id(name, params)
if success then
return success, arena:info()
end
return success, arena
end
}
)
cmdlib.register_chatcommand(
"cells teleport id",
{
params = "<id>",
description = "Teleport to the arena with the corresponding ID",
func = function(name, params)
local success, arena = get_id(name, params)
if success then
local player = minetest.get_player_by_name(name)
if not player then
return false, "You need to be online to teleport"
end
if not arena:is_owner(name) then
return false, "Not an owner of the corresponding arena"
end
arena:teleport(player)
return success, "Teleporting to: " .. arena:info()
end
return success, arena
end
}
)
cmdlib.register_chatcommand(
"cells get player",
{
params = "[name]",
description = "Get the arena the player is currently in",
func = function(name, params)
local arena = arena.get_by_name(params.name or name)
if not arena then
return false, "Not inside an arena"
end
return true, arena:info()
end
}
)
local function get_pos(params)
local vector = {}
for _, param in ipairs({ "x", "y", "z" }) do
vector[param] = tonumber(params[param])
if not vector[param] then
return false, param.upper() .. " needs to be a valid number"
end
end
local arena = arena.get(params)
if not arena then
return false, "Not inside an arena"
end
return true, arena, vector
end
cmdlib.register_chatcommand(
"cells get pos",
{
params = "<x> <y> <z>",
description = "Get the arena at position",
func = function(_, params)
local success, arena = get_pos(params)
if success then
return success, arena:info()
end
return success, arena
end
}
)
cmdlib.register_chatcommand(
"cells teleport pos",
{
params = "<x> <y> <z>",
description = "Teleport to the position",
func = function(name, params)
local success, arena, vector = get_pos(params)
if success then
local player = minetest.get_player_by_name(name)
if not player then
return false, "You need to be online to teleport"
end
if not arena:get_position(name) then
return false, "Not an owner of the arena"
end
player:set_pos(vector)
return success, ("Teleporting to (%s, %s, %s)"):format(vector)
end
return success, arena
end
}
)
local function create_teleport_request(name, arena, property, value)
if teleport_requests[name] then
return false, "You already have a running request"
end
if arena:get_position(name) then
return false, "No need for a teleport request"
end
local sent_to = {}
local timers = {}
for _, owner in ipairs(arena.meta.owners) do
owner_ref = minetest.get_player_by_name(owner)
if owner_ref then
table.insert(sent_to, owner)
minetest.chat_send_player(owner, "Player " .. minetest.get_color_escape_sequence(colors.cell.fill) .. name .. " " ..
minetest.get_color_escape_sequence("#FFFFFF") .. " requests to teleport to (%s, %s, %s).")
timers[owner] = hud_timers.add_timer(owner, { name = name .. "'s request", duration = request_duration, color = colors.cell.fill:sub(2) })
end
end
if #sent_to == 0 then
return false, ("No owner (none of %s) online"):format(arena:owner_info())
end
local timer = hud_timers.add_timer(name, { name = "Teleport", duration = request_duration, color = colors.cell.edge:sub(2), on_complete = modlib.func.curry(remove_teleport_request, name) })
local request = { timer = timer, timers = timers, [property] = value }
for _, owner in pairs(sent_to) do
teleport_requests_last[owner] = request
end
teleport_requests[name] = request
return true, "Teleport request sent to " .. table.concat(sent_to, ", ")
end
request_duration = 30
teleport_requests = {}
teleport_requests_last = {}
local function remove_request_receivers(name)
local request = teleport_requests[name]
for owner_name, timer in pairs(request.timers) do
hud_timers.remove_timer_by_reference(owner_name, timer)
local last_requests = teleport_requests_last[owner_name]
local index = modlib.table.find(last_requests, name)
if index then
table.remove(last_requests, index)
end
end
end
local function remove_teleport_request(name)
remove_request_receivers(name)
teleport_requests[name] = nil
end
minetest.register_on_joinplayer(function(player)
teleport_requests_last[player:get_player_name()] = {}
end)
minetest.register_on_leaveplayer(function(player)
local name = player:get_player_name()
local request = teleport_requests[name]
if request then
remove_teleport_request(name, request)
end
local last_requests = teleport_requests_last[name]
if last_requests then
for _, requester_name in pairs(teleport_requests_last) do
local request = teleport_requests[requester_name]
if request then
request.timers[name] = nil
if modlib.table.is_empty(request.timers) then
remove_teleport_request(requester_name)
end
end
end
teleport_requests_last[name] = nil
end
end)
cmdlib.register_chatcommand(
"cells teleport request pos",
{
params = "<x> <y> <z>",
description = "Send teleport request to owners of the arena",
func = function(name, params)
local success, arena, vector = get_pos(params)
if success then
local player = minetest.get_player_by_name(name)
if not player then
return false, "You need to be online to teleport"
end
return unpack({create_teleport_request(name, arena, "pos", vector)})
end
return success, arena
end
}
)
cmdlib.register_chatcommand(
"cells teleport request id",
{
params = "<id>",
description = "Send teleport request to owners of the arena",
func = function(name, params)
local id = tonumber(params.id)
if not id or id % 1 ~= 0 or id < 0 then
return false, "ID needs to be a non-negative integer number"
end
local arena = arena.get_by_id(id)
if not arena then
return false, "No arena with the ID #"..id
end
return unpack({create_teleport_request(name, arena, "id", id)})
end
}
)
cmdlib.register_chatcommand(
"cells teleport request player",
{
params = "<name>",
description = "Send teleport request to player",
func = function(name, params)
local player = minetest.get_player_by_name(name)
if not player then
return false, "You need to be online to teleport"
end
if teleport_requests[name] then
return false, "You already have a running request"
end
local target = minetest.get_player_by_name(params.name)
if not target then
return false, "Player "..params.name.." is not online"
end
local request = {
name = params.name,
timer = hud_timers.add_timer(name, { name = "Teleport", duration = request_duration, color = colors.cell.edge:sub(2), on_complete = modlib.func.curry(remove_teleport_request, name) }),
timers = { [params.name] = hud_timers.add_timer(params.name, { name = name .. "'s request", duration = request_duration, color = colors.cell.fill:sub(2) }) },
}
teleport_requests[name] = request
table.insert(teleport_requests_last[params.name], name)
end
}
)
cmdlib.register_chatcommand(
"cells teleport accept",
{
params = "[name]",
description = "Accept teleport request of player or last request",
func = function(name, params)
local requester_name = params.name
if not requester_name then
requester_name = teleport_requests_last[name]
if not requester_name then
return false, "No outstanding last request"
end
end
local request = teleport_requests[params.name]
if params.name then
request = teleport_requests[params.name]
if not request or not request.timers[name] then
return false, "No outstanding request by player " .. params.name
end
else
request = teleport_requests_last[name]
if not request then
return false, "No outstanding last request"
end
end
remove_teleport_request(requester_name)
local pos, message
if request.pos then
pos = request.pos
message = ("Player %s teleported to %s, %s, %s"):format(requester_name, tonumber(request.pos.x), tonumber(request.pos.y), tonumber(request.pos.z))
elseif request.name then
pos = minetest.get_player_by_name(request.name):get_pos()
message = ("Player %s teleported to %s"):format(requester_name, request.name)
else
arena.get_by_id(request.id):teleport(requester_name)
return true, ("Player %s teleported to %s"):format(requester_name, request.name)
end
minetest.get_player_by_name(requester_name):set_pos(pos)
return true, message
end
}
)
function create_arena(sendername, params, nums)
for _, param in ipairs({ "x", "y", "z", "width", "height", "length" }) do
if not nums[param] then
local number = tonumber(params[param])
if not number or number % 1 ~= 0 then
return false, modlib.text.upper_first(param) .. " needs to be a valid integer number"
end
nums[param] = number
end
end
for _, param in ipairs({ "width", "height", "length" }) do
if nums[param] < 3 then
return false, modlib.text.upper_first(param) .. " needs to be positive and at least 3"
end
end
local name = params.name
local owners = params.owners or { sendername }
for _, owner in ipairs(owners) do
if not minetest.get_player_by_name(owner) then
return false, "Player " .. owner .. " is not online. All owners need to be online."
end
end
local min = { x = nums.x, y = nums.y, z = nums.z }
local max = { x = nums.x + nums.width, y = nums.y + nums.height, z = nums.z + nums.length }
if arena.overlaps(min, max) then
return false, "Selected area intersects with existing arenas"
end
local arena = arena.new(min, max, { name = name, owners = owners })
arena:teleport(minetest.get_player_by_name(owners[1]))
if arena then
return true, "Arena created"
end
return false, "Failed to create arena, would intersect with other arenas"
end
cmdlib.register_chatcommand(
"cells create there",
{
params = "<x> <y> <z> <width> <height> <length> [name] {owners}",
description = "Create a new arena",
func = function(sendername, params)
return unpack({ create_arena(sendername, params, {}) })
end
}
)
cmdlib.register_chatcommand(
"cells create here",
{
params = "<width> <height> <length> [name] {owners}",
description = "Create a new arena",
func = function(sendername, params)
local player = minetest.get_player_by_name(sendername)
if not player then
return false, "You need to be online in-game to use the command."
end
return unpack({ create_arena(sendername, params, vector.floor(player:get_pos())) })
end
}
)
cmdlib.register_chatcommand(
"cells arenas list",
{
params = "[name]",
description = "Lists all arenas of a player",
func = function(sendername, params)
local name = sendername or params.name
local ids = arena.list_by_name(name)
if not ids then
return false, "Player " .. name .. " is not online."
end
if #ids == 0 then
return true, "Player " .. name .. " does not have any arenas."
end
modlib.table.map(ids, arena.info)
table.insert(ids, 1, "Player " .. name .. " owns the following arenas:")
return true, table.concat(ids, "\n")
end
}
)
function show_arenas_formspec(sendername, name)
local ids = arena.list_by_name(name) or {}
modlib.table.map(ids, arena.formspec_table_info)
local table_height = math.min(3, #ids * 0.35)
local message, fs_table
if #ids == 0 then
message = "Player " .. name .. " does not have any arenas."
fs_table = ""
table_height = 0.25
else
message = "Player " .. name .. " owns the following arenas (double-click to teleport):"
fs_table = ([[
tablecolumns[color;text,align=inline;color;text,align=inline;color;text,align=inline;text,align=inline]
tableoptions[background=#00000000;highlight=#00000000;border=false]
table[0.15,1.6;7.6,%s;arenas;%s]
]]):format(table_height, table.concat(ids, ","))
table_height = table_height + 0.25
end
minetest.show_formspec(sendername, "cellestial:arenas",
([[
size[8,%s]
real_coordinates[true]
box[0,0;8,1;%s]
label[0.25,0.5;Arenas of player]
field[2,0.25;2,0.5;player;;%s]
field_close_on_enter[player;false]
button[4.25,0.25;1,0.5;show;Show]
label[0.25,1.35;%s]
%simage_button_exit[7.25,0.25;0.5,0.5;cmdlib_cross.png;close;]
]]):format(table_height + 1.5, colors.cell.fill, minetest.formspec_escape(name), minetest.formspec_escape(message), fs_table))
end
cmdlib.register_chatcommand(
"cells arenas show",
{
params = "[name]",
description = "Shows all arenas of a player",
func = function(sendername, params)
show_arenas_formspec(sendername, params.name or sendername)
return true
end
}
)
modlib.minetest.register_form_listener("cellestial:arenas", function(player, fields)
if fields.quit then
return
end
local name
if fields.player then
-- not using key_enter_field
name = fields.player
end
if not name or name:len() == 0 then
name = player:get_player_name()
end
if fields.arenas then
local event = minetest.explode_table_event(fields.arenas)
if event.type == "DCL" then
local id = arena.list_ids_by_name(name)[event.row]
if id then
local arena = arena.get_by_id(id)
if arena:get_position(player:get_player_name()) then
arena:teleport(player)
else
create_teleport_request(player:get_player_name(), arena, "id", id)
end
end
end
end
show_arenas_formspec(player:get_player_name(), name)
end)
cmdlib.register_chatcommand(
"cells help",
{
description = "Shows help",
func = function(name, _)
show_help(name)
end
}
)
register_chatcommand(
"owner add",
"Add owner to the current arena",
function(arena, name, param)
local param_name = param.name or name
local namepos = arena:is_owner(name)
if not namepos and minetest.check_player_privs(name, { cellestial = true }) then
namepos = 1
end
local position
if param.position then
position = tonumber(param.position)
if not position or position % 1 ~= 0 or position < namepos or position > #arena.meta.owners + 1 then
return false, "Position needs to be an integer number between " .. namepos .. " (your position) and " .. #arena.meta.owners + 1
end
end
local success = arena:add_owner(name, position)
if success == false then
return false, "Player " .. param_name .. " is not online"
end
return true, "Added player " .. param_name .. " to arena #" .. arena.id .. ", owners now: " .. table.concat(arena.meta.owners, ", ")
end,
"[name] [position]"
)
register_chatcommand(
"owner remove",
"Remove owner from current arena",
function(arena, name, param)
local param_name = param.name or name
local namepos = arena:is_owner(name)
if not namepos and minetest.check_player_privs(name, { cellestial = true }) then
namepos = 1
end
if namepos > arena:is_owner(param_name) then
return false, "Player " .. param_name .. " is in a higher position"
end
local success = arena:remove_owner(param_name)
if success == false then
return false, "Player " .. param_name .. " is not online"
end
return true, "Removed player " .. param_name .. " from arena #" .. arena.id .. ", owners now: " .. table.concat(arena.meta.owners, ", ")
end,
"[name]"
)
register_chatcommand(
"set_name",
"Set name of current arena",
function(arena, name, params)
local namepos = arena:is_owner(name)
if namepos > 1 then
return false, "Only the first owner can change the name."
end
local oldname = arena.meta.name
arena.meta.name = params.name
arena:store()
return true, ('Name changed from "%s" to "%s"'):format(oldname, arena.meta.name)
end,
"<name>"
)

39
conf.lua Normal file
View File

@ -0,0 +1,39 @@
local int = function(value) if value % 1 ~= 0 then return "Integer instead of float expected." end end
local pos_int = { type = "number", range = { 1 }, func = int }
local component = { type = "number", range = {0, 255}, func = int }
local color = { type = "table", children = {r = component, g = component, b = component} }
local node_colors = { fill = color, edge = color }
local vector = { type = "table", children = { x = pos_int, y = pos_int, z = pos_int } }
local conf_spec = {
type = "table",
children = {
colors = {
type = "table",
children = {
cell = node_colors,
border = node_colors
}
},
max_steps = pos_int,
request_duration = pos_int,
arena_defaults = {
name = { type = "string" },
dimension = vector,
search_origin = vector,
steps = pos_int,
threshold = { type = "number", range = {0, 1} }
},
creative = { type = "boolean" },
speedup = { type = "boolean" },
mapcache = { type = "boolean" },
place_inside_player = { type = "boolean" }
}
}
conf = modlib.conf.import("cellestial", conf_spec)
for _, colors in pairs(conf.colors) do
for prop, color in pairs(colors) do
colors[prop] = ("#%02X%02X%02X"):format(color.r, color.g, color.b)
end
end

41
default_config.json Normal file
View File

@ -0,0 +1,41 @@
{
"colors": {
"cell": {
"edge": {
"r": 0,
"g": 128,
"b": 0
},
"fill": {
"r": 0,
"g": 255,
"b": 0
}
},
"border": {
"edge": {
"r": 51,
"g": 51,
"b": 51
},
"fill": {
"r": 77,
"g": 77,
"b": 77
}
}
},
"max_steps": 10,
"request_duration": 30,
"creative": true,
"place_inside_player": false,
"speedup": true,
"mapcache": false,
"arena_defaults": {
"name": "unnamed",
"dimension": {"x": 80, "y": 80, "z": 80},
"search_origin": {"x": 0, "y": 0, "z": 0},
"steps": 1,
"threshold": 0.5
}
}

13
init.lua Normal file
View File

@ -0,0 +1,13 @@
if not minetest.features.area_store_persistent_ids then
error("Cellestial requires persistent area store IDs, upgrade to Minetest 5.1 or newer")
end
cellestial = {} -- to stop Minetest complaining about undeclared globals...
modlib.mod.extend("cellestial", "conf")
local cellestiall_init = modlib.mod.get_resource("cellestiall", "init.lua")
if cellestiall and modlib.file.exists(cellestiall_init) then
dofile(cellestiall_init)
end
modlib.mod.extend("cellestial", "main")
cellestial.arena = modlib.mod.loadfile_exports(modlib.mod.get_resource("cellestial", "arena.lua"))
modlib.mod.extend("cellestial", "chatcommands")
cellestiall.after_cellestial_loaded()

498
main.lua Normal file
View File

@ -0,0 +1,498 @@
minetest.register_privilege("cellestial", {
description = "Can manage cellestial arenas",
give_to_admin = true,
give_to_singleplayer = true
})
function is_cellestial(name)
return minetest.check_player_privs(name, { cellestial = true })
end
local creative = conf.creative
arenas = {}
colors = conf.colors
function add_area(params)
table.insert(arenas, arena.new(params))
end
function get_tile(name)
return "cellestial_fill.png^[multiply:" .. colors[name].fill .. "^(cellestial_edge.png^[multiply:" .. colors[name].edge .. ")"
end
local border = get_tile("border")
local cell = get_tile("cell")
local max_steps = conf.max_steps
local ces = minetest.get_color_escape_sequence
local _help_content = {
2, 1, "About",
1, 2, 'A mod made by LMD aka appguru(eu)',
2, 1, "Automata",
1, 2, 'Cellular automata work using simple principles:',
1, 2, '- the world is made out of cells, which are dead or alive',
1, 2, '- based on their neighbors, cells die or new ones are born',
2, 1, "Instructions",
1, 2, [[How to simulate cellular automata using Cellestial.
Remember that you can open this dialog using "/cells help".]],
3, 2, "Chat",
1, 3, [[The chat is where you talk with others and send commands.
Start your message with @name to send it to a player.
Use @#id to send it to all owners of the arena.]],
3, 2, "Commands",
1, 3,
[[Use chatcommands to manage your arena and simulation.
Send "/help cells" in chat to see further help.]],
3, 2, "Arenas",
1, 3,
[[Arenas are areas delimited by undestructible borders.
Only their owners can modify them.]],
3, 2, "Cells",
1, 3, "Cells live in your arenas. You can place & dig them at any time.",
3, 2, "Wand",
1, 3,
[[A powerful tool controlling the simulation.
Right-click to configure, left-click to apply.
Possible modes / actions are:
- Advance: Simulates steps
- Simulate: Starts / stops simulation, steps per second
- Place: Living cell ray, steps are length
- Dig: Dead cell ray, steps are length
Rules work as follows:
- Short notation: As described by Bayes. Uses base 27.
- Neighbors: Numbers signify the amount of neighbors.]],
}
for i = 1, #_help_content, 3 do
_help_content[i] = ({ "#FFFFFF", colors.cell.fill, colors.cell.edge })[_help_content[i]]
end
local help_content = {}
for i = 1, #_help_content, 3 do
local parts = modlib.text.split(_help_content[i + 2], "\n")
for _, part in ipairs(parts) do
table.insert(help_content, _help_content[i])
table.insert(help_content, _help_content[i + 1])
table.insert(help_content, minetest.formspec_escape(part))
end
end
help_formspec = ([[
size[8,5]
real_coordinates[true]
box[0,0;8,1;%s]
label[0.25,0.35;%sCellestial%s - cellular automata for Minetest]
label[0.25,0.7;%shttps://appgurueu.github.io/cellestial]
tablecolumns[color;tree;text]
tableoptions[background=#00000000;highlight=#00000000;border=false;opendepth=2]
table[-0.15,1.25;7.9,3.5;help;%s]
image_button_exit[7.25,0.25;0.5,0.5;cmdlib_cross.png;close;]
]]):format(colors.cell.fill, ces(colors.cell.edge), ces("#FFFFFF"), ces(colors.cell.edge), table.concat(help_content, ","))
function show_help(name)
minetest.show_formspec(name, "cellestial:help", help_formspec)
end
-- Almost indestructible borders
minetest.register_node("cellestial:border", {
description = "Arena Border",
post_effect_color = colors.border.fill,
sunlight_propagates = true,
light_source = minetest.LIGHT_MAX,
tiles = { border },
groups = { not_in_creative_inventory = 1 },
can_dig = function()
return false
end,
on_dig = function()
end,
on_place = function()
end,
on_use = function()
end,
on_secondary_use = function()
end
})
-- Cells, item can be used for digging & placing
minetest.register_node("cellestial:cell", {
description = "Cell",
post_effect_color = colors.cell.fill,
sunlight_propagates = true,
light_source = minetest.LIGHT_MAX,
tiles = { cell },
groups = { oddly_breakable_by_hand = 3 },
range = (creative and 20) or 4,
on_dig = function(pos, node, digger)
if minetest.is_protected(pos, digger:get_player_name()) then
return
end
local arena = arena.get(pos)
if arena and arena:is_owner(digger:get_player_name()) then
arena:set_cell(pos)
else
minetest.set_node(pos, { name = "air" })
end
if not creative then
local leftover = digger:get_inventory():add_item("main", "cellestial:cell")
if leftover then
minetest.add_item(pos, leftover)
end
end
end,
on_place = function(itemstack, placer, pointed_thing)
local pos = pointed_thing.above
if not conf.place_inside_player then
for _, player in pairs(minetest.get_connected_players()) do
local ppos = player:get_pos()
ppos.y = ppos.y + player:get_properties().eye_height
if ppos.x >= pos.x and ppos.y >= pos.y and ppos.z >= pos.z and ppos.x <= pos.x +1 and ppos.y <= pos.y + 1 and ppos.z <= pos.z + 1 then
return itemstack
end
end
end
if minetest.is_protected(pos, placer:get_player_name()) then
return
end
local arena = arena.get(pos)
if arena and arena:is_owner(placer:get_player_name()) then
arena:set_cell(pos, true)
else
minetest.set_node(pos, { name = "cellestial:cell" })
end
if not creative then
itemstack:take_item()
return itemstack
end
end
})
local serialized_modes = { advance = "a", simulate = "s", place = "p", dig = "d" }
local function serialize_rule(rule)
local number = 0
for i = 26, 0, -1 do
number = number * 2
if rule[i] then
number = number + 1
end
end
return modlib.number.tostring(number, 36)
end
function serialize_wand(wand, meta)
meta:set_string("mode", serialized_modes[wand.mode])
meta:set_string("steps", modlib.number.tostring(wand.steps, 36))
meta:set_string("death", serialize_rule(wand.rule.death))
meta:set_string("birth", serialize_rule(wand.rule.birth))
end
local deserialized_modes = modlib.table.flip(serialized_modes)
local function deserialize_rule(text)
local number = tonumber(text, 36)
local rule = {}
for i = 0, 26 do
local digit = math.floor(number % 2)
rule[i] = digit == 1
number = math.floor(number / 2)
end
return rule
end
function deserialize_mode(meta)
return deserialized_modes[meta:get("mode")]
end
function deserialize_steps(meta)
return tonumber(meta:get("steps"), 36)
end
function deserialize_full_rule(meta)
return { death = deserialize_rule(meta:get("death")), birth = deserialize_rule(meta:get("birth")) }
end
function deserialize_wand(meta)
return {
mode = deserialize_mode(meta),
steps = deserialize_steps(meta),
rule = deserialize_full_rule(meta)
}
end
local c0, ca, cA = ("0"):byte(), ("a"):byte(), ("A"):byte()
function read_rule(text)
if text:len() ~= 4 then
return nil
end
local nums = { text:byte(1), text:byte(2), text:byte(3), text:byte(4) }
for i, num in pairs(nums) do
if num >= ca then
num = num - ca + 10
elseif num >= cA then
num = num - cA + 10
else
num = num - c0
end
if num < 0 or num > 26 then
return nil
end
nums[i] = num
end
if nums[1] > nums[2] or nums[3] > nums[4] then
return nil
end
local min_env, max_env, min_birth, max_birth = unpack(nums)
local rule = { death = {}, birth = {} }
for i = 0, 26 do
rule.death[i] = not (i >= min_env and i <= max_env)
rule.birth[i] = i >= min_birth and i <= max_birth
end
return rule
end
local dfunc = modlib.number.default_digit_function
function find_rule(rule)
local death, birth = rule.death, rule.birth
-- Finding min. env. and max. env
local min_env, max_env
local i = 0
while i <= 26 and death[i] do
i = i + 1
end
min_env = i
while i <= 26 and not death[i + 1] do
i = i + 1
end
max_env = i
for i = max_env + 1, 26 do
if not death[i] then
return
end
end
-- Finding min. birth and max. birth
local min_birth, max_birth
i = 0
while i <= 26 and not birth[i] do
i = i + 1
end
min_birth = i
while i <= 26 and birth[i + 1] do
i = i + 1
end
max_birth = i
for i = max_birth + 1, 26 do
if birth[i] then
return
end
end
return dfunc(min_env) .. dfunc(max_env) .. dfunc(min_birth) .. dfunc(max_birth)
end
local default_wand = {
mode = "advance",
steps = 1,
rule = read_rule("5766")
}
local ray_steps = 10
function ray_function(cell)
return function(steps, player, arena)
local eye_offset = player:get_eye_offset()
eye_offset.y = eye_offset.y + player:get_properties().eye_height
local lookdir = player:get_look_dir()
local start = vector.add(vector.add(player:get_pos(), eye_offset), lookdir)
local step = vector.multiply(lookdir, 1 / ray_steps)
local set = {}
local set_count = 0
local pos = start
for _ = 1, ray_steps * steps * math.sqrt(3) do
local rounded = vector.round(pos)
local min, max = arena.min, arena.max
if rounded.x <= min.x or rounded.y <= min.y or rounded.z <= min.z or rounded.x >= max.x or rounded.y >= max.y or rounded.z >= max.z then
break
end
local index = arena.voxelarea:indexp(rounded)
if not set[index] then
set[index] = true
arena:set_cell(rounded, cell)
set_count = set_count + 1
if set_count == steps then
break
end
end
pos = vector.add(pos, step)
end
end
end
actions = {
advance = function(steps, _, arena, meta)
arena:next_steps(steps, deserialize_full_rule(meta))
end,
simulate = function(steps, _, arena, meta)
arena:simulate(steps, deserialize_full_rule(meta))
end,
place = ray_function(true),
dig = ray_function()
}
function show_wand_formspec(name, wand)
local function get_image(n)
if wand.rule.death[n] then
if wand.rule.birth[n] then
return "cellestial_fertility.png"
end
return "cellestial_border.png"
else
if wand.rule.birth[n] then
return "cellestial_cell.png"
end
return "cellestial_environment.png"
end
end
local neighbor_buttons = {
"image_button[5.25,1.25;0.5,0.5;" .. get_image(0) .. ";n0;0;false;false]",
"image_button[6.25,1.25;0.5,0.5;" .. get_image(1) .. ";n1;1;false;false]",
"image_button[7.25,1.25;0.5,0.5;" .. get_image(2) .. ";n2;2;false;false]"
}
for y = 0, 2 do
for x = 0, 7 do
local n = y * 8 + x + 3
local t = get_image(n)
table.insert(neighbor_buttons, ("image_button[%s,%s;0.5,0.5;%s;n%d;%d;false;false]"):format(tostring(0.25 + x * 1), tostring(2 + y * 0.75), t, n, n))
end
end
neighbor_buttons = table.concat(neighbor_buttons, "\n")
minetest.show_formspec(name, "cellestial:wand",
([[
size[8,5]
real_coordinates[true]
box[0,0;8,1;%s]
label[0.25,0.5;Mode:]
dropdown[1,0.25;1.5,0.5;mode;Advance,Simulate,Place,Dig;%d]
label[2.75,0.5;Steps:]
button[3.5,0.25;0.5,0.5;steps_minus;-]
field[4,0.25;0.75,0.5;steps;;%d]
field_close_on_enter[steps;false]
button[4.75,0.25;0.5,0.5;steps_plus;+]
button[5.75,0.25;1,0.5;apply;Apply]
image_button_exit[7.25,0.25;0.5,0.5;cmdlib_cross.png;close;]
label[0.25,1.5;Rule:]
field[1,1.25;1,0.5;rule;;%s]
button[2.25,1.25;1,0.5;set;Set]
label[3.75,1.5;Neighbors:]
%s
image[0.25,4.25;0.5,0.5;cellestial_border.png]
label[1,4.5;Death]
image[2.25,4.25;0.5,0.5;cellestial_environment.png]
label[3,4.5;Survival]
image[4.25,4.25;0.5,0.5;cellestial_fertility.png]
label[5,4.5;Birth]
image[6.25,4.25;0.5,0.5;cellestial_cell.png]
label[7,4.5;Both]
]]):format(colors.cell.fill, ({ advance = 1, simulate = 2, place = 3, dig = 4 })[wand.mode], wand.steps, find_rule(wand.rule) or "", neighbor_buttons))
end
function ensure_wand(meta)
if not meta:get("mode") or not meta:get("steps") or not meta:get("death") or not meta:get("birth") then
serialize_wand(default_wand, meta)
return true
end
end
function obtain_wand(meta)
local wand
if ensure_wand(meta) then
wand = modlib.table.tablecopy(default_wand)
else
wand = deserialize_wand(meta)
end
return wand
end
function wand_on_secondary_use(itemstack, user, pointed_thing)
local name = user:get_player_name()
local meta = itemstack:get_meta()
show_wand_formspec(name, obtain_wand(meta))
return itemstack
end
-- Wand
minetest.register_tool("cellestial:wand", {
description = "Cellestial Wand",
inventory_image = "cellestial_wand.png",
on_use = function(itemstack, user, pointed_thing)
local name = user:get_player_name()
local arena = arena.get_by_name(name)
if arena and arena:is_owner(name) then
local meta = itemstack:get_meta()
ensure_wand(meta)
local mode = deserialize_mode(meta)
actions[mode](deserialize_steps(meta), user, arena, meta)
end
return itemstack
end,
on_secondary_use = wand_on_secondary_use,
on_place = wand_on_secondary_use
})
modlib.minetest.register_form_listener("cellestial:wand", function(player, fields)
if fields.quit then
return
end
local wielded_item = player:get_wielded_item()
local meta = wielded_item:get_meta()
local wand = obtain_wand(meta)
if fields.steps then
local steps = tonumber(fields.steps)
if steps then
wand.steps = steps
end
end
if fields.mode then
local lower = fields.mode:lower()
if serialized_modes[lower] then
wand.mode = lower
end
end
if fields.apply then
local arena = arena.get_by_player(player)
if arena and arena:is_owner(player:get_player_name()) then
actions[wand.mode](wand.steps, player, arena, meta)
end
elseif fields.set or fields.key_enter_field == "rule" then
local rule = read_rule(fields.rule)
if rule then
wand.rule = rule
end
elseif fields.steps_minus then
wand.steps = wand.steps - 1
elseif fields.steps_plus then
wand.steps = wand.steps + 1
else
for field, _ in pairs(fields) do
if modlib.text.starts_with(field, "n") then
local n = tonumber(field:sub(2))
if n then
if wand.rule.birth[n] then
if wand.rule.death[n] then
wand.rule.death[n] = false
else
wand.rule.death[n] = true
wand.rule.birth[n] = false
end
else
if wand.rule.death[n] then
wand.rule.death[n] = false
else
wand.rule.death[n] = true
wand.rule.birth[n] = true
end
end
end
break
end
end
end
wand.steps = math.max(1, math.min(wand.steps, max_steps))
serialize_wand(wand, meta)
player:set_wielded_item(wielded_item)
if not fields.close then
show_wand_formspec(player:get_player_name(), wand)
end
end)
local adv_chat = minetest.global_exists("adv_chat") and adv_chat
minetest.register_on_joinplayer(function(player)
arena.get(player:get_pos())
local name = player:get_player_name()
for _, id in pairs(arena.list_ids_by_name(name)) do
local role = "#" .. id
if adv_chat and adv_chat.roles[role] then
adv_chat.add_role(name, role)
end
end
end)
if adv_chat then
adv_chat.roles.minetest.color = colors.cell.fill
end

6
mod.conf Normal file
View File

@ -0,0 +1,6 @@
name = cellestial
title = Cellular Automata
description = Simulates 3D cellular automata
depends = modlib, cmdlib, hud_timers
optional_depends = adv_chat, cellestiall
author = LMD aka appguru(eu)

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B