Skip to content

Commit 70ef786

Browse files
sofarparamat
authored andcommittedMay 25, 2016
Farming: Convert plants to use node timers
This PR requires @minetest/minetest#3677 Farming and plant growth has traditionally in minetest been implemented using ABM's. These ABM's periodically tick and cause plants to grow. The way these ABM's work has several side effects that can be considered harmful. Not to mention a comprehensive list of downsides here, but ABM's are chance-dependent. That results in the chance that some nodes potentially never get processed by the ABM action, and others get processed always. One can easily find this effect by planting a large field of crops, and seeing that some nodes are fully grown really fast, and some just won't make it to fully grown status even after hours or play time. One could solve the problem by making the ABM's slower, and giving them a 100% of action, but this would cause the entire field to grow a step instantly at ABM intervals, and is both ugly, and a large number of node updates that needs to be sent out to each client. Very un-ideal. With NodeTimers though, each node will see a separate node timer event, and they will likely not coalesce. This means that we can stop relying on chance to distribute plant growth, and assign a single timer event to grow the plant to the next phase. Due to the timer implementation, we won't ever miss a growth event, and we can re-scehdule them until the plant has reached full size. Previously, plants would attempt to grow every 9 seconds, with a chance of 1/20. This means typically, a plant would need 9*20 seconds to grow 1 phase, and since there are 8 steps, a typical plant growth would require 9*20*8 ABM node events. (spread out over 9*8 ABM actual underlying events per block, roughly). because plants are likely not growing to full for a very long time due to statistics working against it (5% of the crops take 20x longer than the median to grow to full, we'd be seeing ABMs fire possibly up to 9*20*8*20 with a 95% confidence interval (the actual math is likely off, but the scale should be correct). That's incredibly wasteful. We'd reach those conditions easily with 20 plant nodes. Now, after we convert to NodeTimers, each plant node will see exactly 8 NodeTimer events, and no more. This scales lineairly per plant. I've tuned the growth rate of crops to be mature in just under 3 whole days. That's about 1hr of game time. Previously, about half the crops would grow to full in under 2 days, but many plants would still not be mature by the end of day 3. This is more consistent. An additional problem in the farming mod was that the final fully-grown plant was also included in the ABM, causing infinite more ABM's even after the entire field had grown to completion. Now, we're left with the problem that none of the pre-existing plants have actual node timers started on them, and we do not want a new ABM to fix this issue, since that would be wasteful. Fortunately, there is now an LBM concept, and we can use it to assure that NodeTimers on crop nodes are properly started, and only have to do the actual conversion once per block, ever. We want to provide a fairly similar growth rate after this conversion and as such I've resorted to modelling some statistical data. For this I created a virtual 32x32 crop field with 9 steps (8 transitions) as is the default wheat crop. We then apply a step where 1 in 20 plants in the field grows a step (randomly chosen) and count the number of steps needed to get to 25%, 50, 75% and 95% grown. The resulting data looks as follows: 25% - ~120 steps * 9 sec / abm = 1080s 50% - ~152 steps = 1368s 75% - ~194 steps = 1746s 95% - ~255 steps = 2295s Next, we want to create a model where the chance that a crop grows is 100% every node timer. Since there will only be 8 steps ever, we want the slowest crops to grow in intervals of ~ 2300 / 8 seconds and the fastest 1/4 of crops to grow 1080 / 8 seconds intervals. We can roughly compare this to a normal distribution with a median of 1400 with a stddev of ~350 (thick fingering this one here). The rest is a bit of thick-fingering to get similar growth rates, taking into account that ABM's fire regularly so if they're missed it's fairly painless, but our timers are going to be 1-2 minutes apart at minimum. I calculate the timer should be around 150s median, and experimented with several jitter ranges. Eventually I settled for now on [80,200] with a redo of [40,80], meaning that each growth step at minimum takes (80 to 200) seconds, and if a negative growth condition was found (darkness, soil not wet, etc), then the growth step is retried every (40 to 80) seconds. The end result is a growth period from seed to full in ~ 2.25 minetest days. This is a little bit shorter than the current growth rate but the chances you'll miss timer ticks is a bit larger, so in normal gameplay it should be fairly comparable. A side effect is that fields grow to full yield fairly quickly after crops make it to mature growth, and no crops are mature a very long time before the majority grows to full. The spread and view over a growing field is also fairly even, there's no large updates with plenty of nodes. Just a node here or there every second or so in large fields. Ultimately, we get rid of ABM rollercoasters that cause tens of node updates every 9 seconds. This will help multiplayer servers likely a lot.
1 parent fc902a7 commit 70ef786

File tree

1 file changed

+105
-67
lines changed

1 file changed

+105
-67
lines changed
 

‎mods/farming/api.lua

+105-67
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
-- Wear out hoes, place soil
23
-- TODO Ignore group:flower
34
farming.hoe_on_use = function(itemstack, user, pointed_thing, uses)
@@ -9,35 +10,35 @@ farming.hoe_on_use = function(itemstack, user, pointed_thing, uses)
910
if pt.type ~= "node" then
1011
return
1112
end
12-
13+
1314
local under = minetest.get_node(pt.under)
1415
local p = {x=pt.under.x, y=pt.under.y+1, z=pt.under.z}
1516
local above = minetest.get_node(p)
16-
17+
1718
-- return if any of the nodes is not registered
1819
if not minetest.registered_nodes[under.name] then
1920
return
2021
end
2122
if not minetest.registered_nodes[above.name] then
2223
return
2324
end
24-
25+
2526
-- check if the node above the pointed thing is air
2627
if above.name ~= "air" then
2728
return
2829
end
29-
30+
3031
-- check if pointing at soil
3132
if minetest.get_item_group(under.name, "soil") ~= 1 then
3233
return
3334
end
34-
35+
3536
-- check if (wet) soil defined
3637
local regN = minetest.registered_nodes
3738
if regN[under.name].soil == nil or regN[under.name].soil.wet == nil or regN[under.name].soil.dry == nil then
3839
return
3940
end
40-
41+
4142
if minetest.is_protected(pt.under, user:get_player_name()) then
4243
minetest.record_protection_violation(pt.under, user:get_player_name())
4344
return
@@ -47,14 +48,13 @@ farming.hoe_on_use = function(itemstack, user, pointed_thing, uses)
4748
return
4849
end
4950

50-
5151
-- turn the node into soil, wear out item and play sound
5252
minetest.set_node(pt.under, {name = regN[under.name].soil.dry})
5353
minetest.sound_play("default_dig_crumbly", {
5454
pos = pt.under,
5555
gain = 0.5,
5656
})
57-
57+
5858
if not minetest.setting_getbool("creative_mode") then
5959
itemstack:add_wear(65535/(uses-1))
6060
end
@@ -119,6 +119,15 @@ farming.register_hoe = function(name, def)
119119
end
120120
end
121121

122+
-- how often node timers for plants will tick, +/- some random value
123+
local function tick(pos)
124+
minetest.get_node_timer(pos):start(math.random(166, 286))
125+
end
126+
-- how often a growth failure tick is retried (e.g. too dark)
127+
local function tick_again(pos)
128+
minetest.get_node_timer(pos):start(math.random(40, 80))
129+
end
130+
122131
-- Seed placement
123132
farming.place_seed = function(itemstack, placer, pointed_thing, plantname)
124133
local pt = pointed_thing
@@ -129,10 +138,10 @@ farming.place_seed = function(itemstack, placer, pointed_thing, plantname)
129138
if pt.type ~= "node" then
130139
return
131140
end
132-
141+
133142
local under = minetest.get_node(pt.under)
134143
local above = minetest.get_node(pt.above)
135-
144+
136145
if minetest.is_protected(pt.under, placer:get_player_name()) then
137146
minetest.record_protection_violation(pt.under, placer:get_player_name())
138147
return
@@ -142,38 +151,93 @@ farming.place_seed = function(itemstack, placer, pointed_thing, plantname)
142151
return
143152
end
144153

145-
146154
-- return if any of the nodes is not registered
147155
if not minetest.registered_nodes[under.name] then
148156
return
149157
end
150158
if not minetest.registered_nodes[above.name] then
151159
return
152160
end
153-
161+
154162
-- check if pointing at the top of the node
155163
if pt.above.y ~= pt.under.y+1 then
156164
return
157165
end
158-
166+
159167
-- check if you can replace the node above the pointed node
160168
if not minetest.registered_nodes[above.name].buildable_to then
161169
return
162170
end
163-
171+
164172
-- check if pointing at soil
165173
if minetest.get_item_group(under.name, "soil") < 2 then
166174
return
167175
end
168-
176+
169177
-- add the node and remove 1 item from the itemstack
170178
minetest.add_node(pt.above, {name = plantname, param2 = 1})
179+
tick(pt.above)
171180
if not minetest.setting_getbool("creative_mode") then
172181
itemstack:take_item()
173182
end
174183
return itemstack
175184
end
176185

186+
farming.grow_plant = function(pos, elapsed)
187+
local node = minetest.get_node(pos)
188+
local name = node.name
189+
local def = minetest.registered_nodes[name]
190+
191+
if not def.next_plant then
192+
-- disable timer for fully grown plant
193+
return
194+
end
195+
196+
-- grow seed
197+
if minetest.get_item_group(node.name, "seed") and def.fertility then
198+
local soil_node = minetest.get_node_or_nil({x = pos.x, y = pos.y - 1, z = pos.z})
199+
if not soil_node then
200+
tick_again(pos)
201+
return
202+
end
203+
-- omitted is a check for light, we assume seeds can germinate in the dark.
204+
for _, v in pairs(def.fertility) do
205+
if minetest.get_item_group(soil_node.name, v) ~= 0 then
206+
minetest.swap_node(pos, {name = def.next_plant})
207+
if minetest.registered_nodes[def.next_plant].next_plant then
208+
tick(pos)
209+
return
210+
end
211+
end
212+
end
213+
214+
return
215+
end
216+
217+
-- check if on wet soil
218+
local below = minetest.get_node({x = pos.x, y = pos.y - 1, z = pos.z})
219+
if minetest.get_item_group(below.name, "soil") < 3 then
220+
tick_again(pos)
221+
return
222+
end
223+
224+
-- check light
225+
local light = minetest.get_node_light(pos)
226+
if not light or light < def.minlight or light > def.maxlight then
227+
tick_again(pos)
228+
return
229+
end
230+
231+
-- grow
232+
minetest.swap_node(pos, {name = def.next_plant})
233+
234+
-- new timer needed?
235+
if minetest.registered_nodes[def.next_plant].next_plant then
236+
tick(pos)
237+
end
238+
return
239+
end
240+
177241
-- Register plants
178242
farming.register_plant = function(name, def)
179243
local mname = name:split(":")[1]
@@ -200,6 +264,7 @@ farming.register_plant = function(name, def)
200264
end
201265

202266
-- Register seed
267+
local lbm_nodes = {mname .. ":seed_" .. pname}
203268
local g = {seed = 1, snappy = 3, attached_node = 1}
204269
for k, v in pairs(def.fertility) do
205270
g[v] = 1
@@ -228,6 +293,10 @@ farming.register_plant = function(name, def)
228293
on_place = function(itemstack, placer, pointed_thing)
229294
return farming.place_seed(itemstack, placer, pointed_thing, mname .. ":seed_" .. pname)
230295
end,
296+
next_plant = mname .. ":" .. pname .. "_1",
297+
on_timer = farming.grow_plant,
298+
minlight = def.minlight,
299+
maxlight = def.maxlight,
231300
})
232301

233302
-- Register harvest
@@ -237,7 +306,7 @@ farming.register_plant = function(name, def)
237306
})
238307

239308
-- Register growing steps
240-
for i=1,def.steps do
309+
for i = 1, def.steps do
241310
local drop = {
242311
items = {
243312
{items = {mname .. ":" .. pname}, rarity = 9 - i},
@@ -248,6 +317,16 @@ farming.register_plant = function(name, def)
248317
}
249318
local nodegroups = {snappy = 3, flammable = 2, plant = 1, not_in_creative_inventory = 1, attached_node = 1}
250319
nodegroups[pname] = i
320+
321+
local next_plant = nil
322+
local on_timer = nil
Has comments. Original line has comments.
323+
324+
if i < def.steps then
325+
next_plant = mname .. ":" .. pname .. "_" .. (i + 1)
326+
on_timer = farming.grow_plant
327+
lbm_nodes[#lbm_nodes + 1] = mname .. ":" .. pname .. "_" .. i
328+
end
329+
251330
minetest.register_node(mname .. ":" .. pname .. "_" .. i, {
252331
drawtype = "plantlike",
253332
waving = 1,
@@ -262,61 +341,20 @@ farming.register_plant = function(name, def)
262341
},
263342
groups = nodegroups,
264343
sounds = default.node_sound_leaves_defaults(),
344+
next_plant = next_plant,
345+
on_timer = farming.grow_plant,
346+
minlight = def.minlight,
347+
maxlight = def.maxlight,
265348
})
266349
end
267350

268-
-- Growing ABM
269-
minetest.register_abm({
270-
nodenames = {"group:" .. pname, "group:seed"},
271-
neighbors = {"group:soil"},
272-
interval = 9,
273-
chance = 20,
351+
-- replacement LBM for pre-nodetimer plants
352+
minetest.register_lbm({
353+
name = "farming:start_nodetimer_" .. mname .. "_" .. pname,
354+
nodenames = lbm_nodes,
274355
action = function(pos, node)
275-
local plant_height = minetest.get_item_group(node.name, pname)
276-
277-
-- return if already full grown
278-
if plant_height == def.steps then
279-
return
280-
end
281-
282-
local node_def = minetest.registered_items[node.name] or nil
283-
284-
-- grow seed
285-
if minetest.get_item_group(node.name, "seed") and node_def.fertility then
286-
local can_grow = false
287-
local soil_node = minetest.get_node_or_nil({x = pos.x, y = pos.y - 1, z = pos.z})
288-
if not soil_node then
289-
return
290-
end
291-
for _, v in pairs(node_def.fertility) do
292-
if minetest.get_item_group(soil_node.name, v) ~= 0 then
293-
can_grow = true
294-
end
295-
end
296-
if can_grow then
297-
minetest.set_node(pos, {name = node.name:gsub("seed_", "") .. "_1"})
298-
end
299-
return
300-
end
301-
302-
-- check if on wet soil
303-
pos.y = pos.y - 1
304-
local n = minetest.get_node(pos)
305-
if minetest.get_item_group(n.name, "soil") < 3 then
306-
return
307-
end
308-
pos.y = pos.y + 1
309-
310-
-- check light
311-
local ll = minetest.get_node_light(pos)
312-
313-
if not ll or ll < def.minlight or ll > def.maxlight then
314-
return
315-
end
316-
317-
-- grow
318-
minetest.set_node(pos, {name = mname .. ":" .. pname .. "_" .. plant_height + 1})
319-
end
356+
tick_again(pos)
357+
end,
320358
})
321359

322360
-- Return

0 commit comments

Comments
 (0)
Please sign in to comment.