Cut down on some undo closures
If I can get rid of all of these, I can combine multiple undo entries, and allow undoing backwards in time further (but more coarsely) with the same memory usage. This also introduces some actor pooling, which... reduces memory usage very slightly on clone-heavy levels, but may not be worth it overall.
This commit is contained in:
parent
c900ec80db
commit
20b19c53ff
155
js/game.js
155
js/game.js
@ -181,7 +181,7 @@ Object.assign(Tile.prototype, {
|
|||||||
wire_tunnel_directions: 0,
|
wire_tunnel_directions: 0,
|
||||||
// Actor defaults
|
// Actor defaults
|
||||||
movement_cooldown: 0,
|
movement_cooldown: 0,
|
||||||
movement_speed: 12,
|
movement_speed: null,
|
||||||
previous_cell: null,
|
previous_cell: null,
|
||||||
is_sliding: false,
|
is_sliding: false,
|
||||||
is_pending_slide: false,
|
is_pending_slide: false,
|
||||||
@ -264,6 +264,18 @@ export class Cell extends Array {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UndoEntry {
|
||||||
|
constructor() {
|
||||||
|
this.misc_closures = [];
|
||||||
|
this.tile_changes = new Map;
|
||||||
|
this.level_props = {};
|
||||||
|
this.actor_splices = [];
|
||||||
|
this.toggle_green_tiles = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// The undo stack is implemented with a ring buffer, and this is its size. One entry per tic.
|
// The undo stack is implemented with a ring buffer, and this is its size. One entry per tic.
|
||||||
// Based on Chrome measurements made against the pathological level CCLP4 #40 (Periodic Lasers) and
|
// Based on Chrome measurements made against the pathological level CCLP4 #40 (Periodic Lasers) and
|
||||||
// sitting completely idle, undo consumes about 2 MB every five seconds, so this shouldn't go beyond
|
// sitting completely idle, undo consumes about 2 MB every five seconds, so this shouldn't go beyond
|
||||||
@ -353,10 +365,14 @@ export class Level extends LevelInterface {
|
|||||||
this.undo_buffer[i] = null;
|
this.undo_buffer[i] = null;
|
||||||
}
|
}
|
||||||
this.undo_buffer_index = 0;
|
this.undo_buffer_index = 0;
|
||||||
this.pending_undo = this.create_undo_entry();
|
this.pending_undo = new UndoEntry;
|
||||||
// If undo_enabled is false, we won't create any undo entries.
|
// On levels with a lot of cloners pointing directly into water, monsters can be created and
|
||||||
// Undo is only disabled during bulk testing, where a) there's no
|
// destroyed frequently, and each of them has to be persisted in the undo buffer. To cut
|
||||||
// possibility of needing to undo and b) the overhead is noticable.
|
// down on the bloat somewhat, keep around the last handful of destroyed actors, and reuse
|
||||||
|
// them when spawning a new actor.
|
||||||
|
this.destroyed_tile_pool = [];
|
||||||
|
// If undo_enabled is false, we won't create any undo entries. Undo is only disabled during
|
||||||
|
// bulk testing, where a) no one will ever undo and b) the overhead is significant.
|
||||||
this.undo_enabled = true;
|
this.undo_enabled = true;
|
||||||
|
|
||||||
// Order in which actors try to collide with tiles.
|
// Order in which actors try to collide with tiles.
|
||||||
@ -463,7 +479,6 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.recalculate_circuitry_next_wire_phase = false;
|
this.recalculate_circuitry_next_wire_phase = false;
|
||||||
this.undid_past_recalculate_circuitry = false;
|
|
||||||
this.recalculate_circuitry(true);
|
this.recalculate_circuitry(true);
|
||||||
|
|
||||||
// Finally, let all tiles do custom init behavior... but backwards, to match actor order
|
// Finally, let all tiles do custom init behavior... but backwards, to match actor order
|
||||||
@ -478,7 +493,7 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Erase undo, in case any on_ready added to it (we don't want to undo initialization!)
|
// Erase undo, in case any on_ready added to it (we don't want to undo initialization!)
|
||||||
this.pending_undo = this.create_undo_entry();
|
this.pending_undo = new UndoEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
connect_button(connectable) {
|
connect_button(connectable) {
|
||||||
@ -525,9 +540,10 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
recalculate_circuitry(first_time = false, undoing = false) {
|
recalculate_circuitry(first_time = false) {
|
||||||
// Build circuits out of connected wires
|
// Build circuits out of connected wires
|
||||||
// TODO document this idea
|
// TODO document this idea
|
||||||
|
// TODO moving a circuit block should only need to invalidate the circuits it touches
|
||||||
|
|
||||||
this.circuits = [];
|
this.circuits = [];
|
||||||
this.power_sources = [];
|
this.power_sources = [];
|
||||||
@ -631,14 +647,11 @@ export class Level extends LevelInterface {
|
|||||||
for (let j = 0; j < this.height; j++) {
|
for (let j = 0; j < this.height; j++) {
|
||||||
let terrain = this.cell(i, j).get_terrain();
|
let terrain = this.cell(i, j).get_terrain();
|
||||||
if (terrain.is_wired !== undefined) {
|
if (terrain.is_wired !== undefined) {
|
||||||
|
// XXX begin? if it's NOT the first time??
|
||||||
terrain.type.on_begin(terrain, this);
|
terrain.type.on_begin(terrain, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! undoing) {
|
|
||||||
this._push_pending_undo(() => this.undid_past_recalculate_circuitry = true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -827,7 +840,7 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
// It's not possible to rewind to before this happened, so clear undo and permanently
|
// It's not possible to rewind to before this happened, so clear undo and permanently
|
||||||
// set a flag
|
// set a flag
|
||||||
this.pending_undo = this.create_undo_entry();
|
this.pending_undo = new UndoEntry;
|
||||||
this.done_on_begin = true;
|
this.done_on_begin = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1119,7 +1132,7 @@ export class Level extends LevelInterface {
|
|||||||
if (actor.movement_cooldown <= 0) {
|
if (actor.movement_cooldown <= 0) {
|
||||||
if (actor.type.ttl) {
|
if (actor.type.ttl) {
|
||||||
// This is an animation that just finished, so destroy it
|
// This is an animation that just finished, so destroy it
|
||||||
this.remove_tile(actor);
|
this.remove_tile(actor, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1273,10 +1286,11 @@ export class Level extends LevelInterface {
|
|||||||
if (this.pending_green_toggle) {
|
if (this.pending_green_toggle) {
|
||||||
// Swap green objects
|
// Swap green objects
|
||||||
this.__toggle_green_tiles();
|
this.__toggle_green_tiles();
|
||||||
this._push_pending_undo(() => {
|
|
||||||
this.__toggle_green_tiles();
|
|
||||||
});
|
|
||||||
this.pending_green_toggle = false;
|
this.pending_green_toggle = false;
|
||||||
|
|
||||||
|
if (this.undo_enabled) {
|
||||||
|
this.pending_undo.toggle_green_tiles = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1326,9 +1340,8 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
p += 1;
|
p += 1;
|
||||||
}
|
}
|
||||||
else {
|
else if (this.undo_enabled) {
|
||||||
let local_p = p;
|
this.pending_undo.actor_splices.push([p, 0, actor]);
|
||||||
this._push_pending_undo(() => this.actors.splice(local_p, 0, actor));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.actors.length = p;
|
this.actors.length = p;
|
||||||
@ -2020,7 +2033,7 @@ export class Level extends LevelInterface {
|
|||||||
let original_cell = actor.cell;
|
let original_cell = actor.cell;
|
||||||
// Physically remove the actor first, so that it won't get in the way of e.g. a splash
|
// Physically remove the actor first, so that it won't get in the way of e.g. a splash
|
||||||
// spawned from stepping off of a lilypad
|
// spawned from stepping off of a lilypad
|
||||||
this.remove_tile(actor);
|
this.remove_tile(actor, false);
|
||||||
|
|
||||||
// Announce we're leaving, for the handful of tiles that care about it. Do so from the top
|
// Announce we're leaving, for the handful of tiles that care about it. Do so from the top
|
||||||
// down, specifically so dynamite becomes lit before a lilypad tries to splash
|
// down, specifically so dynamite becomes lit before a lilypad tries to splash
|
||||||
@ -2197,7 +2210,7 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now physically move the actor, but their movement waits until next decision phase
|
// Now physically move the actor, but their movement waits until next decision phase
|
||||||
this.remove_tile(actor);
|
this.remove_tile(actor, false);
|
||||||
this.add_tile(actor, dest.cell);
|
this.add_tile(actor, dest.cell);
|
||||||
// Erase this to prevent tail-biting through a teleport
|
// Erase this to prevent tail-biting through a teleport
|
||||||
this._set_tile_prop(actor, 'previous_cell', null);
|
this._set_tile_prop(actor, 'previous_cell', null);
|
||||||
@ -2309,11 +2322,17 @@ export class Level extends LevelInterface {
|
|||||||
|
|
||||||
_do_wire_phase() {
|
_do_wire_phase() {
|
||||||
let force_next_wire_phase = false;
|
let force_next_wire_phase = false;
|
||||||
if (this.recalculate_circuitry_next_wire_phase)
|
if (this.recalculate_circuitry_next_wire_phase) {
|
||||||
{
|
|
||||||
this.recalculate_circuitry();
|
this.recalculate_circuitry();
|
||||||
this.recalculate_circuitry_next_wire_phase = false;
|
this.recalculate_circuitry_next_wire_phase = false;
|
||||||
force_next_wire_phase = true;
|
force_next_wire_phase = true;
|
||||||
|
|
||||||
|
// This property doesn't tend to last beyond a single tic, but if we recalculate now, we
|
||||||
|
// also need to recalculate if we undo beyond this point. So set it as a level prop,
|
||||||
|
// which after an undo, will then cause us to recalculate the next time we advance
|
||||||
|
if (this.undo_enabled) {
|
||||||
|
this.pending_undo.level_props.recalculate_circuitry_next_wire_phase = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.circuits.length === 0)
|
if (this.circuits.length === 0)
|
||||||
@ -2490,13 +2509,6 @@ export class Level extends LevelInterface {
|
|||||||
|
|
||||||
// Undo/redo --------------------------------------------------------------------------------------
|
// Undo/redo --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
create_undo_entry() {
|
|
||||||
let entry = [];
|
|
||||||
entry.tile_changes = new Map;
|
|
||||||
entry.level_props = {};
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
has_undo() {
|
has_undo() {
|
||||||
let prev_index = this.undo_buffer_index - 1;
|
let prev_index = this.undo_buffer_index - 1;
|
||||||
if (prev_index < 0) {
|
if (prev_index < 0) {
|
||||||
@ -2511,7 +2523,7 @@ export class Level extends LevelInterface {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.undo_buffer[this.undo_buffer_index] = this.pending_undo;
|
this.undo_buffer[this.undo_buffer_index] = this.pending_undo;
|
||||||
this.pending_undo = this.create_undo_entry();
|
this.pending_undo = new UndoEntry;
|
||||||
|
|
||||||
this.undo_buffer_index += 1;
|
this.undo_buffer_index += 1;
|
||||||
this.undo_buffer_index %= UNDO_BUFFER_SIZE;
|
this.undo_buffer_index %= UNDO_BUFFER_SIZE;
|
||||||
@ -2522,7 +2534,7 @@ export class Level extends LevelInterface {
|
|||||||
|
|
||||||
// In turn-based mode, we might still be in mid-tic with a partial undo stack; do that first
|
// In turn-based mode, we might still be in mid-tic with a partial undo stack; do that first
|
||||||
this._undo_entry(this.pending_undo);
|
this._undo_entry(this.pending_undo);
|
||||||
this.pending_undo = this.create_undo_entry();
|
this.pending_undo = new UndoEntry;
|
||||||
|
|
||||||
this.undo_buffer_index -= 1;
|
this.undo_buffer_index -= 1;
|
||||||
if (this.undo_buffer_index < 0) {
|
if (this.undo_buffer_index < 0) {
|
||||||
@ -2530,11 +2542,6 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
this._undo_entry(this.undo_buffer[this.undo_buffer_index]);
|
this._undo_entry(this.undo_buffer[this.undo_buffer_index]);
|
||||||
this.undo_buffer[this.undo_buffer_index] = null;
|
this.undo_buffer[this.undo_buffer_index] = null;
|
||||||
|
|
||||||
if (this.undid_past_recalculate_circuitry) {
|
|
||||||
this.recalculate_circuitry_next_wire_phase = true;
|
|
||||||
this.undid_past_recalculate_circuitry = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reverse a single undo entry
|
// Reverse a single undo entry
|
||||||
@ -2543,13 +2550,29 @@ export class Level extends LevelInterface {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Undo in reverse order! There's no redo, so it's okay to destroy this
|
console.log(entry);
|
||||||
entry.reverse();
|
|
||||||
for (let undo of entry) {
|
// Undo in reverse order! There's no redo, so it's okay to use the destructive reverse().
|
||||||
undo();
|
// Green toggle goes first, since it's the last thing to happen in a tic
|
||||||
|
if (entry.pending_green_toggle) {
|
||||||
|
this.__toggle_green_tiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry.misc_closures.reverse();
|
||||||
|
for (let closure of entry.misc_closures) {
|
||||||
|
closure();
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.actor_splices.reverse();
|
||||||
|
for (let args of entry.actor_splices) {
|
||||||
|
this.actors.splice(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
let needs_readding = [];
|
||||||
for (let [tile, changes] of entry.tile_changes) {
|
for (let [tile, changes] of entry.tile_changes) {
|
||||||
// If a tile's cell or layer changed, it needs to be removed and then added
|
// If a tile's cell or layer changed, it needs to be removed and then added -- but to
|
||||||
|
// avoid ordering problems when a tile leaves a cell and a different tile enters that
|
||||||
|
// cell on the same tic, we can't add back any tiles until they've all been removed
|
||||||
let do_cell_dance = (Object.hasOwn(changes, 'cell') || (
|
let do_cell_dance = (Object.hasOwn(changes, 'cell') || (
|
||||||
Object.hasOwn(changes, 'type') && tile.type.layer !== changes.type.layer));
|
Object.hasOwn(changes, 'type') && tile.type.layer !== changes.type.layer));
|
||||||
if (do_cell_dance && tile.cell) {
|
if (do_cell_dance && tile.cell) {
|
||||||
@ -2557,9 +2580,12 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
Object.assign(tile, changes);
|
Object.assign(tile, changes);
|
||||||
if (do_cell_dance && tile.cell) {
|
if (do_cell_dance && tile.cell) {
|
||||||
tile.cell._add(tile);
|
needs_readding.push(tile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (let tile of needs_readding) {
|
||||||
|
tile.cell._add(tile);
|
||||||
|
}
|
||||||
for (let [key, value] of Object.entries(entry.level_props)) {
|
for (let [key, value] of Object.entries(entry.level_props)) {
|
||||||
this[key] = value;
|
this[key] = value;
|
||||||
}
|
}
|
||||||
@ -2567,7 +2593,7 @@ export class Level extends LevelInterface {
|
|||||||
|
|
||||||
_push_pending_undo(thunk) {
|
_push_pending_undo(thunk) {
|
||||||
if (this.undo_enabled) {
|
if (this.undo_enabled) {
|
||||||
this.pending_undo.push(thunk)
|
this.pending_undo.misc_closures.push(thunk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2684,7 +2710,7 @@ export class Level extends LevelInterface {
|
|||||||
this.transmute_tile(actor, animation_name);
|
this.transmute_tile(actor, animation_name);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.remove_tile(actor);
|
this.remove_tile(actor, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2761,9 +2787,16 @@ export class Level extends LevelInterface {
|
|||||||
|
|
||||||
// Tile stuff in particular
|
// Tile stuff in particular
|
||||||
|
|
||||||
remove_tile(tile) {
|
remove_tile(tile, destroying = false) {
|
||||||
tile.cell._remove(tile);
|
tile.cell._remove(tile);
|
||||||
this._set_tile_prop(tile, 'cell', null);
|
this._set_tile_prop(tile, 'cell', null);
|
||||||
|
|
||||||
|
if (destroying) {
|
||||||
|
this.destroyed_tile_pool.push(tile);
|
||||||
|
if (this.destroyed_tile_pool.length > 8) {
|
||||||
|
this.destroyed_tile_pool.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
add_tile(tile, cell) {
|
add_tile(tile, cell) {
|
||||||
@ -2771,6 +2804,24 @@ export class Level extends LevelInterface {
|
|||||||
cell._add(tile);
|
cell._add(tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
make_actor(type, direction = 'south') {
|
||||||
|
let actor;
|
||||||
|
if (this.destroyed_tile_pool.length > 0) {
|
||||||
|
actor = this.destroyed_tile_pool.shift();
|
||||||
|
// Clear out anything already set on the tile
|
||||||
|
for (let key of Object.keys(actor)) {
|
||||||
|
this._set_tile_prop(actor, key, Tile.prototype[key]);
|
||||||
|
}
|
||||||
|
this._set_tile_prop(actor, 'type', type);
|
||||||
|
this._set_tile_prop(actor, 'direction', direction);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
actor = new Tile(type, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
return actor;
|
||||||
|
}
|
||||||
|
|
||||||
add_actor(actor) {
|
add_actor(actor) {
|
||||||
if (this.compat.actors_move_instantly) {
|
if (this.compat.actors_move_instantly) {
|
||||||
actor.moves_instantly = true;
|
actor.moves_instantly = true;
|
||||||
@ -2783,14 +2834,18 @@ export class Level extends LevelInterface {
|
|||||||
let old_actor = this.actors[i];
|
let old_actor = this.actors[i];
|
||||||
if (old_actor !== this.player && ! old_actor.cell) {
|
if (old_actor !== this.player && ! old_actor.cell) {
|
||||||
this.actors[i] = actor;
|
this.actors[i] = actor;
|
||||||
this._push_pending_undo(() => this.actors[i] = old_actor);
|
if (this.undo_enabled) {
|
||||||
|
this.pending_undo.actor_splices.push([i, 1, old_actor]);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.actors.push(actor);
|
this.actors.push(actor);
|
||||||
this._push_pending_undo(() => this.actors.pop());
|
if (this.undo_enabled) {
|
||||||
|
this.pending_undo.actor_splices.push([this.actors.length - 1, 1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_init_animation(tile) {
|
_init_animation(tile) {
|
||||||
@ -2819,7 +2874,7 @@ export class Level extends LevelInterface {
|
|||||||
if (type.layer === LAYERS.vfx) {
|
if (type.layer === LAYERS.vfx) {
|
||||||
let vfx = cell[type.layer];
|
let vfx = cell[type.layer];
|
||||||
if (vfx) {
|
if (vfx) {
|
||||||
this.remove_tile(vfx);
|
this.remove_tile(vfx, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let tile = new Tile(type);
|
let tile = new Tile(type);
|
||||||
|
|||||||
@ -1685,7 +1685,7 @@ const TILE_TYPES = {
|
|||||||
}
|
}
|
||||||
if (success) {
|
if (success) {
|
||||||
// FIXME add this underneath, just above the cloner, so the new actor is on top
|
// FIXME add this underneath, just above the cloner, so the new actor is on top
|
||||||
let new_template = new actor.constructor(type, direction);
|
let new_template = level.make_actor(type, direction);
|
||||||
if (type.on_clone) {
|
if (type.on_clone) {
|
||||||
type.on_clone(new_template, actor);
|
type.on_clone(new_template, actor);
|
||||||
}
|
}
|
||||||
@ -3148,11 +3148,7 @@ const TILE_TYPES = {
|
|||||||
level.transmute_tile(level.ankh_tile, 'floor');
|
level.transmute_tile(level.ankh_tile, 'floor');
|
||||||
level.spawn_animation(level.ankh_tile.cell, 'puff');
|
level.spawn_animation(level.ankh_tile.cell, 'puff');
|
||||||
}
|
}
|
||||||
let old_tile = level.ankh_tile;
|
|
||||||
level.ankh_tile = terrain;
|
level.ankh_tile = terrain;
|
||||||
level._push_pending_undo(() => {
|
|
||||||
level.ankh_tile = old_tile;
|
|
||||||
});
|
|
||||||
level.transmute_tile(terrain, 'floor_ankh');
|
level.transmute_tile(terrain, 'floor_ankh');
|
||||||
// TODO some kinda vfx + sfx
|
// TODO some kinda vfx + sfx
|
||||||
level.remove_tile(me);
|
level.remove_tile(me);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user