Initial release 🥳
|
@ -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
|
||||
}
|
||||
}
|
||||
```
|
|
@ -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
|
||||
)
|
|
@ -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>"
|
||||
)
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
After Width: | Height: | Size: 112 KiB |
After Width: | Height: | Size: 131 B |
After Width: | Height: | Size: 126 B |
After Width: | Height: | Size: 120 B |
After Width: | Height: | Size: 115 B |
After Width: | Height: | Size: 117 B |
After Width: | Height: | Size: 117 B |
After Width: | Height: | Size: 195 B |