2024-09-20 14:00:49 +02:00
-- Possible future improvements:
-- * rewrite to use node timers instead of ABMs, but needs benchmarking
-- * redesign the catch-up logic
-- * switch to exponentially-weighted moving average for light instead using a single variable to conserve IO
--
2021-05-29 16:12:33 +02:00
local math = math
2024-09-20 14:00:49 +02:00
local vector = vector
2021-05-29 16:12:33 +02:00
2021-03-12 00:10:50 +01:00
local plant_lists = { }
2024-09-20 14:00:49 +02:00
mcl_farming.plant_lists = plant_lists -- export
2024-10-10 16:23:54 +02:00
local plant_nodename_to_id_list = { } -- map nodes to plants
local plant_step_from_name = { } -- map nodes to growth steps
local growth_factor = tonumber ( minetest.settings : get ( " vl_plant_growth " ) ) or 1.0
2020-09-05 19:49:12 +02:00
2024-09-20 14:00:49 +02:00
local time_speed = tonumber ( minetest.settings : get ( " time_speed " ) ) or 72
local time_multiplier = time_speed > 0 and ( 86400 / time_speed ) or 0
2020-09-05 19:49:12 +02:00
2024-09-20 14:00:49 +02:00
local function get_intervals_counter ( pos , interval , chance )
if time_multiplier == 0 then return 0 end
-- "wall clock time", so plants continue to grow while sleeping
local current_game_time = ( minetest.get_day_count ( ) + minetest.get_timeofday ( ) ) * time_multiplier
2020-09-05 19:49:12 +02:00
local approx_interval = math.max ( interval , 1 ) * math.max ( chance , 1 )
2024-09-20 14:00:49 +02:00
local meta = minetest.get_meta ( pos )
local last_game_time = meta : get_float ( " last_gametime " )
if last_game_time < 1 then
last_game_time = current_game_time - approx_interval * 0.5
2020-09-05 19:49:12 +02:00
elseif last_game_time == current_game_time then
current_game_time = current_game_time + approx_interval
end
2024-09-20 14:00:49 +02:00
meta : set_float ( " last_gametime " , current_game_time )
return ( current_game_time - last_game_time ) / approx_interval
2020-09-05 19:49:12 +02:00
end
2024-10-09 22:15:30 +02:00
local function get_moisture_level ( pos )
local n = vector.offset ( pos , 0 , - 1 , 0 )
local totalm = 1
for z = - 1 , 1 do
n.z = pos.z + z
for x = - 1 , 1 do
n.x = pos.x + x
local ndef = minetest.registered_nodes [ minetest.get_node ( n ) . name ]
local soil = ndef and ndef.groups . soil or 0
local m = ( soil == 2 and 2 ) or ( soil >= 3 and 4 ) or 0
if x ~= 0 and z ~= 0 then m = m * 0.25 end
totalm = totalm + m
end
end
return totalm
end
-- moisture penalty function:
-- 0.5 if both on the x axis and the z axis the same plant growth
-- 0.5 if one diagonal neighbor is the same
-- 1.0 otherwise
local function get_moisture_penalty ( pos )
local name = minetest.get_node ( pos ) . name
local n , p = vector.copy ( pos ) , 1
-- check adjacent points, avoid vector allocations and reduce node accesses
n.x = pos.x - 1
local dx = minetest.get_node ( n ) . name == name
n.x = pos.x + 1
dx = dx or minetest.get_node ( n ) . name == name
if dx then
n.x = pos.x
n.z = pos.z - 1
local dz = minetest.get_node ( n ) . name == name
n.z = pos.z + 1
dz = dz or minetest.get_node ( n ) . name == name
if dz then return 0.5 end
end
-- check diagonals, clockwise
n.x , n.z = pos.x - 1 , pos.z - 1
if minetest.get_node ( n ) . name == name then return 0.5 end
n.x = pos.x + 1
if minetest.get_node ( n ) . name == name then return 0.5 end
n.z = pos.z + 1
if minetest.get_node ( n ) . name == name then return 0.5 end
n.x = pos.x - 1
if minetest.get_node ( n ) . name == name then return 0.5 end
return 1
end
2017-04-01 03:54:58 +02:00
function mcl_farming : add_plant ( identifier , full_grown , names , interval , chance )
2024-10-10 16:23:54 +02:00
interval = growth_factor > 0 and ( interval / growth_factor ) or 0
2024-09-20 14:00:49 +02:00
local plant_info = { }
plant_info.full_grown = full_grown
plant_info.names = names
plant_info.interval = interval
plant_info.chance = chance
for _ , nodename in pairs ( names ) do
plant_nodename_to_id_list [ nodename ] = identifier
end
for i , name in ipairs ( names ) do
2024-10-10 16:23:54 +02:00
plant_step_from_name [ name ] = i
2024-09-20 14:00:49 +02:00
end
plant_lists [ identifier ] = plant_info
2024-10-10 16:23:54 +02:00
if interval == 0 then return end -- growth disabled
2017-03-13 23:09:27 +01:00
minetest.register_abm ( {
2017-05-15 00:45:54 +02:00
label = string.format ( " Farming plant growth (%s) " , identifier ) ,
2017-03-13 23:09:27 +01:00
nodenames = names ,
interval = interval ,
chance = chance ,
action = function ( pos , node )
2024-10-09 22:15:30 +02:00
mcl_farming : grow_plant ( identifier , pos , node , 1 , false )
2017-04-01 03:54:58 +02:00
end ,
2017-03-13 23:09:27 +01:00
} )
end
2017-04-01 03:54:58 +02:00
-- Attempts to advance a plant at pos by one or more growth stages (if possible)
-- identifier: Identifier of plant as defined by mcl_farming:add_plant
-- pos: Position
-- node: Node table
-- stages: Number of stages to advance (optional, defaults to 1)
2024-10-10 16:23:54 +02:00
-- ignore_light_water: if true, ignore light and water requirements for growing
2017-05-25 03:24:11 +02:00
-- Returns true if plant has been grown by 1 or more stages.
-- Returns false if nothing changed.
2024-10-10 16:23:54 +02:00
function mcl_farming : grow_plant ( identifier , pos , node , stages , ignore_light_water )
stages = stages or 1 -- 0 when run from block loading
-- check light
if not ignore_light_water and ( minetest.get_node_light ( pos ) or 0 ) < 0 then return false end
-- number of missed interval ticks, for catch-up in block loading
2020-09-05 19:49:12 +02:00
local plant_info = plant_lists [ identifier ]
2024-10-10 16:23:54 +02:00
if plant_info then
stages = stages + math.floor ( get_intervals_counter ( pos , plant_info.interval , plant_info.chance ) )
2017-04-01 03:54:58 +02:00
end
2024-10-10 16:23:54 +02:00
if not ignore_light_water then
local odds = math.floor ( 25 / ( get_moisture_level ( pos ) * get_moisture_penalty ( pos ) ) ) + 1
for i = 1 , stages do
if math.random ( 1 , odds ) ~= 1 then stages = stages - 1 end
2020-09-05 19:49:12 +02:00
end
end
2024-09-20 14:00:49 +02:00
if stages == 0 then return false end
2024-10-10 16:23:54 +02:00
local step = plant_step_from_name [ node.name ]
if step == nil then return false end
minetest.set_node ( pos , {
name = plant_info.names [ step + stages ] or plant_info.full_grown ,
param = node.param , param2 = node.param2 ,
} )
2017-05-25 03:24:11 +02:00
return true
2017-04-01 03:54:58 +02:00
end
2017-03-13 23:09:27 +01:00
function mcl_farming : place_seed ( itemstack , placer , pointed_thing , plantname )
local pt = pointed_thing
2024-09-20 14:00:49 +02:00
if not pt or pt.type ~= " node " then return end
2017-03-13 23:09:27 +01:00
-- Use pointed node's on_rightclick function first, if present
local node = minetest.get_node ( pt.under )
if placer and not placer : get_player_control ( ) . sneak then
if minetest.registered_nodes [ node.name ] and minetest.registered_nodes [ node.name ] . on_rightclick then
return minetest.registered_nodes [ node.name ] . on_rightclick ( pt.under , node , placer , itemstack ) or itemstack
end
end
2017-03-14 03:56:33 +01:00
2024-09-20 14:00:49 +02:00
if minetest.get_node ( pt.above ) . name ~= " air " then return end
local farmland = minetest.registered_nodes [ minetest.get_node ( vector.offset ( pt.above , 0 , - 1 , 0 ) ) . name ]
if not farmland or ( farmland.groups . soil or 0 ) < 2 then return end
minetest.sound_play ( minetest.registered_nodes [ plantname ] . sounds.place , { pos = pt.above } , true )
minetest.add_node ( pt.above , { name = plantname , param2 = minetest.registered_nodes [ plantname ] . place_param2 } )
2017-03-13 23:09:27 +01:00
2024-09-20 14:00:49 +02:00
if not minetest.is_creative_enabled ( placer : get_player_name ( ) ) then itemstack : take_item ( ) end
2017-03-13 23:09:27 +01:00
return itemstack
end
2017-03-13 22:30:37 +01:00
--[[ Helper function to create a gourd (e.g. melon, pumpkin), the connected stem nodes as
2022-11-16 13:17:51 +01:00
- full_unconnected_stem : itemstring of the full - grown but unconnected stem node . This node must already be done
2017-03-13 22:30:37 +01:00
- connected_stem_basename : prefix of the itemstrings used for the 4 connected stem nodes to create
2017-03-14 04:17:35 +01:00
- stem_itemstring : Desired itemstring of the fully - grown unconnected stem node
- stem_def : Partial node definition of the fully - grown unconnected stem node . Many fields are already defined . You need to add ` tiles ` and ` description ` at minimum . Don ' t define on_construct without good reason
- stem_drop : Drop probability table for all stem
2017-03-13 22:30:37 +01:00
- gourd_itemstring : Desired itemstring of the full gourd node
2022-02-23 18:14:44 +01:00
- gourd_def : ( almost ) full definition of the gourd node . This function will add on_construct and after_destruct to the definition for unconnecting any connected stems
2017-03-13 22:43:47 +01:00
- grow_interval : Will attempt to grow a gourd periodically at this interval in seconds
- grow_chance : Chance of 1 / grow_chance to grow a gourd next to the full unconnected stem after grow_interval has passed . Must be a natural number
2017-07-17 14:26:25 +02:00
- connected_stem_texture : Texture of the connected stem
2017-03-13 22:30:37 +01:00
] ]
2024-09-20 14:00:49 +02:00
function mcl_farming : add_gourd ( full_unconnected_stem , connected_stem_basename , stem_itemstring , stem_def , stem_drop , gourd_itemstring , gourd_def , grow_interval , grow_chance , connected_stem_texture )
2024-10-10 16:23:54 +02:00
grow_interval = growth_factor > 0 and ( grow_interval / growth_factor ) or 0
2021-05-22 23:50:28 +02:00
local connected_stem_names = {
2017-03-13 22:30:37 +01:00
connected_stem_basename .. " _r " ,
connected_stem_basename .. " _l " ,
connected_stem_basename .. " _t " ,
2024-09-20 14:00:49 +02:00
connected_stem_basename .. " _b " }
2017-03-14 03:56:33 +01:00
2017-03-13 22:30:37 +01:00
-- Register gourd
2022-02-23 18:14:44 +01:00
if not gourd_def.after_destruct then
gourd_def.after_destruct = function ( blockpos , oldnode )
2017-03-13 22:30:37 +01:00
-- Disconnect any connected stems, turning them back to normal stems
2024-09-20 14:00:49 +02:00
-- four directions, but avoid using a table
-- opposite directions to above, as we go from groud to stem now!
local stempos = vector.offset ( blockpos , - 1 , 0 , 0 )
if minetest.get_node ( stempos ) . name == connected_stem_names [ 1 ] then
minetest.swap_node ( stempos , { name = full_unconnected_stem } )
end
local stempos = vector.offset ( blockpos , 1 , 0 , 0 )
if minetest.get_node ( stempos ) . name == connected_stem_names [ 2 ] then
minetest.swap_node ( stempos , { name = full_unconnected_stem } )
end
local stempos = vector.offset ( blockpos , 0 , 0 , - 1 )
if minetest.get_node ( stempos ) . name == connected_stem_names [ 3 ] then
minetest.swap_node ( stempos , { name = full_unconnected_stem } )
end
local stempos = vector.offset ( blockpos , 0 , 0 , 1 )
if minetest.get_node ( stempos ) . name == connected_stem_names [ 4 ] then
minetest.swap_node ( stempos , { name = full_unconnected_stem } )
2017-03-13 22:30:37 +01:00
end
end
end
minetest.register_node ( gourd_itemstring , gourd_def )
2017-03-14 04:17:35 +01:00
-- Register unconnected stem
-- Default values for the stem definition
if not stem_def.selection_box then
2024-09-20 14:00:49 +02:00
stem_def.selection_box = { type = " fixed " , fixed = { { - 0.15 , - 0.5 , - 0.15 , 0.15 , 0.5 , 0.15 } } }
end
stem_def.paramtype = stem_def.paramtype or " light "
stem_def.drawtype = stem_def.drawtype or " plantlike "
stem_def.walkable = stem_def.walkable or false
stem_def.sunlight_propagates = stem_def.sunlight_propagates == nil or stem_def.sunlight_propagates
stem_def.drop = stem_def.drop or stem_drop
stem_def.groups = stem_def.groups or { dig_immediate = 3 , not_in_creative_inventory = 1 , plant = 1 , attached_node = 1 , dig_by_water = 1 , destroy_by_lava_flow = 1 }
stem_def.sounds = stem_def.sounds or mcl_sounds.node_sound_leaves_defaults ( )
2017-03-14 04:17:35 +01:00
minetest.register_node ( stem_itemstring , stem_def )
2017-03-13 22:30:37 +01:00
-- Register connected stems
local connected_stem_tiles = {
2024-09-20 14:00:49 +02:00
{ " blank.png " , -- top
2022-11-16 13:17:51 +01:00
" blank.png " , -- bottom
" blank.png " , -- right
" blank.png " , -- left
connected_stem_texture , -- back
2024-09-20 14:00:49 +02:00
connected_stem_texture .. " ^[transformFX " -- front
2017-03-13 22:30:37 +01:00
} ,
2024-09-20 14:00:49 +02:00
{ " blank.png " , -- top
2022-11-16 13:17:51 +01:00
" blank.png " , -- bottom
" blank.png " , -- right
" blank.png " , -- left
2024-09-20 14:00:49 +02:00
connected_stem_texture .. " ^[transformFX " , -- back
2022-11-16 13:17:51 +01:00
connected_stem_texture , -- front
2017-03-13 22:30:37 +01:00
} ,
2024-09-20 14:00:49 +02:00
{ " blank.png " , -- top
2022-11-16 13:17:51 +01:00
" blank.png " , -- bottom
2023-05-27 17:27:01 +02:00
connected_stem_texture .. " ^[transformFX " , -- right
2022-11-16 13:17:51 +01:00
connected_stem_texture , -- left
2024-09-20 14:00:49 +02:00
" blank.png " , -- back
2022-11-16 13:17:51 +01:00
" blank.png " , -- front
2017-03-13 22:30:37 +01:00
} ,
2024-09-20 14:00:49 +02:00
{ " blank.png " , -- top
2022-11-16 13:17:51 +01:00
" blank.png " , -- bottom
connected_stem_texture , -- right
2023-05-27 17:27:01 +02:00
connected_stem_texture .. " ^[transformFX " , -- left
2024-09-20 14:00:49 +02:00
" blank.png " , -- back
2022-11-16 13:17:51 +01:00
" blank.png " , -- front
2017-03-13 22:30:37 +01:00
}
}
local connected_stem_nodebox = {
2022-11-16 13:17:51 +01:00
{ - 0.5 , - 0.5 , 0 , 0.5 , 0.5 , 0 } ,
{ - 0.5 , - 0.5 , 0 , 0.5 , 0.5 , 0 } ,
{ 0 , - 0.5 , - 0.5 , 0 , 0.5 , 0.5 } ,
{ 0 , - 0.5 , - 0.5 , 0 , 0.5 , 0.5 } ,
2017-03-13 22:30:37 +01:00
}
2017-03-14 02:43:48 +01:00
local connected_stem_selectionbox = {
2022-11-16 13:17:51 +01:00
{ - 0.1 , - 0.5 , - 0.1 , 0.5 , 0.2 , 0.1 } ,
{ - 0.5 , - 0.5 , - 0.1 , 0.1 , 0.2 , 0.1 } ,
{ - 0.1 , - 0.5 , - 0.1 , 0.1 , 0.2 , 0.5 } ,
{ - 0.1 , - 0.5 , - 0.5 , 0.1 , 0.2 , 0.1 } ,
2017-03-14 02:43:48 +01:00
}
2017-03-13 22:30:37 +01:00
2022-11-16 13:17:51 +01:00
for i = 1 , 4 do
2017-03-13 22:30:37 +01:00
minetest.register_node ( connected_stem_names [ i ] , {
_doc_items_create_entry = false ,
paramtype = " light " ,
sunlight_propagates = true ,
walkable = false ,
2017-03-14 04:17:35 +01:00
drop = stem_drop ,
2017-03-13 22:30:37 +01:00
drawtype = " nodebox " ,
2024-09-20 14:00:49 +02:00
node_box = { type = " fixed " , fixed = connected_stem_nodebox [ i ] } ,
selection_box = { type = " fixed " , fixed = connected_stem_selectionbox [ i ] } ,
2017-03-13 22:30:37 +01:00
tiles = connected_stem_tiles [ i ] ,
2021-02-18 14:00:17 +01:00
use_texture_alpha = minetest.features . use_texture_alpha_string_modes and " clip " or true ,
2024-09-20 14:00:49 +02:00
groups = { dig_immediate = 3 , not_in_creative_inventory = 1 , plant = 1 , attached_node = 1 , dig_by_water = 1 , destroy_by_lava_flow = 1 } ,
2017-03-13 22:30:37 +01:00
sounds = mcl_sounds.node_sound_leaves_defaults ( ) ,
_mcl_blast_resistance = 0 ,
} )
2017-03-21 04:56:16 +01:00
if minetest.get_modpath ( " doc " ) then
doc.add_entry_alias ( " nodes " , full_unconnected_stem , " nodes " , connected_stem_names [ i ] )
end
2017-03-13 22:30:37 +01:00
end
2024-10-10 16:23:54 +02:00
if grow_interval == 0 then return end
2017-03-13 22:30:37 +01:00
minetest.register_abm ( {
2022-11-16 13:17:51 +01:00
label = " Grow gourd stem to gourd ( " .. full_unconnected_stem .. " → " .. gourd_itemstring .. " ) " ,
nodenames = { full_unconnected_stem } ,
neighbors = { " air " } ,
2017-03-13 22:43:47 +01:00
interval = grow_interval ,
chance = grow_chance ,
2017-03-13 22:30:37 +01:00
action = function ( stempos )
local light = minetest.get_node_light ( stempos )
2024-10-09 22:15:30 +02:00
if not light or light < 9 then return end
2024-10-09 18:01:20 +02:00
-- Pick one neighbor and check if it can be used to grow
local dir = math.random ( 1 , 4 ) -- pick direction at random
local neighbor = ( dir == 1 and vector.offset ( stempos , 1 , 0 , 0 ) )
or ( dir == 2 and vector.offset ( stempos , - 1 , 0 , 0 ) )
or ( dir == 3 and vector.offset ( stempos , 0 , 0 , 1 ) )
or vector.offset ( stempos , 0 , 0 , - 1 )
if minetest.get_node ( neighbor ) . name ~= " air " then return end -- occupied
-- check for suitable floor: grass, dirt, or soil
local floorpos = vector.offset ( neighbor , 0 , - 1 , 0 )
local floorname = minetest.get_node ( floorpos ) . name
local floordef = minetest.registered_nodes [ floorname ]
if not floordef then return end
if ( floordef.groups . grass_block or 0 ) == 0 and ( floordef.groups . dirt or 0 ) == 0 and ( floordef.groups . soil or 0 ) < 2 then return end -- not suitable for growing
2024-09-20 14:00:49 +02:00
2024-10-09 22:15:30 +02:00
-- check moisture level
local moisture = get_moisture_level ( stempos ) * get_moisture_penalty ( stempos )
if math.random ( 1 , math.floor ( 25 / moisture ) + 1 ) ~= 1 then return end
2024-09-20 14:00:49 +02:00
minetest.swap_node ( stempos , { name = connected_stem_names [ dir ] } )
if gourd_def.paramtype2 == " facedir " then
local p2 = ( dir == 1 and 3 ) or ( dir == 2 and 1 ) or ( dir == 3 and 2 ) or 0
minetest.add_node ( neighbor , { name = gourd_itemstring , param2 = p2 } )
else
minetest.add_node ( neighbor , { name = gourd_itemstring } )
end
-- Reset farmland, etc. to dirt when the gourd grows on top
2024-10-09 18:01:20 +02:00
if ( floordef.groups . dirtifies_below_solid or 0 ) > 0 then
2024-09-20 14:00:49 +02:00
minetest.set_node ( floorpos , { name = " mcl_core:dirt " } )
end
2017-03-13 22:30:37 +01:00
end ,
} )
end
2017-07-21 19:47:20 +02:00
-- Used for growing gourd stems. Returns the intermediate color between startcolor and endcolor at a step
-- * startcolor: ColorSpec in table form for the stem in its lowest growing stage
-- * endcolor: ColorSpec in table form for the stem in its final growing stage
-- * step: The nth growth step. Counting starts at 1
-- * step_count: The number of total growth steps
function mcl_farming : stem_color ( startcolor , endcolor , step , step_count )
2024-09-20 14:00:49 +02:00
local mix = ( step - 1 ) / ( step_count - 1 )
return string.format ( " #%02X%02X%02X " ,
math.max ( 0 , math.min ( 255 , math.round ( ( 1 - mix ) * startcolor.r + mix * endcolor.r ) ) ) ,
math.max ( 0 , math.min ( 255 , math.round ( ( 1 - mix ) * startcolor.g + mix * endcolor.g ) ) ) ,
math.max ( 0 , math.min ( 255 , math.round ( ( 1 - mix ) * startcolor.b + mix * endcolor.b ) ) ) )
2017-07-21 19:47:20 +02:00
end
2020-09-05 19:49:12 +02:00
2022-12-23 15:05:23 +01:00
--[[Get a callback that either eats the item or plants it.
Used for on_place callbacks for craft items which are seeds that can also be consumed .
2022-12-24 19:38:32 +01:00
] ]
2022-12-23 15:05:23 +01:00
function mcl_farming : get_seed_or_eat_callback ( plantname , hp_change )
return function ( itemstack , placer , pointed_thing )
2024-09-20 14:00:49 +02:00
return mcl_farming : place_seed ( itemstack , placer , pointed_thing , plantname )
or minetest.do_item_eat ( hp_change , nil , itemstack , placer , pointed_thing )
2022-12-23 15:05:23 +01:00
end
end
2020-09-05 19:49:12 +02:00
minetest.register_lbm ( {
label = " Add growth for unloaded farming plants " ,
name = " mcl_farming:growth " ,
2022-11-16 13:17:51 +01:00
nodenames = { " group:plant " } ,
2020-09-05 19:49:12 +02:00
run_at_every_load = true ,
2024-09-20 14:00:49 +02:00
action = function ( pos , node , dtime_s )
2020-09-05 19:49:12 +02:00
local identifier = plant_nodename_to_id_list [ node.name ]
2024-09-20 14:00:49 +02:00
if not identifier then return end
2024-10-09 22:15:30 +02:00
mcl_farming : grow_plant ( identifier , pos , node , 0 , false )
2020-09-05 19:49:12 +02:00
end ,
} )
2024-09-20 14:00:49 +02:00
2024-10-10 16:23:54 +02:00
-- The average light levels were unreliable
minetest.register_lbm ( {
label = " Drop legacy average lighting data " ,
name = " mcl_farming:drop_average_light_meta " ,
nodenames = { " group:plant " } ,
run_at_every_load = false , -- only convert once
action = function ( pos , node , dtime_s )
local meta = minetest.get_meta ( pos )
meta : set_string ( " avg_light_summary " , " " ) -- drop
meta : set_string ( " avg_light_count " , " " ) -- drop
end ,
} )