Skip to content

Commit 0788926

Browse files
committed
fix fatal crashes/desyncs in collaboration
1 parent bb25fdd commit 0788926

7 files changed

Lines changed: 134 additions & 76 deletions

File tree

src/addons/addons/collaboration/helpers/constants.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,17 @@ export const mutableRefs = {
6262
inactivityTimerY: null,
6363
userIconContainer: null,
6464

65-
isWorkspaceLoading: false,
65+
isInitialRoomSync: false,
6666
loadingCooldownTimer: null,
6767

6868
syncingTargets: new Set(),
6969
syncingCostumes: new Set(),
7070
syncingSounds: new Set(),
7171
costumeIndexMaps: new Map(),
7272

73-
isWorkspaceLoading: true,
73+
isUiTransition: false,
7474
initialSyncEvents: [],
75+
pendingLocalEvents: [],
7576

7677
roomUUID: null
7778
};

src/addons/addons/collaboration/helpers/costumeSync.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,16 +172,18 @@ async function syncRemoteToLocal(targetId) {
172172

173173
const loadPromises = remoteCostumesData.map(async (remoteMeta) => {
174174
let existingMatch = currentCostumes.find(c => c.id === remoteMeta.id);
175+
let staleMatch = null;
175176
if (existingMatch && (existingMatch.md5 || existingMatch.md5ext) !== remoteMeta.md5ext) {
176-
existingMatch = null;
177+
staleMatch = existingMatch;
178+
existingMatch = null;
177179
}
178180
if (!existingMatch) {
179-
existingMatch = currentCostumes.find(c =>
181+
existingMatch = currentCostumes.find(c =>
180182
c.assetId === remoteMeta.assetId && (c.md5 === remoteMeta.md5ext || c.md5ext === remoteMeta.md5ext)
181183
);
182184
}
183185
if (existingMatch) return { existing: existingMatch, meta: remoteMeta };
184-
const loaded = await helper.loadRemoteCostume(remoteMeta, constants.mutableRefs.vm.runtime);
186+
const loaded = await helper.loadRemoteCostume(remoteMeta, constants.mutableRefs.vm.runtime, 3, staleMatch);
185187
return { loaded: loaded, meta: remoteMeta };
186188
});
187189

@@ -210,6 +212,12 @@ async function syncRemoteToLocal(targetId) {
210212
}
211213
target.setCostume(target.currentCostume);
212214
target.updateAllDrawableProperties();
215+
const keptSkinIds = new Set(newCostumeList.map(c => c.skinId).filter(id => id !== undefined));
216+
currentCostumes.forEach(oldCostume => {
217+
if (oldCostume.skinId !== undefined && !keptSkinIds.has(oldCostume.skinId)) {
218+
constants.mutableRefs.vm.runtime.renderer.destroySkin(oldCostume.skinId);
219+
}
220+
});
213221
setTimeout(function() {
214222
const vm = constants.mutableRefs.vm;
215223
if (vm) {

src/addons/addons/collaboration/helpers/helper.js

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -265,55 +265,56 @@ export async function uploadCollaborationAsset(runtime, asset) {
265265
}
266266
}
267267

268-
export async function loadRemoteCostume(costumeData, runtime, retries = 3) {
269-
270-
268+
export async function loadRemoteCostume(costumeData, runtime, retries = 3, existingCostume = null) {
271269
const costume = { ...costumeData };
272270
if (!costume.md5) costume.md5 = costume.md5ext;
273271
if (!costume.dataFormat && costume.md5ext) {
274272
costume.dataFormat = costume.md5ext.split('.').pop();
275273
}
276-
277-
const assetType = costume.dataFormat === 'svg'
278-
? runtime.storage.AssetType.ImageVector
274+
const assetType = costume.dataFormat === 'svg'
275+
? runtime.storage.AssetType.ImageVector
279276
: runtime.storage.AssetType.ImageBitmap;
280-
281277
try {
282-
283278
const asset = await runtime.storage.load(assetType, costume.assetId, costume.dataFormat);
284-
285279
if (!asset) {
286280
if (retries > 0) {
287-
288281
await new Promise(resolve => setTimeout(resolve, 500));
289-
return loadRemoteCostume(costumeData, runtime, retries - 1);
282+
return loadRemoteCostume(costumeData, runtime, retries - 1, existingCostume);
290283
}
291-
292-
return costume;
284+
return costume;
293285
}
294-
295286
costume.asset = asset;
296-
297-
298287
return new Promise(resolve => {
299288
if (costume.dataFormat === 'svg') {
300289
const svgString = asset.decodeText();
301-
costume.skinId = runtime.renderer.createSVGSkin(svgString, [costume.rotationCenterX, costume.rotationCenterY]);
290+
if (existingCostume && existingCostume.skinId !== undefined && existingCostume.dataFormat === 'svg') {
291+
runtime.renderer.updateSVGSkin(existingCostume.skinId, svgString, [costume.rotationCenterX, costume.rotationCenterY]);
292+
costume.skinId = existingCostume.skinId;
293+
} else {
294+
costume.skinId = runtime.renderer.createSVGSkin(svgString, [costume.rotationCenterX, costume.rotationCenterY]);
295+
if (existingCostume && existingCostume.skinId !== undefined) {
296+
runtime.renderer.destroySkin(existingCostume.skinId);
297+
}
298+
}
302299
costume.size = runtime.renderer.getSkinSize(costume.skinId);
303-
304300
resolve(costume);
305301
} else {
306302
const image = new Image();
307303
image.onload = function () {
308-
const skinId = runtime.renderer.createBitmapSkin(
309-
image,
310-
costume.bitmapResolution || 1,
311-
[costume.rotationCenterX, costume.rotationCenterY]
312-
);
313-
costume.skinId = skinId;
304+
const resolution = costume.bitmapResolution || 1;
305+
const center = [costume.rotationCenterX, costume.rotationCenterY];
306+
if (existingCostume && existingCostume.skinId !== undefined && existingCostume.dataFormat !== 'svg') {
307+
runtime.renderer.updateBitmapSkin(existingCostume.skinId, image, resolution,
308+
[center[0] / resolution, center[1] / resolution]);
309+
costume.skinId = existingCostume.skinId;
310+
} else {
311+
costume.skinId = runtime.renderer.createBitmapSkin(image, resolution, center);
312+
if (existingCostume && existingCostume.skinId !== undefined) {
313+
runtime.renderer.destroySkin(existingCostume.skinId);
314+
}
315+
}
314316
const renderSize = runtime.renderer.getSkinSize(costume.skinId);
315-
costume.size = [renderSize[0] * 2, renderSize[1] * 2];
316-
317+
costume.size = [renderSize[0] * 2, renderSize[1] * 2];
317318
resolve(costume);
318319
};
319320
image.onerror = function (err) {
@@ -323,7 +324,6 @@ export async function loadRemoteCostume(costumeData, runtime, retries = 3) {
323324
}
324325
});
325326
} catch (e) {
326-
327327
return costume;
328328
}
329329
}
@@ -577,12 +577,8 @@ export function performInitialSync() {
577577
const remoteName = yTargetMap.get('__targetName');
578578
if (remoteName) {
579579
const localTarget = vm.runtime.targets.find(t => t.getName() === remoteName);
580-
if (localTarget && localTarget.isStage && localTarget.id !== remoteId) {
581-
localTarget.id = remoteId;
582-
localTarget.originalTargetId = remoteId;
583-
Object.values(localTarget.variables).forEach(v => {
584-
v.targetId = remoteId;
585-
});
580+
if (localTarget && localTarget.isStage && localTarget.id !== remoteId) {
581+
vm.runtime.updateTargetId(localTarget, remoteId);
586582
}
587583
}
588584
});
@@ -604,8 +600,7 @@ export function performInitialSync() {
604600
target = vm.runtime.getTargetForStage();
605601
if (target) {
606602
localTargetsMap.delete(target.id);
607-
target.id = id;
608-
target.originalTargetId = id;
603+
vm.runtime.updateTargetId(target, id);
609604
}
610605
}
611606

src/addons/addons/collaboration/helpers/observeHandlers.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,20 @@ export function sharedBlocks(event){
8787
block[key] = val;
8888
}
8989
} else if (change.action === 'delete') {
90-
delete block[key];
90+
if (key.startsWith('["')) {
91+
try {
92+
const pathParts = JSON.parse(key);
93+
let current = block;
94+
for (let i = 0; i < pathParts.length - 1 && current; i++) {
95+
current = current[pathParts[i]];
96+
}
97+
if (current) delete current[pathParts[pathParts.length - 1]];
98+
} catch (e) {
99+
delete block[key];
100+
}
101+
} else {
102+
delete block[key];
103+
}
91104
}
92105
});
93106
target.blocks.updateBlock(block);
@@ -113,6 +126,8 @@ export function sharedBlocksRefresh(needsToolboxRefresh,needsWorkspaceRefresh){
113126

114127
if (needsWorkspaceRefresh) {
115128
if (constants.mutableRefs.vm.editingTarget) {
129+
const blocks = constants.mutableRefs.vm.editingTarget.blocks;
130+
if (typeof blocks.validateAndRepair === 'function') blocks.validateAndRepair();
116131
constants.mutableRefs.vm.emitWorkspaceUpdate();
117132
}
118133
}

src/addons/addons/collaboration/helpers/soundSync.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,21 @@ async function syncRemoteToLocal(targetId) {
121121
return finalSound;
122122
});
123123
constants.mutableRefs.BlocklyInstance.Events.setGroup('yjs-remote-sync');
124+
const soundBank = target.sprite.soundBank;
125+
const keptPlayerIds = new Set(newSoundList.map(s => s.soundId).filter(Boolean));
126+
if (soundBank && soundBank.removeSoundPlayer) {
127+
currentSounds.forEach(oldSound => {
128+
if (oldSound.soundId && !keptPlayerIds.has(oldSound.soundId)) {
129+
soundBank.removeSoundPlayer(oldSound.soundId);
130+
}
131+
});
132+
}
124133
target.sprite.sounds = newSoundList;
125134
for (const sound of newSoundList) {
126-
if (sound.asset && target.sprite.soundBank) {
135+
if (sound.asset && soundBank && !sound.soundId) {
127136
const player = await constants.mutableRefs.vm.runtime.audioEngine.decodeSoundPlayer({ ...sound, data: sound.asset.data });
128137
sound.soundId = player.id;
129-
target.sprite.soundBank.addSoundPlayer(player);
138+
soundBank.addSoundPlayer(player);
130139
}
131140
}
132141
if (constants.mutableRefs.vm.editingTarget?.id === target.id) constants.mutableRefs.vm.emitTargetsUpdate();

0 commit comments

Comments
 (0)