avm2: Make Math.random() more closely match Flash Player

See the comments for details. Our previous implementation
was 'too good', and broke Bloons Tower Defense 5 by
generating `Number`s that Flash Player would never generate.
This commit is contained in:
Aaron Hill 2024-01-28 13:47:25 -05:00
parent f2880975ba
commit f31d9bc491
7 changed files with 126 additions and 1 deletions

View File

@ -145,5 +145,9 @@ pub fn random<'gc>(
_this: Object<'gc>, _this: Object<'gc>,
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> { ) -> Result<Value<'gc>, Error<'gc>> {
Ok(activation.context.rng.gen_range(0.0f64..1.0f64).into()) // See https://github.com/adobe/avmplus/blob/858d034a3bd3a54d9b70909386435cf4aec81d21/core/MathUtils.cpp#L1731C24-L1731C44
// This generated a restricted set of 'f64' values, which some SWFs implicitly rely on.
const MAX_VAL: u32 = 0x7FFFFFFF;
let rand = activation.context.rng.gen_range(0..MAX_VAL);
Ok(((rand as f64) / (MAX_VAL as f64 + 1f64)).into())
} }

View File

@ -0,0 +1,75 @@
package
{
import flash.display.MovieClip;
import flash.events.Event;
public class CryptScore
{
private static var randamaroo:MovieClip;
private var bs1:Number;
private var a:Number;
private var b:Number;
private var bs2:Number;
private var c:Number;
private var bs3:Number;
private var bs4:Number;
private var d:Number;
private var count:int = 0;
public function CryptScore(param1:Number = 0, param2:Boolean = false)
{
super();
if(randamaroo == null)
{
randamaroo = new MovieClip();
}
randamaroo.addEventListener(Event.ENTER_FRAME,this.randomise,false,0,true);
this.count = Math.random() * 30;
this.value = param1;
}
private function randomise(param1:Event) : void
{
++this.count;
if(this.count >= 15)
{
this.value = this.value;
this.count -= 15;
}
}
public function get value() : Number
{
return this.a - this.b + (this.c - this.d);
}
public function set value(param1:Number) : void
{
this.bs1 = param1;
this.a = Math.random() * 32;
this.bs2 = -this.a;
do
{
this.b = Math.random() * 14;
this.bs3 = -this.b;
}
while(false);
var _loc2_:Number = param1 - (this.a - this.b);
this.c = Math.random() * 23;
this.d = this.c - _loc2_;
this.bs4 = -param1;
}
}
}

View File

@ -0,0 +1,28 @@
package {
import flash.display.MovieClip;
import flash.events.Event;
public class Test extends MovieClip {
public function Test() {
var score = new CryptScore();
for (var i = 0; i < 10000; i ++) {
if (i % 1000 == 0) {
trace("i = " + i);
}
score.value = score.value;
var value = score.value;
if (value !== 0) {
trace("i = " + i + " Bad value: " + value);
}
}
trace("Done!");
}
}
}

View File

@ -0,0 +1,11 @@
i = 0
i = 1000
i = 2000
i = 3000
i = 4000
i = 5000
i = 6000
i = 7000
i = 8000
i = 9000
Done!

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,7 @@
# This file tests that our Math.random() implementation works
# correctly for some weird memory-obfuscation logic.
# Flash Player's `Math.random()` implementation generates
# a limited range of `Number`s in the range [0.0, 1.0), which
# allows this swf to correctly compute the original value
# (instead of a very close value due to rounding).
num_ticks = 1