Prevent desync of currentCostume

This commit is contained in:
apple502j 2021-02-26 16:42:36 +09:00
parent 113a4b2c8a
commit cbc370ec58
No known key found for this signature in database
GPG key ID: 13BE71607A63CDF2
3 changed files with 127 additions and 11 deletions

View file

@ -505,18 +505,28 @@ class RenderedTarget extends Target {
return null; return null;
} }
const deletedCostume = this.sprite.deleteCostumeAt(index); const deletedCostume = this.sprite.deleteCostumeAt(index, true); // Update current costume
if (index === this.currentCostume && index === originalCostumeCount - 1) { this.runtime.requestTargetsUpdate(this);
return deletedCostume;
}
/**
* Shift current costume index if needed and re-render this target.
* This needs to be called on all clones to prevent desync crash. (#2661)
* @param {number} index Index of deleted costume
* @param {boolean} isLastCostume Whether the deleted costume was the last costume or not
*/
shiftCurrentCostume (index, isLastCostume) {
if (index === this.currentCostume && isLastCostume) {
this.setCostume(index - 1); this.setCostume(index - 1);
} else if (index < this.currentCostume) { } else if (index < this.currentCostume) {
this.setCostume(this.currentCostume - 1); this.setCostume(this.currentCostume - 1);
} else { } else {
// Index is still the same; however, the costume itself may have changed.
// This causes the renderer to re-render.
this.setCostume(this.currentCostume); this.setCostume(this.currentCostume);
} }
this.runtime.requestTargetsUpdate(this);
return deletedCostume;
} }
/** /**
@ -630,14 +640,15 @@ class RenderedTarget extends Target {
if (newIndex === costumeIndex) return false; if (newIndex === costumeIndex) return false;
const currentCostume = this.getCurrentCostume(); this.sprite.saveCurrentCostumeName();
const costume = this.sprite.costumes[costumeIndex]; const costume = this.sprite.costumes[costumeIndex];
// Use the sprite method for deleting costumes because setCostume is handled manually // Use the sprite method for deleting costumes because setCostume is handled manually
this.sprite.deleteCostumeAt(costumeIndex); this.sprite.deleteCostumeAt(costumeIndex);
this.addCostume(costume, newIndex); this.addCostume(costume, newIndex);
this.currentCostume = this.getCostumeIndexByName(currentCostume.name); // This sets currentCostume
this.sprite.restoreCurrentCostume();
return true; return true;
} }

View file

@ -92,12 +92,43 @@ class Sprite {
} }
/** /**
* Delete a costume by index. * Delete a costume by index, and optionally update current costume for all clones.
* @param {number} index Costume index to be deleted * @param {number} index Costume index to be deleted
* @param {boolean} [optUpdateCurrentCostume] Whether or not to update current costume for all clones
* @return {?object} The deleted costume * @return {?object} The deleted costume
*/ */
deleteCostumeAt (index) { deleteCostumeAt (index, optUpdateCurrentCostume) {
return this.costumes.splice(index, 1)[0]; const isLastCostume = index === this.costumes.length - 1;
const deletedCostume = this.costumes.splice(index, 1)[0];
if (optUpdateCurrentCostume) {
for (const target of this.clones) {
target.shiftCurrentCostume(index, isLastCostume);
}
}
return deletedCostume;
}
/**
* Save current costume temporarliy during reordering, for all clones.
*/
saveCurrentCostumeName () {
for (const target of this.clones) {
target._currentCostumeName = target.getCurrentCostume().name;
}
}
/**
* Restore current costume from saved name.
*/
restoreCurrentCostume () {
for (const target of this.clones) {
if (typeof target._currentCostumeName === 'undefined') continue;
const costumeIndex = this.costumes.findIndex(c => c.name === target._currentCostumeName);
delete target._currentCostumeName;
if (costumeIndex !== target.currentCostume) {
target.setCostume(costumeIndex);
}
}
} }
/** /**

View file

@ -119,6 +119,7 @@ test('deleteCostume', t => {
const a = new RenderedTarget(s, r); const a = new RenderedTarget(s, r);
const renderer = new FakeRenderer(); const renderer = new FakeRenderer();
a.renderer = renderer; a.renderer = renderer;
s.clones = [a];
// x* Costume 1 * Costume 2 // x* Costume 1 * Costume 2
// Costume 2 => Costume 3 // Costume 2 => Costume 3
@ -211,6 +212,70 @@ test('deleteCostume', t => {
t.end(); t.end();
}); });
test('deleteCostume shifts current costume (1/2)', t => {
const o1 = {id: 1};
const o2 = {id: 2};
const o3 = {id: 3};
const r = new Runtime();
const s = new Sprite(null, r);
s.costumes = [o1, o2, o3];
const renderer = new FakeRenderer();
const c1 = new RenderedTarget(s, r);
c1.renderer = renderer;
c1.currentCostume = 0;
const c2 = new RenderedTarget(s, r);
c2.renderer = renderer;
c2.currentCostume = 1;
const c3 = new RenderedTarget(s, r);
c3.renderer = renderer;
c3.currentCostume = 2;
s.clones = [c1, c2, c3];
c1.deleteCostume(0);
t.equals(c1.currentCostume, 0);
t.equals(c2.currentCostume, 0);
t.equals(c3.currentCostume, 1);
t.end();
});
test('deleteCostume shifts current costume (2/2)', t => {
const o1 = {id: 1};
const o2 = {id: 2};
const o3 = {id: 3};
const r = new Runtime();
const s = new Sprite(null, r);
s.costumes = [o1, o2, o3];
const renderer = new FakeRenderer();
const c1 = new RenderedTarget(s, r);
c1.renderer = renderer;
c1.currentCostume = 0;
const c2 = new RenderedTarget(s, r);
c2.renderer = renderer;
c2.currentCostume = 1;
const c3 = new RenderedTarget(s, r);
c3.renderer = renderer;
c3.currentCostume = 2;
s.clones = [c1, c2, c3];
c1.deleteCostume(2);
t.equals(c1.currentCostume, 0);
t.equals(c2.currentCostume, 1);
t.equals(c3.currentCostume, 1);
t.end();
});
test('deleteSound', t => { test('deleteSound', t => {
const o1 = {id: 1}; const o1 = {id: 1};
const o2 = {id: 2}; const o2 = {id: 2};
@ -474,12 +539,16 @@ test('#reorderCostume', t => {
const r = new Runtime(); const r = new Runtime();
const s = new Sprite(null, r); const s = new Sprite(null, r);
s.costumes = [o1, o2, o3, o4, o5]; s.costumes = [o1, o2, o3, o4, o5];
const a = new RenderedTarget(s, r);
const renderer = new FakeRenderer(); const renderer = new FakeRenderer();
const a = new RenderedTarget(s, r);
a.renderer = renderer; a.renderer = renderer;
const b = new RenderedTarget(s, r);
b.renderer = renderer;
s.clones = [a, b];
const resetCostumes = () => { const resetCostumes = () => {
a.setCostume(0); a.setCostume(0);
b.setCostume(0);
s.costumes = [o1, o2, o3, o4, o5]; s.costumes = [o1, o2, o3, o4, o5];
}; };
const costumeIds = () => a.sprite.costumes.map(c => c.id); const costumeIds = () => a.sprite.costumes.map(c => c.id);
@ -487,6 +556,7 @@ test('#reorderCostume', t => {
resetCostumes(); resetCostumes();
t.deepEquals(costumeIds(), [0, 1, 2, 3, 4]); t.deepEquals(costumeIds(), [0, 1, 2, 3, 4]);
t.equals(a.currentCostume, 0); t.equals(a.currentCostume, 0);
t.equals(b.currentCostume, 0);
// Returns false if the costumes are the same and no change occurred // Returns false if the costumes are the same and no change occurred
t.equal(a.reorderCostume(3, 3), false); t.equal(a.reorderCostume(3, 3), false);
@ -495,15 +565,19 @@ test('#reorderCostume', t => {
// Make sure reordering up and down works and current costume follows // Make sure reordering up and down works and current costume follows
resetCostumes(); resetCostumes();
b.setCostume(4);
t.equal(a.reorderCostume(0, 3), true); t.equal(a.reorderCostume(0, 3), true);
t.deepEquals(costumeIds(), [1, 2, 3, 0, 4]); t.deepEquals(costumeIds(), [1, 2, 3, 0, 4]);
t.equals(a.currentCostume, 3); // Index of id=0 t.equals(a.currentCostume, 3); // Index of id=0
t.equals(b.currentCostume, 4); // Index of id=4
resetCostumes(); resetCostumes();
a.setCostume(1); a.setCostume(1);
b.setCostume(3);
t.equal(a.reorderCostume(3, 1), true); t.equal(a.reorderCostume(3, 1), true);
t.deepEquals(costumeIds(), [0, 3, 1, 2, 4]); t.deepEquals(costumeIds(), [0, 3, 1, 2, 4]);
t.equals(a.currentCostume, 2); // Index of id=1 t.equals(a.currentCostume, 2); // Index of id=1
t.equals(b.currentCostume, 1); // Index of id=3
// Out of bounds indices get clamped // Out of bounds indices get clamped
resetCostumes(); resetCostumes();