--[[ X Farming. Extends Minetest farming mod with new plants, crops and ice fishing. Copyright (C) 2024 SaKeL This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to juraj.vajda@gmail.com --]] local S = minetest.get_translator(minetest.get_current_modname()) local stove_fire_sounds = {} local function get_grid_matrix_items(grid) local grid_matrix = table.copy(grid) local _items = {} for row, items in ipairs(grid_matrix) do for item_pos, item in pairs(items) do if item.itemstring ~= '' then -- add entity position to item table item.ent_pos = item_pos table.insert(_items, item) end end end return _items end local function add_item_to_grid_matrix(grid_matrix, item) local result = {} for row, items in ipairs(grid_matrix) do local found = false for item_pos, _item in pairs(items) do if _item.itemstring == '' then grid_matrix[row][item_pos] = item result.added_to_row = row result.added_to_pos = item_pos found = true break end end if found then break end end return result end local function stop_stove_sound(pos, fadeout_step) local hash = minetest.hash_node_position(pos) local sound_ids = stove_fire_sounds[hash] if sound_ids then for _, sound_id in ipairs(sound_ids) do minetest.sound_fade(sound_id, -1, 0) end stove_fire_sounds[hash] = nil end end local function remove_item_from_grid_matrix(grid_matrix, item) local result = {} for row, items in ipairs(grid_matrix) do local found = false for item_pos, _item in pairs(items) do if _item.itemstring == item.itemstring then grid_matrix[row][item_pos] = { itemstring = '', output = {} } result.removed_from_row = row result.removed_from_pos = item_pos found = true break end end if found then break end end return result end local function add_item_smoke_particles(pos) local particlespawner_def = { amount = 5, time = 1, minpos = vector.new(pos.x - 0.1, pos.y, pos.z - 0.1), maxpos = vector.new(pos.x + 0.1, pos.y, pos.z + 0.1), minvel = vector.new(-0.1, 0.2, -0.1), maxvel = vector.new(0.1, 0.4, 0.1), minacc = vector.new(0, 0.1, 0), maxacc = vector.new(0, 0.2, 0), minexptime = 1, maxexptime = 3, minsize = 1, maxsize = 1.5, texture = 'x_farming_stove_item_smoke_particle.png', collisiondetection = true } if minetest.has_feature({ dynamic_add_media_table = true, particlespawner_tweenable = true }) then -- new syntax, after v5.6.0 particlespawner_def = { amount = 5, time = 1, pos = { min = vector.new(pos.x - 0.1, pos.y, pos.z - 0.1), max = vector.new(pos.x + 0.1, pos.y, pos.z + 0.1) }, size = { min = 1, max = 1.5, }, vel = { min = vector.new(-0.1, 0.2, -0.1), max = vector.new(0.1, 0.4, 0.1) }, acc = { min = vector.new(0, 0.1, 0), max = vector.new(0, 0.2, 0) }, exptime = { min = 1, max = 3 }, texture = { name = 'x_farming_stove_item_smoke_particle.png', alpha_tween = { 1, 0.5, style = 'fwd', reps = 1 }, scale_tween = { { x = 1, y = 1 }, { x = 0.5, y = 0.5 }, } }, collisiondetection = true } end minetest.add_particlespawner(particlespawner_def) end -- Entity minetest.register_entity('x_farming:stove_food', { initial_properties = { visual = 'wielditem', visual_size = { x = 0.2, y = 0.2, z = 0.2 }, physical = false, collide_with_objects = false, collisionbox = { 0, 0, 0, 0, 0, 0 }, -- collisionbox = { -0.15, -0.05, -0.15, 0.15, 0.05, 0.15 }, selectionbox = { 0, 0, 0, 0, 0, 0 }, -- selectionbox = { -0.15, -0.05, -0.15, 0.15, 0.05, 0.15 }, pointable = false, makes_footstep_sound = false, static_save = true, shaded = true, glow = 4 }, on_activate = function(self, staticdata, dtime_s) if not self or not staticdata or staticdata == '' then self.object:remove() return end local _staticdata = minetest.deserialize(staticdata) for key, value in pairs(_staticdata) do self[key] = value end self._nodechecktimer = 2 self.object:set_armor_groups({ immortal = 1, stove_food = 1, fleshy = 100 }) self.object:set_properties({ wield_item = _staticdata.itemname, infotext = _staticdata.itemname, }) end, on_step = function(self, dtime, moveresult) self._nodechecktimer = self._nodechecktimer - dtime if self._nodechecktimer <= 0 then self._nodechecktimer = 2 local pos = self.object:get_pos() local node_above = minetest.get_node(vector.new(pos.x, pos.y + 0.5, pos.z)) local node_under = minetest.get_node(vector.new(pos.x, pos.y - 0.5, pos.z)) -- drop items if above is obstructed or no heat_source below if node_above.name ~= 'air' or minetest.get_item_group(node_under.name, 'heat_source') < 1 then local meta = minetest.get_meta(vector.new(pos.x, pos.y - 0.5, pos.z)) local grid_matrix = minetest.deserialize(meta:get_string('grid_matrix')) if not grid_matrix then return end local grid_items = get_grid_matrix_items(grid_matrix) -- remove item from stove meta for i, value in ipairs(grid_items) do remove_item_from_grid_matrix(grid_matrix, value) end meta:set_string('grid_matrix', minetest.serialize(grid_matrix)) -- remove entity and drop item minetest.add_item(pos, ItemStack({ name = self.itemname })) self.object:remove() return end end end, get_staticdata = function(self) local staticdata = { itemname = self.itemname } return minetest.serialize(staticdata) end, }) -- Nodes minetest.register_node('x_farming:stove', { description = S('Stove inactive)'), tiles = { 'x_farming_stove_top.png', 'x_farming_stove_side.png', 'x_farming_stove_side.png', 'x_farming_stove_side.png', 'x_farming_stove_side.png', 'x_farming_stove_front.png', }, paramtype2 = '4dir', is_ground_content = false, groups = { -- MTG cracky = 2, -- MCL pickaxey = 1, container = 4, deco_block = 1, material_stone = 1 }, _mcl_blast_resistance = 3.5, _mcl_hardness = 3.5, sounds = x_farming.node_sound_stone_defaults(), on_construct = function(pos) local meta = minetest.get_meta(pos) local infotext = S('Stove inactive.') .. ' ' .. S('Activate by torch or flint and steel.') local node = minetest.get_node(pos) local x_shift = -0.6 local m_pos = vector.new(pos.x - 0.6, pos.y + 0.5, pos.z) -- param2 = 0 or 2 local initial_grid_matrix = { [1] = { [vector.to_string(vector.new(m_pos.x + (1 * 0.3), m_pos.y, m_pos.z + 0.2))] = { itemstring = '', output = {}, cooked_time = 0 }, [vector.to_string(vector.new(m_pos.x + (2 * 0.3), m_pos.y, m_pos.z + 0.2))] = { itemstring = '', output = {}, cooked_time = 0 }, [vector.to_string(vector.new(m_pos.x + (3 * 0.3), m_pos.y, m_pos.z + 0.2))] = { itemstring = '', output = {}, cooked_time = 0 } }, [2] = { [vector.to_string(vector.new(m_pos.x + (1 * 0.3), m_pos.y, m_pos.z - 0.2))] = { itemstring = '', output = {}, cooked_time = 0 }, [vector.to_string(vector.new(m_pos.x + (2 * 0.3), m_pos.y, m_pos.z - 0.2))] = { itemstring = '', output = {}, cooked_time = 0 }, [vector.to_string(vector.new(m_pos.x + (3 * 0.3), m_pos.y, m_pos.z - 0.2))] = { itemstring = '', output = {}, cooked_time = 0 } }, } if node.param2 == 1 or node.param2 == 3 then m_pos = vector.new(pos.x, pos.y + 0.5, pos.z + x_shift) initial_grid_matrix = { [1] = { [vector.to_string(vector.new(m_pos.x + 0.2, m_pos.y, m_pos.z + (1 * 0.3)))] = { itemstring = '', output = {}, cooked_time = 0 }, [vector.to_string(vector.new(m_pos.x + 0.2, m_pos.y, m_pos.z + (2 * 0.3)))] = { itemstring = '', output = {}, cooked_time = 0 }, [vector.to_string(vector.new(m_pos.x + 0.2, m_pos.y, m_pos.z + (3 * 0.3)))] = { itemstring = '', output = {}, cooked_time = 0 } }, [2] = { [vector.to_string(vector.new(m_pos.x - 0.2, m_pos.y, m_pos.z + (1 * 0.3)))] = { itemstring = '', output = {}, cooked_time = 0 }, [vector.to_string(vector.new(m_pos.x - 0.2, m_pos.y, m_pos.z + (2 * 0.3)))] = { itemstring = '', output = {}, cooked_time = 0 }, [vector.to_string(vector.new(m_pos.x - 0.2, m_pos.y, m_pos.z + (3 * 0.3)))] = { itemstring = '', output = {}, cooked_time = 0 } }, } end meta:set_string('grid_matrix', minetest.serialize(initial_grid_matrix)) meta:set_string('infotext', infotext) end, on_rightclick = function(pos, node, player, itemstack, pointed_thing) local meta = minetest.get_meta(pos) local stack = player:get_wielded_item() local stack_name = stack:get_name() if minetest.get_item_group(stack_name, 'torch') > 0 or stack_name == 'fire:flint_and_steel' or stack_name == 'mcl_fire:flint_and_steel' then local infotext = S('Stove active.') .. ' ' .. S('De-activate with shovel. Stove will de-activate by its self after a while if there are no items to cook.') meta:set_string('infotext', infotext) minetest.swap_node(pos, { name = 'x_farming:stove_active', param2 = node.param2 }) minetest.get_node_timer(pos):start(1) end return itemstack end, on_rotate = function() return false end }) -- Active stove minetest.register_node('x_farming:stove_active', { description = S('Stove active'), tiles = { { name = 'x_farming_stove_top_animated.png', animation = { type = 'vertical_frames', aspect_w = 16, aspect_h = 16, length = 5 }, }, 'x_farming_stove_side.png', 'x_farming_stove_side.png', 'x_farming_stove_side.png', 'x_farming_stove_side.png', { name = 'x_farming_stove_front_animated.png', animation = { type = 'vertical_frames', aspect_w = 16, aspect_h = 16, length = 2 }, }, }, paramtype2 = '4dir', is_ground_content = false, groups = { -- MTG cracky = 2, heat_source = 1, not_in_creative_inventory = 1, -- MCL pickaxey = 1, container = 4, deco_block = 1, material_stone = 1 }, _mcl_blast_resistance = 3.5, _mcl_hardness = 3.5, sounds = x_farming.node_sound_stone_defaults(), light_source = 8, drop = 'x_farming:stove', on_construct = function(pos) local meta = minetest.get_meta(pos) local infotext = S('Stove active.') .. ' ' .. S('De-activate with shovel. Stove will de-activate by its self after a while if there are no items to cook.') meta:set_string('infotext', infotext) end, on_timer = function(pos, elapsed) local meta = minetest.get_meta(pos) local grid_matrix = minetest.deserialize(meta:get_string('grid_matrix')) if not grid_matrix then return end local timer_elapsed = meta:get_int('timer_elapsed') or 0 meta:set_int('timer_elapsed', timer_elapsed + 1) -- total running time without items to cook local total_running_time = meta:get_float('total_running_time') or 0 local grid_items = get_grid_matrix_items(grid_matrix) if #grid_items == 0 then total_running_time = total_running_time + elapsed elseif total_running_time > 0 then -- reset time when added another item to cook total_running_time = 0 end if total_running_time > 180 then -- extinguish stove total_running_time = 0 meta:set_float('total_running_time', total_running_time) local node = minetest.get_node(pos) minetest.swap_node(pos, { name = 'x_farming:stove', param2 = node.param2 }) meta:set_int('timer_elapsed', 0) local infotext = S('Stove inactive.') .. ' ' .. S('Activate by torch or flint and steel.') meta:set_string('infotext', infotext) stop_stove_sound(pos) return false end -- play sound if timer_elapsed == 0 or (timer_elapsed + 1) % 5 == 0 then local sound_id = minetest.sound_play('x_farming_stove_active', { pos = pos, max_hear_distance = 16, gain = 0.25 }) local hash = minetest.hash_node_position(pos) stove_fire_sounds[hash] = stove_fire_sounds[hash] or {} table.insert(stove_fire_sounds[hash], sound_id) -- Only remember the 3 last sound handles if #stove_fire_sounds[hash] > 3 then table.remove(stove_fire_sounds[hash], 1) end -- Remove the sound ID automatically from table after 11 seconds minetest.after(11, function() if not stove_fire_sounds[hash] then return end for f = #stove_fire_sounds[hash], 1, -1 do if stove_fire_sounds[hash][f] == sound_id then table.remove(stove_fire_sounds[hash], f) end end if #stove_fire_sounds[hash] == 0 then stove_fire_sounds[hash] = nil end end) end -- cook items, update cooked times -- if cooked drop items and remove from meta for row, items in ipairs(grid_matrix) do for item_pos, item in pairs(items) do if item.itemstring ~= '' then grid_matrix[row][item_pos].cooked_time = (grid_matrix[row][item_pos].cooked_time or 0) + elapsed -- drop cooked item and remove from meta if grid_matrix[row][item_pos].cooked_time > item.output.time then -- drop cooked item minetest.add_item(vector.from_string(item_pos), ItemStack(item.output.item)) -- drop recipe replacements for _, replacement in ipairs(item.output.replacements) do minetest.add_item(vector.from_string(item_pos), ItemStack(replacement)) end -- remove from metadata local removed_items_result = remove_item_from_grid_matrix(grid_matrix, item) if removed_items_result.removed_from_pos then -- remove entity for _, o in ipairs(minetest.get_objects_inside_radius(pos, 0.7)) do local armor_groups = o:get_armor_groups() or {} if armor_groups.stove_food and armor_groups.stove_food > 0 then if o:get_pos() and vector.to_string(vector.new(o:get_pos())) == removed_items_result.removed_from_pos then o:remove() break end end end end -- Play cooling sound minetest.sound_play('x_farming_stove_sizzle', { pos = vector.from_string(item_pos), max_hear_distance = 16, gain = 0.07 }, true) else add_item_smoke_particles(vector.from_string(item_pos)) end -- restore entity items after `clearobjects` local missing_items = table.copy(items) for _, obj in ipairs(minetest.get_objects_inside_radius(pos, 0.7)) do local armor_groups = obj:get_armor_groups() or {} if armor_groups.stove_food and armor_groups.stove_food > 0 then local lua_ent = obj:get_luaentity() if lua_ent and obj:get_pos() then -- match entity with metadata -- removing the one we found and leaving -- only the ones what needs to be restored for ent_pos, value in pairs(missing_items) do local obj_pos = vector.to_string(vector.new(obj:get_pos())) if obj_pos == ent_pos then missing_items[obj_pos] = nil break end end end end end for ent_pos, value in pairs(missing_items) do if value.itemstring ~= '' then local staticdata = { itemname = value.itemstring } local obj = minetest.add_entity( vector.from_string(ent_pos), 'x_farming:stove_food', minetest.serialize(staticdata) ) if obj then obj:set_rotation(vector.from_string(value.ent_rot)) end end end end end end -- set meta meta:set_string('grid_matrix', minetest.serialize(grid_matrix)) meta:set_float('total_running_time', total_running_time) return true end, on_rightclick = function(pos, node, player, itemstack, pointed_thing) local meta = minetest.get_meta(pos) local wield_stack = player:get_wielded_item() local wield_stack_name = wield_stack:get_name() -- de-activate stove if minetest.get_item_group(wield_stack_name, 'shovel') > 0 then minetest.swap_node(pos, { name = 'x_farming:stove', param2 = node.param2 }) meta:set_int('timer_elapsed', 0) local infotext = S('Stove inactive.') .. ' ' .. S('Activate by torch or flint and steel.') meta:set_string('infotext', infotext) stop_stove_sound(pos) end -- check if item is cook-able local output = minetest.get_craft_result({ method = 'cooking', width = 1, items = { itemstack } }) if output.item:is_empty() then -- item is not cook-able return itemstack end -- check if space above if minetest.get_node(vector.new(pos.x, pos.y + 1, pos.z)).name ~= 'air' then return itemstack end local grid_matrix = minetest.deserialize(meta:get_string('grid_matrix')) local grid_items = get_grid_matrix_items(grid_matrix) if #grid_items >= 6 then -- stove is full return itemstack end local _output = { time = output.time, replacements = {}, item = output.item:to_table() } for _, value in ipairs(output.replacements) do table.insert(_output.replacements, value:to_table()) end -- degrees to radians local pitch = -90 * (math.pi / 180) local roll = math.pi * 2 + node.param2 * math.pi / 2 -- x = pitch (elevation), y = yaw (heading), z = roll (bank) local ent_rot = vector.new(pitch, 0, roll) local added_item_result = add_item_to_grid_matrix(grid_matrix, { itemstring = wield_stack_name, output = _output, ent_rot = vector.to_string(ent_rot), cooked_time = 0 }) if not (added_item_result.added_to_row and added_item_result.added_to_pos) then return itemstack end local ent_pos = vector.from_string(added_item_result.added_to_pos) if not ent_pos then return itemstack end -- Add Entity local staticdata = { itemname = wield_stack_name } local obj = minetest.add_entity( ent_pos, 'x_farming:stove_food', minetest.serialize(staticdata) ) if obj then -- 90 degress to radians obj:set_rotation(ent_rot) end -- Play cooling sound minetest.sound_play('x_farming_stove_sizzle_put', { pos = ent_pos, max_hear_distance = 16, gain = 0.1 }, true) meta:set_string('grid_matrix', minetest.serialize(grid_matrix)) itemstack:take_item() return itemstack end, on_destruct = function(pos) stop_stove_sound(pos) end, after_dig_node = function(pos, oldnode, oldmetadata, digger) if not oldmetadata.fields.grid_matrix then return end local objs = minetest.get_objects_inside_radius(pos, 0.7) local grid_matrix = minetest.deserialize(oldmetadata.fields.grid_matrix) local grid_items = get_grid_matrix_items(grid_matrix) -- remove entitites for _, obj in ipairs(objs) do local armor_groups = obj:get_armor_groups() or {} if armor_groups.stove_food and armor_groups.stove_food > 0 then obj:remove() end end -- drop items for _, item in ipairs(grid_items) do minetest.add_item(vector.new(pos.x, pos.y + 0.7, pos.z), ItemStack(item.itemstring)) end end, on_rotate = function() return false end })