[SPIGOT-7159] Crop Growth Modifier resolution is severely low. Created: 17/Sep/22  Updated: 25/Dec/24  Resolved: 19/Oct/22

Status: Resolved
Project: Spigot
Component/s: None
Affects Version/s: None
Fix Version/s: None

Type: Bug Priority: Minor
Reporter: Jordan Stevens Assignee: Marvin Rieple
Resolution: Fixed Votes: 0
Labels: 1.19, bug, spigot

Attachments: PNG File image-2022-09-17-21-47-20-257.png     PNG File image-2022-09-17-21-47-34-936.png     PNG File image-2022-09-17-21-59-35-865.png     PNG File image-2022-09-17-22-07-39-172.png     PNG File image-2022-09-17-22-18-07-525.png    
Version: 1.19-R0.1-SNAPSHOT
Guidelines Read: Yes

 Description   

For years growth rate issues have plagued spiggot. 

Where the modifiers seemingly did not effect the server if increased and required significant decrease to reduce. 

Today I have discovered why. 

Within "BlockCrops.randomTick",  there is an if statement as follows:

if (randomsource.nextInt((int)(100.0F/ (float)modifier * (25.0F / f)) +  1 ==0)

This is catastrophically wrong. 

The vanilla probability of crop growth is 1/(25 / f +1)

A modifier to this chance would be 100/Modifier[1/(25 / f +1)]{*}with 100 being normal growth.

This is NOT what occurs in the if statement above. 

(int)(100.0F/ (float)modifier * (25.0F / f)) is where the problem lies. 

This value will always come out as an integer rounded down. I hope it is clear to see that, say for a case of modifier = 100 and f = 10

that (int) 100.0F / (float)100 * (25.0F/10) < 1 and hence = 0. 

Hence, most cases (until modifier is equal to or less than 40) the growth chance will always be based on:

if (randomsource.nextInt(0 +  1 ==0)

Hence a 50% chance as a random int between 0 and 1 are chosen. If 0 , then true. 

This is a bug that, considering forum posts about the issue, has been persistent for years. 

As such I will offer my personal recommendation at a fix below: 

int chancevanilla = (int)(100/(floor(25.0F/f)+1))-1; //%chance in vanilla MC

int ModMult = (int)(modifier/100.0F);

int chancemod = (int)(chancevanilla*ModMult;

if((randomsource.nextInt(99) <= chancemod) // rolls a random integer between 0 and 99 (100 numbers).// If chancevanilla or lower is rolled, then true. 

I.e. if chance vanilla == 33 and then the modifier was 200, then chancemod would be 65 and hence have a 66% chance of success (66 choices lead to a true if statement out of the potential 100 ints between 0 and 99) 



 Comments   
Comment by Marvin Rieple [ 30/Oct/22 ]

Thanks for the notice, made a PR to fix this: spigot#122

Comment by Bobcat00 [ 30/Oct/22 ]

You're missing parentheses.  As written, the division only applies to melonModifier, not pumpkinModifier.

if (randomsource.nextFloat() < (this == Blocks.PUMPKIN_STEM ? worldserver.spigotConfig.pumpkinModifier : worldserver.spigotConfig.melonModifier / (100.0f * (Math.floor((25.0F / f) + 1))))) { // Spigot - SPIGOT-7159: Better modifier resolution 
Comment by Marvin Rieple [ 16/Oct/22 ]

Made a PR for this: spigot#121

Comment by Jordan Stevens [ 23/Sep/22 ]

Oh my 1000 apologies. I see now.

To be blunt. That is elegant and effective. I have never reported an issue before, but I do hope your solution is implemented! :O 

Comment by Marvin Rieple [ 23/Sep/22 ]

f = 10

modifier = 100

 

(modifier / (100.0f * (Math.floor((25.0f / f) +1))))

=(100 / (100.0f * (Math.floor((25.0f / 10) + 1))))

=(100 / (100.0f * (Math.floor((2.5) + 1))))

=(100 / (100.0f * (Math.floor(3.5))))

=(100 / (100.0f *3))

=(1 / 1 * 3)

=(1 / 3)

= 0.3333...

 

100 / (100 * x) = 1 / (1 * x) = 1 / x

100 / (100 * x) != x

Comment by Jordan Stevens [ 22/Sep/22 ]

Erm... okay not sure how you mean
if (randomsource.nextFloat() < (modifier / (100.0f * (Math.floor((25.0f / f) + 1)))))
(modifier / (100.0f * (Math.floor((25.0f / f) + 1)

f = 10

modififer = 100

(modifier / (100.0f * (Math.floor((25.0f / f) + 1) = 1*Math.floor(25/10 + 1) = 3

if (randomsource.nextFloat() < 3)

Yeah that aint scaled down at all chief. 

That is literally an if statement that rings true if a random float between 0 and 1.0 is less than 3...

Comment by Marvin Rieple [ 21/Sep/22 ]
And unfortunately that will not work as random.nextFloat() chooses a random float between 0 and 1.0. 

This is why the value gets scaled down between 0 and 1.

Comment by Jordan Stevens [ 19/Sep/22 ]

No i got the initial problem wrong haha

And unfortunately that will not work as random.nextFloat() chooses a random float between 0 and 1.0. 

My method still has a resolution problem, but guarantees a resolution similar to that of what is currently seen for sub 100 modifiers. And hence is an acceptable solution  

Comment by Marvin Rieple [ 18/Sep/22 ]

I think I misunderstood your original post.

But I think to get a better resolution it would be better to remove the nextInt part completely and use nextFloat.
Something like this, this removes the steps caused by casting to int completely. See red graph.

if (randomsource.nextFloat() < (modifier / (100.0f * (Math.floor((25.0f / f) + 1)))))

Your proposed fix, also suffers from int casting since all intermediary steps are ints. See blue graph.
A better way would be to only cast to int as the last step, which gives only minimal steps. See orange graph.

Comment by Jordan Stevens [ 17/Sep/22 ]

To conclude as that last note got very big very fast... 

There is 0 way to get any growth rate that is between 51% and 99%

 

Your choices with the current system, past ideal maximum in vanilla, are 33%  50% or 100%

Comment by Jordan Stevens [ 17/Sep/22 ]

"If we now input the lowest and highest possible f value into the vanilla formula we get value between: (25 / 10) = 2.5 and (25 / 0.5) = 50"
"We can now also look at the case where the modifier is 200, there we have 100/200 = 0.5 which gives us values between 1.25 and 25. Which half's the possible values random can be and therefore doubles the chance of hitting."

With all due respect... you are wrong. 

randomsource.nextInt will choose a random integer between 0 and n-1... and, because of the int casting... a 2.5 is a 2.... a 2.1 is a 2.... a 1.995 is a.... 1 ...

 * * 

so to change your from:
((100.0F / modifier) * (25.0F / f))
(int)(100.0F / modifier * (25.0F / f)) + 1

So if i am not being clear enough, due to the int casting, the resolution for increasing modifier for modifier > 100 is, quite frankly, garbage. 

E.g:

100 modifier = 100/100 * 25/10 = (int)(2.5)+1 = 2+1 = 3 : 33%
125 modifier = 100/125 * 25/10 = (int) (2) + 1 = 2+1 = 3: 33%
130 modifier = 100/130 * 25/10 = (int)(1.92)+1 = 1+1 = 2: 50%
200 modifier = 100/200 * 25/10 = (int)(1.25)+1 = 1+1 = 2: 50 %

etc etc etc 

The resolution isnt reduced, its pretty much non existant between 100 and 126. This gets worse when you then consider the same growth rate occurs between 126 and 250.... 50%....

An increase of the modifier from 126 to 200... would still only net you a 50% growth chance. 

The argument to be made here is that the reason so many people have stated online "Oh i increased the modifier by another 50% and it didn't do anything) is exactly due to this. 

To full visualise the issue...  see the graph and table below. As you can see it causes a little to 0 effect at all part the 125 mark. (And infact a modifier of 245 would only increase the growth rate by     

 

And to really hit the nail home... 

For a growth speed of 5... there are bad resolutions.... and there is increasing a 17% base chance by 400% and only getting 50 % ...

Comment by Marvin Rieple [ 17/Sep/22 ]

I don't see anything wrong with how the modifier is implemented.

The default vanilla calculation is: (As can be seen: here and here

(int) (25.0F / f) + 1

And looking at the getGrowthSpeed speed method f can be between 10 and 0.5.

For simplicity we ignore the + 1.

If we now input the lowest and highest possible f value into the vanilla formula we get value between: (25 / 10) = 2.5 and (25 / 0.5) = 50
Spigot now modifies this crow rate value with the modifier.

((100.0F / modifier) * (25.0F / f))

As seen in the code 100/modifier implies that the modifier in the config gives the growth rate in % where 100% is normal speed, 50% is half the speed, and 200% is double the speed.
If we for example use a modifier of 50 we get 100/50 = 2 and two times a value between 2.5 and 50 is a value between 5 and 100. Which doubles the possible values random can return and therefore half the chance of hitting 0. (Technically it does not double 100% because of the +1 at the end but it is close enough to ignore it).

We can now also look at the case where the modifier is 200, there we have 100/200 = 0.5 which gives us values between 1.25 and 25. Which half's the possible values random can be and therefore doubles the chance of hitting.
Which is what you would expect. To get a value of 0 by a f of 10 you would need a modifier of 250, which you can also test by using this: geogebra

There could be an argument made that because of the int casting the resolution of the modifier gets worse the larger the modifier goes, but in general there is nothing wrong with the implementation.

Comment by Jordan Stevens [ 17/Sep/22 ]

Im actually an idiot and forgot that Random.nextInt(int n) is exclusive to n

Still think this would give more versatility to the modifiers as it would allow for values other than just 33.3 , 50 and 100 percent via said modifiers. 

int chancevanilla = 100/((int)(25.0F/f)+1); //%chance in vanilla MC

int ModMult = (int)(modifier/100.0F);

int chancemod = (int)(chancevanilla*ModMult;

if((randomsource.nextInt(100) <= chancemod) // rolls a random integer between 0 and 99 (100 numbers).// If chancevanilla or lower is rolled, then true. 

In addition, because the modifier is not independent of the in game modifiers, by increasing the growth modifier you risk making the in game modifiers worthless. 

E.g: 

A modifier of 10000 would make farms grown with 0 water as fast as farms with water. 

I.e. a modifier with a limit of 100% has many flaws. 

Generated at Sat Dec 13 20:49:58 UTC 2025 using Jira 10.3.13#10030013-sha1:56dd970ae30ebfeda3a697d25be1f6388b68a422.