cartographer/table.lua
2020-04-26 10:21:24 -04:00

695 lines
26 KiB
Lua

local MAP_SIZE = 40;
local SCALE_SMALL = 1;
local SCALE_MEDIUM = 2;
local SCALE_LARGE = 4;
local SCALE_HUGE = 8;
-- Draw background elements in the same arrangement as inventory slots
-- x: The x position of the inventory
-- y: The y position of the inventory
-- cols: The width of the inventory, in columns
-- rows: The height of the inventory, in rows
-- skin: A 9-slice background skin table
--
-- Returns a formspec string
local function inventory_bg(x, y, cols, rows, skin)
local data = "";
for i = 0,cols - 1 do
for j = 0,rows - 1 do
data = data .. string.format("background9[%f,%f;1,1;%s.png;false;%s]",
x + (i * 1.25), y + (j * 1.25),
skin.texture,
tostring(skin.radius));
end
end
return data;
end
-- Get the material cost for the given map scale and detail level
-- scale: The map scale
-- detail: The detail level
local function get_material_cost(scale, detail)
local paper = scale * 4;
local pigment = detail * 5;
if scale == SCALE_SMALL then
pigment = pigment + 5;
elseif scale == SCALE_MEDIUM then
pigment = pigment + 10;
elseif scale == SCALE_LARGE then
pigment = pigment + 15;
elseif scale == SCALE_HUGE then
pigment = pigment + 20;
end
return {
paper = math.max(paper, 0),
pigment = math.max(pigment, 0),
};
end
-- Get the material cost of the craft settings from the given table metadata
-- meta: The metadata to read
--
-- Returns a table with the material costs, and a boolean indicating if the
-- costs were positive or negative before clamping.
local function get_craft_material_cost(meta)
local cost = get_material_cost(meta:get_int("scale") or SCALE_SMALL, meta:get_int("detail") or 0);
local stack = meta:get_inventory():get_stack("output", 1);
local is_positive = true;
if stack:get_name() == "cartographer:map" then
local smeta = stack:get_meta();
local sub_cost = get_material_cost(smeta:get_int("cartographer:scale") or SCALE_SMALL, (smeta:get_int("cartographer:detail") or 1) - 1);
is_positive = cost.paper >= sub_cost.paper and cost.pigment >= sub_cost.pigment;
cost.paper = math.max(cost.paper - sub_cost.paper, 0);
cost.pigment = math.max(cost.pigment - sub_cost.pigment, 0);
end
return cost, is_positive;
end
-- Check if the given table metadata has enough materials to cover the given
-- cost table.
-- cost: A table of material costs
-- meta: The metadata
--
-- Returns true if the table's materials can cover the cost
local function can_afford(cost, meta)
return cost.paper + cost.pigment > 0
and cost.paper <= meta:get_int("paper")
and cost.pigment <= meta:get_int("pigment");
end
-- Get the material cost of the copy settings from the given table metadata
-- meta: The metadata to read
--
-- Returns a table with the material costs
local function get_copy_material_cost(meta)
local inv = meta:get_inventory();
local in_stack = inv:get_stack("copy_input", 1);
local out_stack = inv:get_stack("copy_output", 1);
if out_stack:is_empty() and in_stack:get_name() == "cartographer:map" then
local smeta = in_stack:get_meta();
local scale = smeta:get_int("cartographer:scale") or SCALE_SMALL;
local detail = smeta:get_int("cartographer:detail") or 1;
return get_material_cost(size, detail - 1);
end
return {
paper = 0,
pigment = 0,
};
end
-- Get the converted material value of the given itemstack
-- stack: The itemstack to convert
--
-- Returns a table with the material values
function cartographer.get_material_value(stack)
local item_name = stack:get_name();
local item_count = stack:get_count();
for name,mats in pairs(_cartographer.materials_by_name) do
if name == item_name then
return {
paper = (mats.paper or 0) * item_count,
pigment = (mats.pigment or 0) * item_count,
}
end
end
for group,mats in pairs(_cartographer.materials_by_group) do
if minetest.get_item_group(item_name, group) ~= 0 then
return {
paper = (mats.paper or 0) * item_count,
pigment = (mats.pigment or 0) * item_count,
}
end
end
return {
paper = 0,
pigment = 0,
};
end
local fs = {};
-- Draw a button, with support for enabled/disabled states
-- x: The x position of the button
-- y: The y position of the button
-- w: The width of the button
-- h: The height of the button
-- id: The element id
-- text: The text to display in the button
-- enabled: Whether or not the button is enabled
--
-- Returns a formspec string
function fs.button(x, y, w, h, id, text, enabled)
if enabled then
return string.format("button[%f,%f;%f,%f;%s;%s]", x, y, w, h, id, text);
end
return string.format("button[%f,%f;%f,%f;disabled_button;%s]", x, y, w, h, text);
end
-- Draw a 1px thick horizontal separator formspec element
-- y: The y position of the separator
-- skin: A 9-slice background skin table
--
-- Returns a formspec string
function fs.separator(y, skin)
return string.format("background9[0.1,%f;10.05,0.01;%s.png;false;%s]", y, skin.texture, tostring(skin.radius))
end
-- Draw all the essential formspec data (size, background, styles, tabs)
-- w: The width of the formspec
-- h: The height of the formspec
-- rank: An into defining the 'rank' of the table being displayed
-- tab: An int defining the index of the selected tab
-- skin: A formspec skin table
--
-- Returns a formspec string
function fs.header(w, h, rank, tab, skin)
local data = "formspec_version[3]"
.. string.format("size[%f,%f]", w, h)
.. string.format("background9[-0.1,0;1,1;%s.png;true;%s]", skin.background.texture, tostring(skin.background.radius))
.. string.format("background9[0.0625,0.125;%f,%f;%s.png;false;%s]", w - 0.125, h - 0.25, skin.inner_background.texture, tostring(skin.inner_background.radius))
.. string.format("style_type[button;noclip=true;border=false;bgimg=%s.png;bgimg_hovered=%s.png;bgimg_pressed=%s.png;bgimg_middle=%s;textcolor=%s]", skin.tab.texture, skin.tab.hovered_texture, skin.tab.pressed_texture, tostring(skin.tab.radius), skin.tab.font_color)
.. string.format("style[tab%d;noclip=true;border=false;bgimg=%s.png;bgimg_hovered=%s.png;bgimg_pressed=%s.png;bgimg_middle=%s;textcolor=%s]", tab, skin.tab.selected_texture, skin.tab.selected_texture, skin.tab.selected_texture, tostring(skin.tab.radius), skin.tab.font_color)
.. string.format("button[0.25,-0.425;1.5,0.55;tab1;Materials]", tab)
.. string.format("button[1.75,-0.425;1.5,0.55;tab2;Create Map]", tab);
if rank >= 2 then
data = data .. string.format("button[3.25,-0.425;1.5,0.55;tab3;Copy Map]", tab);
end
return data .. string.format("style_type[button;border=false;bgimg=%s.png;bgimg_hovered=%s.png;bgimg_pressed=%s.png;bgimg_middle=%s;textcolor=%s]", skin.button.texture, skin.button.hovered_texture, skin.button.pressed_texture, tostring(skin.button.radius), skin.button.font_color)
.. string.format("style[disabled_button;bgimg=;bgimg_hovered=;bgimg_pressed=;textcolor=%s]", skin.button.disabled_font_color);
end
-- Draw material counters from a table's metadata
-- x: The x position of the labels
-- y: The y position of the labels
-- meta: A metadata object containing the material quantities
-- skin: A formspec skin table
--
-- Returns a formspec string
function fs.materials(x, y, meta, skin)
return string.format("container[%f,%f]", x, y)
.. "formspec_version[3]"
.. string.format("background9[0,0.125;1,0.25;%s.png;false;%s]", skin.label.texture, tostring(skin.label.radius))
.. string.format("image[0.125,0.125;0.25,0.25;%s.png]", skin.paper_texture)
.. string.format("label[0.375,0.25;%sx %d]", minetest.get_color_escape_sequence(skin.label.font_color), meta:get_int("paper"))
.. string.format("background9[1.25,0.125;1,0.25;%s.png;false;%s]", skin.label.texture, tostring(skin.label.radius))
.. string.format("image[1.375,0.125;0.25,0.25;%s.png]", skin.pigment_texture)
.. string.format("label[1.625,0.25;%sx %d]", minetest.get_color_escape_sequence(skin.label.font_color), meta:get_int("pigment"))
.. "container_end[]";
end
-- Draw a label with material costs from a table
-- x: The x position of the interface
-- y: The y position of the interface
-- cost: A table of material costs, with string keys for the material
-- names and iteger values
-- skin: A formspec skin table
--
-- Returns a formspec string
function fs.cost(x, y, cost, skin)
local data = string.format("background9[%f,%f;1,0.5;%s.png;false;%s]", x, y - 0.125, skin.label.texture, tostring(skin.label.radius));
local i = 0;
for name,value in pairs(cost) do
if name == "paper" then
data = data .. string.format("image[%f,%f;0.25,0.25;%s.png]", x + 0.125, y + (i * 0.25) - 0.125, skin.paper_texture)
elseif name == "pigment" then
data = data .. string.format("image[%f,%f;0.25,0.25;%s.png]", x + 0.125, y + (i * 0.25) - 0.125, skin.pigment_texture)
end
data = data .. string.format("label[%f,%f;%sx %d]", x + 0.375, y + (i * 0.25), minetest.get_color_escape_sequence(skin.label.font_color), value);
i = i + 1;
end
return data;
end
-- Draw the material conversion tab UI
-- x: The x position of the interface
-- y: The y position of the interface
-- pos: The table position (for displaying the inventory)
-- skin: A formspec skin table
--
-- Returns a formspec string
function fs.convert(x, y, pos, skin)
local meta = minetest.get_meta(pos);
local value = cartographer.get_material_value(meta:get_inventory():get_stack("input", 1));
return string.format("container[%f,%f]", x, y)
.. "formspec_version[3]"
.. inventory_bg(0, 0, 1, 1, skin.slot)
.. string.format("list[nodemeta:%d,%d,%d;input;0,0;1,1;]", pos.x, pos.y, pos.z)
.. "tooltip[0,0;1,1;Place items here to convert\nthem into mapmaking materials]"
.. fs.button(2.5, 0.25, 2, 0.5, "convert", "Convert Materials", value.paper + value.pigment > 0)
.. fs.cost(1.25, 0.375, value, skin)
.. "container_end[]";
end
-- Draw the map crafting tab UI
-- x: The x position of the interface
-- y: The y position of the interface
-- pos: The table position (for displaying the inventory)
-- meta: A metadata object containing the table settings and material
-- quantities
-- skin: A formspec skin table
--
-- Returns a formspec string
function fs.craft(x, y, pos, rank, meta, skin)
local cost, is_positive = get_craft_material_cost(meta);
local data = string.format("container[%f,%f]", x, y)
.. "formspec_version[3]"
.. inventory_bg(0, 1, 1, 1, skin.slot)
.. string.format("list[nodemeta:%d,%d,%d;output;0,1;1,1;]", pos.x, pos.y, pos.z)
.. "tooltip[0,1;1,1;Place a map here to upgrade it,\nor leave empty to craft]"
.. fs.button(2.5, 1.25, 2, 0.5, "craft", "Craft Map", is_positive and can_afford(cost, meta))
.. fs.cost(1.25, 1.375, cost, skin);
if rank > 1 then
data = data .. string.format("style[%dx;bgimg=%s.png;bgimg_hovered=%s.png;bgimg_pressed=%s.png]", meta:get_int("scale"), skin.button.selected_texture, skin.button.selected_texture, skin.button.selected_texture)
.. string.format("label[2.5,0;%sMap Scale]", minetest.get_color_escape_sequence(skin.label.font_color))
.. "button[2.5,0.25;0.5,0.5;1x;1x]"
.. "button[3.0,0.25;0.5,0.5;2x;2x]";
if rank > 2 then
data = data .. "button[3.5,0.25;0.5,0.5;4x;4x]"
.. "button[4.0,0.25;0.5,0.5;8x;8x]";
end
end
data = data .. string.format("style[%d;bgimg=%s.png;bgimg_hovered=%s.png;bgimg_pressed=%s.png]", meta:get_int("detail") + 1, skin.button.selected_texture, skin.button.selected_texture, skin.button.selected_texture)
.. string.format("label[0,0;%sDetail Level]", minetest.get_color_escape_sequence(skin.label.font_color))
.. "button[0.0,0.25;0.5,0.5;1;1]"
.. "button[0.5,0.25;0.5,0.5;2;2]";
if rank > 1 then
data = data .. "button[1.0,0.25;0.5,0.5;3;3]";
if rank > 2 then
data = data .. "button[1.5,0.25;0.5,0.5;4;4]";
end
end
return data .. "container_end[]";
end
-- Draw the map copying tab UI
-- x: The x position of the interface
-- y: The y position of the interface
-- pos: The table position (for displaying the inventory)
-- skin: A formspec skin table
--
-- Returns a formspec string
function fs.copy(x, y, pos, skin)
local meta = minetest.get_meta(pos);
local costs = get_copy_material_cost(meta);
return string.format("container[%f,%f]", x, y)
.. "formspec_version[3]"
.. inventory_bg(0, 0, 1, 1, skin.slot)
.. string.format("list[nodemeta:%d,%d,%d;copy_input;0,0;1,1;]", pos.x, pos.y, pos.z)
.. fs.button(2.5, 0.25, 2, 0.5, "copy", "Copy Map", can_afford(costs, meta))
.. fs.cost(1.25, 0.375, costs, skin)
.. inventory_bg(8.75, 0, 1, 1, skin.slot)
.. string.format("list[nodemeta:%d,%d,%d;copy_output;8.75,0;1,1;]", pos.x, pos.y, pos.z)
.. "container_end[]";
end
-- Draw the player's inventory
-- x: The x position of the inventory
-- y: The y position of the inventory
-- skin: A formspec skin table
--
-- Returns a formspec string
function fs.inv(x, y, skin)
return string.format("container[%f,%f]", x, y)
.. "formspec_version[3]"
.. inventory_bg(0, 0, 8, 4, skin.slot)
.. "list[current_player;main;0,0;8,4;]"
.. "container_end[]";
end
local player_tables = {};
-- Show the table formspec to the specified player
-- The player must be recorded in player_tables in order to receive
-- a formspec.
--
-- player: The player's name
local function table_formspec(player)
local data = player_tables[player];
local pos = data.pos;
if not pos then
return;
end
local meta = minetest.get_meta(pos);
local rank = 1;
local skin = cartographer.skin.table_skins.simple_table;
local name = minetest.get_node(pos).name;
if name == "cartographer:standard_table" then
rank = 2;
skin = cartographer.skin.table_skins.standard_table;
elseif name == "cartographer:advanced_table" then
rank = 3;
skin = cartographer.skin.table_skins.advanced_table;
end
if data.tab == 1 then
minetest.show_formspec(player, "simple_table",
fs.header(10.25, 7.375, rank, data.tab, skin) ..
fs.materials(0.25, 0.1875, meta, skin) ..
fs.separator(0.6875, skin.separator) ..
fs.convert(0.25, 0.875, pos, skin) ..
fs.separator(2.125, skin.separator) ..
fs.inv(0.25, 2.375, skin)
);
elseif data.tab == 2 then
minetest.show_formspec(player, "simple_table",
fs.header(10.25, 8.25, rank, data.tab, skin) ..
fs.materials(0.25, 0.1875, meta, skin) ..
fs.separator(0.6875, skin.separator) ..
fs.craft(0.25, 0.875, pos, rank, meta, skin) ..
fs.separator(3, skin.separator) ..
fs.inv(0.25, 3.25, skin)
);
elseif data.tab == 3 then
minetest.show_formspec(player, "simple_table",
fs.header(10.25, 7.375, rank, data.tab, skin) ..
fs.materials(0.25, 0.1875, meta, skin) ..
fs.separator(0.6875, skin.separator) ..
fs.copy(0.25, 0.875, pos, skin) ..
fs.separator(2.125, skin.separator) ..
fs.inv(0.25, 2.375, skin)
);
end
end
-- Called when a player sends input to the server from a formspec
-- This callback handles player input in the table formspec
-- player: The player who sent the input
-- name: The formspec name
-- fields: A table containing the input
minetest.register_on_player_receive_fields(function(player, name, fields)
if name == "simple_table" then
local meta = minetest.get_meta(player_tables[player:get_player_name()].pos);
if fields["convert"] then
local inv = meta:get_inventory();
local stack = inv:get_stack("input", 1);
local value = cartographer.get_material_value(stack);
if value.paper + value.pigment > 0 then
meta:set_int("paper", meta:get_int("paper") + value.paper);
meta:set_int("pigment", meta:get_int("pigment") + value.pigment);
inv:set_stack("input", 1, ItemStack(nil));
end
elseif fields["craft"] then
local size = meta:get_int("size");
local detail = meta:get_int("detail");
local scale = meta:get_int("scale");
local cost, is_positive = get_craft_material_cost(meta);
if is_positive and can_afford(cost, meta) then
meta:set_int("paper", meta:get_int("paper") - cost.paper);
meta:set_int("pigment", meta:get_int("pigment") - cost.pigment);
local inv = meta:get_inventory();
local stack = inv:get_stack("output", 1);
if stack:is_empty() then
inv:set_stack("output", 1, cartographer.create_map_item(size, 1 + detail, scale));
else
local smeta = stack:get_meta();
smeta:set_int("cartographer:detail", 1 + detail);
cartographer.resize_map_item(smeta, size);
local map = cartographer.get_map(smeta:get_int("cartographer:map_id"));
if map then
map.detail = 1 + detail;
end
inv:set_stack("output", 1, stack);
end
cartographer.map_sound("cartographer_write", player);
end
elseif fields["copy"] then
local cost = get_copy_material_cost(meta);
if can_afford(cost, meta) then
meta:set_int("paper", meta:get_int("paper") - cost.paper);
meta:set_int("pigment", meta:get_int("pigment") - cost.pigment);
cartographer.map_sound("cartographer_write", player);
local inv = meta:get_inventory();
inv:set_stack("copy_output", 1, cartographer.copy_map_item(inv:get_stack("copy_input", 1)));
end
elseif fields["1"] then
meta:set_int("detail", 0);
elseif fields["2"] then
meta:set_int("detail", 1);
elseif fields["3"] then
meta:set_int("detail", 2);
elseif fields["4"] then
meta:set_int("detail", 3);
elseif fields["1x"] then
meta:set_int("scale", SCALE_SMALL);
elseif fields["2x"] then
meta:set_int("scale", SCALE_MEDIUM);
elseif fields["4x"] then
meta:set_int("scale", SCALE_LARGE);
elseif fields["8x"] then
meta:set_int("scale", SCALE_HUGE);
elseif fields["tab1"] then
player_tables[player:get_player_name()].tab = 1;
cartographer.map_sound("cartographer_turn_page", player);
elseif fields["tab2"] then
player_tables[player:get_player_name()].tab = 2;
cartographer.map_sound("cartographer_turn_page", player);
elseif fields["tab3"] then
player_tables[player:get_player_name()].tab = 3;
cartographer.map_sound("cartographer_turn_page", player);
end
if not fields["quit"] then
table_formspec(player:get_player_name());
end
end
end);
-- Called after a table is placed. Sets up the table's inventory and metadata.
-- pos: The node's position
local function setup_table_node(pos)
local meta = minetest.get_meta(pos);
meta:get_inventory():set_size("input", 1);
meta:get_inventory():set_size("output", 1);
meta:get_inventory():set_size("copy_input", 1);
meta:get_inventory():set_size("copy_output", 1);
meta:set_int("size", MAP_SIZE);
meta:set_int("scale", SCALE_SMALL);
meta:set_int("detail", 0);
end
-- Called when the player tries to put an item into one of the table's
-- inventories.
-- listname: The name of the inventory the item is being placed in.
-- stack: The itemstack
--
-- Returns 0 if the place is invalid; otherwise, returns the number of items
-- that can be placed.
local function table_can_put(_, listname, _, stack, _)
if listname == "copy_output" then
return 0;
end
if stack:get_name() ~= "cartographer:map" and (listname == "output" or listname == "copy_input") then
return 0;
end
return stack:get_count();
end
-- Called when the player tries to move an item between two of the table's
-- inventories.
-- to_list: The name of the inventory the item is being placed in.
-- count: The number of items being moved
--
-- Returns 0 if the move is invalid; otherwise, returns the number of items
-- that can be moved.
local function table_can_move(_, _, _, to_list, _, count, _)
if to_list == "copy_output" then
return 0;
end
return count;
end
-- Called when a change occurs in a table's inventory
-- pos: The node's position
-- listname: The name of the changed inventory list
local function table_on_items_changed(pos, listname, _, _, _)
for player, data in pairs(player_tables) do
if vector.equals(pos, data.pos) and (
(data.tab == 1 and listname == "input")
or (data.tab == 2 and listname == "output")
or (data.tab == 3 and listname == "copy_input")) then
table_formspec(player);
end
end
end
minetest.register_node("cartographer:simple_table", {
description = "Shabby Cartographer's Table",
drawtype = "mesh",
mesh = cartographer.skin.table_skins.simple_table.node_mesh,
tiles = { cartographer.skin.table_skins.simple_table.node_texture },
paramtype2 = "facedir",
groups = {
choppy = 2,
oddly_breakable_by_hand = 2,
},
selection_box = {
type = "fixed",
fixed = {
{-0.5, -0.5, -0.375, 0.5, 0.6875, 0.375},
},
},
collision_box = {
type = "fixed",
fixed = {
{-0.5, -0.5, -0.375, 0.5, 0.6875, 0.375},
},
},
on_rightclick = function(pos, node, player, itemstack, pointed_thing)
player_tables[player:get_player_name()] = {
pos = minetest.get_pointed_thing_position(pointed_thing),
tab = 1,
};
cartographer.map_sound("cartographer_open_map", player);
table_formspec(player:get_player_name())
end,
after_place_node = setup_table_node,
allow_metadata_inventory_move = table_can_move,
allow_metadata_inventory_put = table_can_put,
on_metadata_inventory_put = table_on_items_changed,
on_metadata_inventory_take = table_on_items_changed,
});
minetest.register_node("cartographer:standard_table", {
description = "Simple Cartographer's Table",
drawtype = "mesh",
mesh = cartographer.skin.table_skins.standard_table.node_mesh,
tiles = { cartographer.skin.table_skins.standard_table.node_texture },
paramtype2 = "facedir",
groups = {
choppy = 2,
oddly_breakable_by_hand = 2,
},
selection_box = {
type = "fixed",
fixed = {
{-0.5, -0.5, -0.375, 0.5, 0.6875, 0.375},
},
},
collision_box = {
type = "fixed",
fixed = {
{-0.5, -0.5, -0.375, 0.5, 0.6875, 0.375},
},
},
on_rightclick = function(pos, node, player, itemstack, pointed_thing)
player_tables[player:get_player_name()] = {
pos = minetest.get_pointed_thing_position(pointed_thing),
tab = 1,
};
cartographer.map_sound("cartographer_open_map", player);
table_formspec(player:get_player_name())
end,
after_place_node = setup_table_node,
allow_metadata_inventory_move = table_can_move,
allow_metadata_inventory_put = table_can_put,
on_metadata_inventory_put = table_on_items_changed,
on_metadata_inventory_take = table_on_items_changed,
});
minetest.register_node("cartographer:advanced_table", {
description = "Advanced Cartographer's Table",
drawtype = "mesh",
mesh = cartographer.skin.table_skins.advanced_table.node_mesh,
tiles = { cartographer.skin.table_skins.advanced_table.node_texture },
paramtype2 = "facedir",
groups = {
choppy = 2,
oddly_breakable_by_hand = 2,
},
selection_box = {
type = "fixed",
fixed = {
{-0.5, -0.5, -0.375, 0.5, 0.6875, 0.375},
},
},
collision_box = {
type = "fixed",
fixed = {
{-0.5, -0.5, -0.375, 0.5, 0.6875, 0.375},
},
},
on_rightclick = function(pos, node, player, itemstack, pointed_thing)
player_tables[player:get_player_name()] = {
pos = minetest.get_pointed_thing_position(pointed_thing),
tab = 1,
};
cartographer.map_sound("cartographer_open_map", player);
table_formspec(player:get_player_name())
end,
after_place_node = setup_table_node,
allow_metadata_inventory_move = table_can_move,
allow_metadata_inventory_put = table_can_put,
on_metadata_inventory_put = table_on_items_changed,
on_metadata_inventory_take = table_on_items_changed,
});
function cartographer.register_map_material_name(name, material, value)
if _cartographer.materials_by_name[name] then
_cartographer.materials_by_name[name][material] = value or 1;
else
_cartographer.materials_by_name[name] = {
[material] = value or 1,
};
end
end
function cartographer.register_map_material_group(name, material, value)
if _cartographer.materials_by_group[name] then
_cartographer.materials_by_group[name][material] = value or 1;
else
_cartographer.materials_by_group[name] = {
[material] = value or 1,
};
end
end