cellestial/main.lua

499 lines
16 KiB
Lua

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, fall_damage_add_percent = -100 },
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",
-- TODO find a proper way for borders connecting to cells
post_effect_color = "#00000000",
sunlight_propagates = true,
light_source = minetest.LIGHT_MAX,
tiles = { cell },
groups = { oddly_breakable_by_hand = 3, fall_damage_add_percent = -100 },
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
return minetest.node_dig(pos, node, digger)
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_content(pos) and arena:is_owner(placer:get_player_name()) then
arena:set_cell(pos, true)
elseif regular_placing then
return minetest.item_place_node(itemstack, placer, pointed_thing)
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