Reduce undo memory usage by a third

- Changes to tiles are now stored in a plain object rather than a Map,
  which it turns out takes up a decent bit more space.

- Changes to a tile's type or cell no longer need additional closures to
  perform the cell movement.

- Less impactful, but changes to level properties are now stored as a
  diff, not as a full set every tic.
This commit is contained in:
Eevee (Evelyn Woods) 2024-05-06 11:21:27 -06:00
parent 63da1ff38c
commit c900ec80db

View File

@ -181,9 +181,13 @@ 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,
previous_cell: null,
is_sliding: false, is_sliding: false,
is_pending_slide: false, is_pending_slide: false,
can_override_slide: false, can_override_slide: false,
is_blocked: false,
is_pushing: false,
pending_push: null, pending_push: null,
destination_cell: null, destination_cell: null,
is_making_failure_move: false, is_making_failure_move: false,
@ -204,10 +208,8 @@ export class Cell extends Array {
if (index !== LAYERS.vfx) { if (index !== LAYERS.vfx) {
console.error("ATTEMPTING TO ADD", tile, "TO CELL", this, "WHICH ERASES EXISTING TILE", this[index]); console.error("ATTEMPTING TO ADD", tile, "TO CELL", this, "WHICH ERASES EXISTING TILE", this[index]);
} }
this[index].cell = null;
} }
this[index] = tile; this[index] = tile;
tile.cell = this;
} }
// DO NOT use me to remove a tile permanently, only to move it! // DO NOT use me to remove a tile permanently, only to move it!
@ -218,7 +220,6 @@ export class Cell extends Array {
throw new Error("Asked to remove tile that doesn't seem to exist"); throw new Error("Asked to remove tile that doesn't seem to exist");
this[index] = null; this[index] = null;
tile.cell = null;
} }
get_wired_tile() { get_wired_tile() {
@ -426,6 +427,7 @@ export class Level extends LevelInterface {
} }
} }
cell._add(tile); cell._add(tile);
tile.cell = cell;
if (tile.type.connects_to) { if (tile.type.connects_to) {
connectables.push(tile); connectables.push(tile);
@ -830,15 +832,18 @@ export class Level extends LevelInterface {
} }
if (this.undo_enabled) { if (this.undo_enabled) {
let previous_record = this.undo_buffer[(this.undo_buffer_index - 2 + UNDO_BUFFER_SIZE) % UNDO_BUFFER_SIZE];
// Store some current level state in the undo entry. (These will often not be modified, but // Store some current level state in the undo entry. (These will often not be modified, but
// they only take a few bytes each so that's fine.) // they only take a few bytes each so that's fine.)
for (let key of [ for (let key of [
'_rng1', '_rng2', '_blob_modifier', '_tw_rng', 'force_floor_direction', '_rng1', '_rng2', '_blob_modifier', '_tw_rng', 'force_floor_direction',
'tic_counter', 'frame_offset', 'time_remaining', 'timer_paused', 'tic_counter', 'frame_offset', 'time_remaining', 'timer_paused',
'chips_remaining', 'bonus_points', 'state', 'chips_remaining', 'bonus_points', 'state', 'ankh_tile',
'player1_move', 'player2_move', 'remaining_players', 'player', 'player1_move', 'player2_move', 'remaining_players', 'player',
]) { ]) {
this.pending_undo.level_props[key] = this[key]; if (! previous_record || previous_record.level_props[key] !== this[key]) {
this.pending_undo.level_props[key] = this[key];
}
} }
} }
@ -2015,7 +2020,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, true); this.remove_tile(actor);
// 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
@ -2049,9 +2054,6 @@ export class Level extends LevelInterface {
// Now add the actor back; we have to wait this long because e.g. monsters erase splashes // Now add the actor back; we have to wait this long because e.g. monsters erase splashes
if (goal_cell.get_actor()) { if (goal_cell.get_actor()) {
// FIXME a monster or block killing the player will still move into her cell!!! i don't
// know what to do about this, i feel like i tried making monster/player block each
// other before and it did not go well. maybe it was an ordering issue though?
this.add_tile(actor, original_cell); this.add_tile(actor, original_cell);
return; return;
} }
@ -2547,8 +2549,15 @@ export class Level extends LevelInterface {
undo(); undo();
} }
for (let [tile, changes] of entry.tile_changes) { for (let [tile, changes] of entry.tile_changes) {
for (let [key, value] of changes) { // If a tile's cell or layer changed, it needs to be removed and then added
tile[key] = value; let do_cell_dance = (Object.hasOwn(changes, 'cell') || (
Object.hasOwn(changes, 'type') && tile.type.layer !== changes.type.layer));
if (do_cell_dance && tile.cell) {
tile.cell._remove(tile);
}
Object.assign(tile, changes);
if (do_cell_dance && tile.cell) {
tile.cell._add(tile);
} }
} }
for (let [key, value] of Object.entries(entry.level_props)) { for (let [key, value] of Object.entries(entry.level_props)) {
@ -2577,18 +2586,18 @@ export class Level extends LevelInterface {
let changes = this.pending_undo.tile_changes.get(tile); let changes = this.pending_undo.tile_changes.get(tile);
if (! changes) { if (! changes) {
changes = new Map; changes = {};
this.pending_undo.tile_changes.set(tile, changes); this.pending_undo.tile_changes.set(tile, changes);
} }
// If we haven't yet done so, log the original value // If we haven't yet done so, log the original value
if (! changes.has(key)) { if (! Object.hasOwn(changes, key)) {
changes.set(key, tile[key]); changes[key] = tile[key];
} }
// If there's an original value already logged, and it's the value we're about to change // If there's an original value already logged, and it's the value we're about to change
// back to, then delete the change // back to, then delete the change
else if (changes.get(key) === val) { else if (changes[key] === val) {
changes.delete(key); delete changes[key];
} }
tile[key] = val; tile[key] = val;
@ -2653,11 +2662,7 @@ export class Level extends LevelInterface {
this.transmute_tile(this.ankh_tile, 'floor'); this.transmute_tile(this.ankh_tile, 'floor');
this.spawn_animation(ankh_cell, 'resurrection'); this.spawn_animation(ankh_cell, 'resurrection');
let old_tile = this.ankh_tile;
this.ankh_tile = null; this.ankh_tile = null;
this._push_pending_undo(() => {
this.ankh_tile = old_tile;
});
return; return;
} }
} }
@ -2755,19 +2760,15 @@ export class Level extends LevelInterface {
} }
// Tile stuff in particular // Tile stuff in particular
// TODO should add in the right layer? maybe? hard to say what that is when mscc levels might
// have things stacked in a weird order though
// TODO would be nice to make these not be closures but order matters much more here
remove_tile(tile) { remove_tile(tile) {
let cell = tile.cell; tile.cell._remove(tile);
cell._remove(tile); this._set_tile_prop(tile, 'cell', null);
this._push_pending_undo(() => cell._add(tile));
} }
add_tile(tile, cell) { add_tile(tile, cell) {
this._set_tile_prop(tile, 'cell', cell);
cell._add(tile); cell._add(tile);
this._push_pending_undo(() => cell._remove(tile));
} }
add_actor(actor) { add_actor(actor) {
@ -2849,15 +2850,11 @@ export class Level extends LevelInterface {
let new_type = TILE_TYPES[name]; let new_type = TILE_TYPES[name];
if (old_type.layer !== new_type.layer) { if (old_type.layer !== new_type.layer) {
// Move it to the right layer! // Move it to the right layer!
let cell = tile.cell; // (No need to handle undo specially here; undoing the 'type' prop automatically does
cell._remove(tile); // this same remove/add dance)
tile.type = new_type; tile.cell._remove(tile);
cell._add(tile); this._set_tile_prop(tile, 'type', new_type);
this._push_pending_undo(() => { tile.cell._add(tile);
cell._remove(tile);
tile.type = old_type;
cell._add(tile);
});
} }
else { else {
this._set_tile_prop(tile, 'type', new_type); this._set_tile_prop(tile, 'type', new_type);