Skip to content

add null safety to FlxReplay #3420

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 66 additions & 112 deletions flixel/system/replay/FlxReplay.hx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package flixel.system.replay;

import flixel.FlxG;
import flixel.util.FlxArrayUtil;
import flixel.util.FlxDestroyUtil;

/**
* The replay object both records and replays game recordings,
Expand All @@ -10,192 +11,155 @@ import flixel.util.FlxArrayUtil;
* but since Flixel is fairly deterministic, we can use these to play back
* recordings of gameplay with a decent amount of fidelity.
*/
class FlxReplay
@:nullSafety(Strict)
class FlxReplay implements IFlxDestroyable
{
/**
* The random number generator seed value for this recording.
*/
public var seed:Int;

public var seed(default, null):Int = 0;
/**
* The current frame for this recording.
*/
public var frame:Int;

public var frame(default, null):Int = 0;
/**
* The number of frames in this recording.
* **Note:** This doesn't include empty records, unlike `getDuration()`
*/
public var frameCount:Int;

public var frameCount(get, never):Int;
inline function get_frameCount() return _frames.length;

/**
* Whether the replay has finished playing or not.
*/
public var finished:Bool;

public var finished(default, null):Bool = false;
/**
* Internal container for all the frames in this replay.
*/
var _frames:Array<FrameRecord>;

/**
* Internal tracker for max number of frames we can fit before growing the _frames again.
*/
var _capacity:Int;

final _frames:Array<FrameRecord> = [];

/**
* Internal helper variable for keeping track of where we are in _frames during recording or replay.
*/
var _marker:Int;

var _marker:Int = 0;
/**
* Instantiate a new replay object. Doesn't actually do much until you call create() or load().
*/
public function new()
public function new() {}

/**
* Common initialization terms used by both create() and load() to set up the replay object.
*/
function init():Void
{
seed = 0;
frame = 0;
frameCount = 0;
finished = false;
_frames = null;
_capacity = 0;
_marker = 0;
destroy();
}

/**
* Clean up memory.
*/
public function destroy():Void
{
if (_frames == null)
{
return;
}
var i:Int = frameCount - 1;
while (i >= 0)
{
_frames[i--].destroy();
}
_frames = null;
FlxDestroyUtil.destroyArray(_frames);
}

/**
* Create a new gameplay recording. Requires the current random number generator seed.
*
* @param Seed The current seed from the random number generator.
* @param seed The current seed from the random number generator.
*/
public function create(Seed:Int):Void
public function create(seed:Int):Void
{
destroy();
init();
seed = Seed;
this.seed = seed;
rewind();
}

/**
* Load replay data from a String object.
* Strings can come from embedded assets or external
* Load replay data from a String object. Strings can come from embedded assets or external
* files loaded through the debugger overlay.
* @param FileContents A String object containing a gameplay recording.
*
* @param fileContents A String object containing a gameplay recording.
*/
public function load(FileContents:String):Void
public function load(fileContents:String):Void
{
init();

var lines:Array<String> = FileContents.split("\n");

seed = Std.parseInt(lines[0]);

var line:String;
var i:Int = 1;
var l:Int = lines.length;
while (i < l)
final lines = fileContents.split("\n");
final seedStr = lines.shift();
final parsedSeed:Null<Int> = seedStr == null ? null : Std.parseInt(seedStr);
if (parsedSeed == null)
throw 'Invalid replay: $fileContents';

seed = parsedSeed;
for (line in lines)
{
line = lines[i++];
if (line.length > 3)
{
_frames[frameCount++] = new FrameRecord().load(line);
if (frameCount >= _capacity)
{
_capacity *= 2;
_frames.resize(_capacity);
}
}
_frames.push(new FrameRecord().load(line));
}

rewind();
}

/**
* Save the current recording data off to a String object.
* Basically goes through and calls FrameRecord.save() on each frame in the replay.
* return The gameplay recording in simple ASCII format.
* Save the current recording data off to a String object. Basically goes through and
* calls FrameRecord.save() on each frame in the replay.
*
* @return The gameplay recording in simple ASCII format.
*/
public function save():String
public function save():Null<String>
{
if (frameCount <= 0)
{
return null;
}
var output:String = seed + "\n";
var i:Int = 0;
while (i < frameCount)
{
output += _frames[i++].save() + "\n";
}
return output;
return Lambda.fold(_frames, (frame, result)->'${result}${frame.save()}\n', '$seed\n');
}

/**
* Get the current input data from the input managers and store it in a new frame record.
*/
public function recordFrame():Void
{
var continueFrame = true;

#if FLX_KEYBOARD
var keysRecord:Array<CodeValuePair> = FlxG.keys.record();
if (keysRecord != null)
continueFrame = false;
#end

#if FLX_MOUSE
var mouseRecord:MouseRecord = FlxG.mouse.record();
if (mouseRecord != null)
continueFrame = false;
#end

if (continueFrame)
{
frame++;
return;
}

var frameRecorded = new FrameRecord().create(frame++);
#if FLX_MOUSE
frameRecorded.mouse = mouseRecord;
#end
#if FLX_KEYBOARD
frameRecorded.keys = keysRecord;
#end

_frames[frameCount++] = frameRecorded;

if (frameCount >= _capacity)
{
_capacity *= 2;
_frames.resize(_capacity);
}

_frames.push(frameRecorded);
}

/**
* Get the current frame record data and load it into the input managers.
*/
public function playNextFrame():Void
{
FlxG.inputs.reset();

if (_marker >= frameCount)
if (_marker >= _frames.length)
{
finished = true;
return;
Expand All @@ -204,24 +168,24 @@ class FlxReplay
{
return;
}

var fr:FrameRecord = _frames[_marker++];

#if FLX_KEYBOARD
if (fr.keys != null)
{
FlxG.keys.playback(fr.keys);
}
#end

#if FLX_MOUSE
if (fr.mouse != null)
{
FlxG.mouse.playback(fr.mouse);
}
#end
}

/**
* Reset the replay back to the first frame.
*/
Expand All @@ -231,16 +195,6 @@ class FlxReplay
frame = 0;
finished = false;
}

/**
* Common initialization terms used by both create() and load() to set up the replay object.
*/
function init():Void
{
_capacity = 100;
_frames = new Array<FrameRecord>();
frameCount = 0;
}

/**
* The duration of this replay, in frames. **Note:** this is different from `frameCount`, which
Expand Down
4 changes: 3 additions & 1 deletion flixel/system/replay/FrameRecord.hx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package flixel.system.replay;

import flixel.util.FlxDestroyUtil;

/**
* Helper class for the new replay system. Represents all the game inputs for one "frame" or "step" of the game loop.
*/
class FrameRecord
class FrameRecord implements IFlxDestroyable
{
/**
* Which frame of the game loop this record is from or for.
Expand Down
4 changes: 3 additions & 1 deletion flixel/util/FlxDestroyUtil.hx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ class FlxDestroyUtil
{
for (e in array)
destroy(e);
array.splice(0, array.length);

array.resize(0);
}

return null;
}

Expand Down