From ebedac146ca7353733ce5dce0c7b5e09e49b4529 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Mon, 25 Nov 2024 15:33:19 +0800 Subject: [PATCH] :bug: fix: codingclip sideload issues Signed-off-by: SimonShiki --- README.md | 8 ++- src/main/ctx.ts | 1 + src/main/dashboard/app.tsx | 11 +++- src/main/middleware/index.ts | 3 + src/main/patches/applier.ts | 119 ++++++++++++++++++++++++++++------- src/main/util/settings.ts | 2 + src/types/ducktypes.d.ts | 41 +++++++++++- src/types/global.d.ts | 1 + 8 files changed, 156 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 21d6017..d56baed 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ Eureka is a userscript which can load 3rd-party extensions in any Scratch-based |---------------------------------|-------------------------|---------------------------|-----------------------------------------|--------------------------|--------------------| | Scratch | ✅ | ✅ | ✅ | ✅ | ✅ | | Scratch(Spork) | ✅ | ✅ | ✅ | ✅ | ✅ | -| Codingclip | ✅ | ✅ | ❌ | ✅ | ❓ | -| Cocrea | ✅ | ✅ | ❌ | ✅ | ❓ | +| Codingclip | ✅ | ✅ | ☣️(1) | ✅ | ❓ | +| Cocrea | ✅ | ✅ | ✅ | ✅ | ❓ | | Aerfaying (阿儿法营) | ✅ | ✅ | ✅ | ✅ | ❓ | -| Co-Create World (共创世界) | ✅ | ✅ | ❌ | ✅ | ❓ | +| Co-Create World (共创世界) | ✅ | ✅ | ✅ | ✅ | ❓ | | Xiaomawang (小码王) | ✅ | ✅ | ✅ | ✅ | ❓ | | CodeLab | ✅ | ✅ | ✅ | ✅ | ❓ | | 40code | ✅ | ✅ | ✅ | ✅ | ❓ | @@ -42,6 +42,8 @@ Eureka is a userscript which can load 3rd-party extensions in any Scratch-based | ElectraMod | ✅ | ❓ | ❓ | ❓ | ❓ | | XPLab | ✅ | ❓ | ❓ | ❓ | ❓ | +(1): The initialize of the sideloaded blocks in workspace are broken + # 🧵 Why my extensions don't works? Eureka is the glue that makes it all work by independently implementing a Scratch extension loading system in a non-sandboxed environment. But Eureka doesn't completely eliminate the problems that come with different environments - as a prime example, the extension tries to read either a vm or a blocks instance. If your extension doesn't work, check to see if the extension modifies something specific to the Scratch mod, and try to report it to the extension's author. diff --git a/src/main/ctx.ts b/src/main/ctx.ts index b795ecc..b42fb9b 100644 --- a/src/main/ctx.ts +++ b/src/main/ctx.ts @@ -2,5 +2,6 @@ import { version } from '../../package.json'; export const eureka: EurekaContext = { declaredIds: [], + idToURLMapping: new Map(), version }; diff --git a/src/main/dashboard/app.tsx b/src/main/dashboard/app.tsx index a220f07..ae4c0d9 100644 --- a/src/main/dashboard/app.tsx +++ b/src/main/dashboard/app.tsx @@ -485,7 +485,14 @@ function Dashboard() { ); } -document.addEventListener('DOMContentLoaded', () => { + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initialize); +} else { + initialize(); +} + +function initialize () { const style = document.createElement('style'); style.id = 'eureka-styles'; style.innerHTML = `${globalCss}\n${stylesheet}`; @@ -496,7 +503,7 @@ document.addEventListener('DOMContentLoaded', () => { ), document.body); -}); +} eureka.openDashboard = (status: Exclude = DashboardStatus.LOADER) => { setModalStatus(status); diff --git a/src/main/middleware/index.ts b/src/main/middleware/index.ts index 2130ec9..19391db 100644 --- a/src/main/middleware/index.ts +++ b/src/main/middleware/index.ts @@ -35,6 +35,9 @@ export async function forwardedLoadExtensionURL (url: string) { loadedExtensions.set(url, { extension: extensionObj, info }); + eureka.declaredIds.push(info.id); + eureka.idToURLMapping.set(info.id, url); + // Dispose temporary extension container URL.revokeObjectURL(src); document.head.removeChild(elem); diff --git a/src/main/patches/applier.ts b/src/main/patches/applier.ts index 0c9db33..c55a909 100644 --- a/src/main/patches/applier.ts +++ b/src/main/patches/applier.ts @@ -10,7 +10,6 @@ import * as l10n from '../util/l10n'; import formatMessage from 'format-message'; const settings = settingsAgent.getSettings(); -const idToURLMapping = new Map(); /** * Utility function to determine if a value is a Promise. @@ -175,8 +174,8 @@ export function applyPatchesForVM (vm: DucktypedVM, ctx: EurekaContext) { vm.extensionManager, { loadExtensionURL (originalMethod, extensionURL) { - if (idToURLMapping.has(extensionURL)) { - extensionURL = idToURLMapping.get(extensionURL)!; + if (ctx.idToURLMapping.has(extensionURL)) { + extensionURL = ctx.idToURLMapping.get(extensionURL)!; } if (settings.behavior.redirectDeclared && ctx.declaredIds.includes(extensionURL) && !loadedExtensions.has(extensionURL)) { @@ -340,7 +339,7 @@ export function applyPatchesForVM (vm: DucktypedVM, ctx: EurekaContext) { continue; } ctx.declaredIds.push(extensionId, url); - idToURLMapping.set(extensionId, url); + ctx.idToURLMapping.set(extensionId, url); block.opcode = originalOpcode; try { @@ -366,12 +365,19 @@ export function applyPatchesForVM (vm: DucktypedVM, ctx: EurekaContext) { } ctx.declaredIds.push(extensionId, url); - idToURLMapping.set(extensionId, url); + ctx.idToURLMapping.set(extensionId, url); } } } } + // ClipCC-specific, to correctly handle load order + if (!Array.isArray(projectJSON.extensions)) { + for (const extensionId in sideloadExtensionURLs) { + projectJSON.extensions[extensionId] = '0.0.0'; + } + } + // Remove eureka's stuffs, make project data clean if (typeof projectJSON.sideloadExtensionURLs === 'object') { delete projectJSON.sideloadExtensionURLs; @@ -483,29 +489,96 @@ export function applyPatchesForVM (vm: DucktypedVM, ctx: EurekaContext) { } // ClipCC specific patches, to make sideloaded extension a ClipCC extension - if (typeof vm.ccExtensionManager === 'object' && settings.mixins['vm.ccExtensionManager.getExtensionLoadOrder']) { - MixinApplicator.applyTo( - vm.ccExtensionManager, - { - getExtensionLoadOrder (originalMethod, extensions) { - for (const extensionId of extensions) { - if ( - !Object.prototype.hasOwnProperty.call( - vm.ccExtensionManager!.info, - extensionId - ) && - ctx.declaredIds.includes(extensionId) - ) { - vm.ccExtensionManager!.info[extensionId] = { - api: 0 - }; + if (typeof vm.ccExtensionManager === 'object') { + if (settings.mixins['vm.ccExtensionManager.getExtensionLoadOrder']) { + MixinApplicator.applyTo( + vm.ccExtensionManager, + { + getExtensionLoadOrder (originalMethod, extensions) { + for (const extensionId of extensions) { + if ( + !Object.prototype.hasOwnProperty.call( + vm.ccExtensionManager!.info, + extensionId + ) && + ctx.declaredIds.includes(extensionId) + ) { + vm.ccExtensionManager!.info[extensionId] = { + api: 0, + optional: true + }; + } } + + return originalMethod?.(extensions); } + } + ); + } + if (settings.mixins['vm.ccExtensionManager.getLoadedExtensions']) { + MixinApplicator.applyTo( + vm.ccExtensionManager, + { + getLoadedExtensions (originalMethod, optional) { + const result = originalMethod?.(optional); + if ('__eureka' in result) { + delete result.__eureka; + } - return originalMethod?.(extensions); + return result; + } + } + ); + } + + vm.ccExtensionManager.info.__eureka = vm.ccExtensionManager.load.__eureka = { + api: 0, + optional: true + }; + + vm.ccExtensionManager.instance.__eureka = { + beforeProjectSave ({projectData}: CCXSaveData) { + // Create a Record of extension's id - extension's url from loadedExtensions + const extensionInfo: Record = {}; + loadedExtensions.forEach(({ info }, url) => { + extensionInfo[info.id] = url; + }); + + const sideloadIds = Object.keys(extensionInfo); + + for (const target of projectData.targets) { + for (const blockId in target.blocks) { + const block = target.blocks[blockId]; + if (!block.opcode) continue; + const extensionId = getExtensionIdForOpcode(block.opcode); + if (!extensionId) continue; + if (sideloadIds.includes(extensionId)) { + const mutation = block.mutation ? JSON.stringify(block.mutation) : null; + if (!('mutation' in block)) block.mutation = {}; + block.mutation.proccode = `[📎 Sideload] ${block.opcode}`; + block.mutation.children = []; + if (mutation) block.mutation.mutation = mutation; + block.mutation.tagName = 'mutation'; + + block.opcode = 'procedures_call'; + } + } + } + for (let i = 0; i < projectData.monitors.length; i++) { + const monitor = projectData.monitors[i]; + if (!monitor.opcode) continue; + const extensionId = getExtensionIdForOpcode(monitor.opcode); + if (!extensionId) continue; + if (sideloadIds.includes(extensionId)) { + if (!('sideloadMonitors' in projectData)) projectData.sideloadMonitors = []; + projectData.sideloadMonitors.push(monitor); + projectData.monitors.splice(i, 1); + } } + + projectData.sideloadExtensionURLs = extensionInfo; } - ); + }; } // Turbowarp extension's polyfill diff --git a/src/main/util/settings.ts b/src/main/util/settings.ts index 2cc398f..a4bcc2a 100644 --- a/src/main/util/settings.ts +++ b/src/main/util/settings.ts @@ -16,6 +16,7 @@ interface Settings { 'vm.runtime._primitives.argument_reporter_boolean': boolean; 'vm.exports.ScriptTreeGenerator.prototype.descendInput': boolean; 'vm.ccExtensionManager.getExtensionLoadOrder': boolean; + 'vm.ccExtensionManager.getLoadedExtensions': boolean; 'blocks.Procedures.addCreateButton_': boolean; 'blocks.getMainWorkspace().toolboxCategoryCallbacks_.PROCEDURE': boolean; 'blocks.WorkspaceSvg.prototype.registerToolboxCategoryCallback': boolean; @@ -57,6 +58,7 @@ const defaultSettings: Settings = { 'vm.runtime._primitives.argument_reporter_boolean': true, 'vm.exports.ScriptTreeGenerator.prototype.descendInput': true, 'vm.ccExtensionManager.getExtensionLoadOrder': true, + 'vm.ccExtensionManager.getLoadedExtensions': true, 'blocks.Procedures.addCreateButton_': true, 'blocks.getMainWorkspace().toolboxCategoryCallbacks_.PROCEDURE': true, 'blocks.WorkspaceSvg.prototype.registerToolboxCategoryCallback': true, diff --git a/src/types/ducktypes.d.ts b/src/types/ducktypes.d.ts index 0dd8a66..7f0160d 100644 --- a/src/types/ducktypes.d.ts +++ b/src/types/ducktypes.d.ts @@ -18,6 +18,36 @@ interface DucktypedToolbox { refreshSelection(): void; } +interface DucktypedTarget { + isStage: boolean; + blocks: { + [id: string]: { + opcode: string; + inputs: Record; + mutation?: Record; + } + }; +} + +interface DucktypedMonitor { + id: string; + mode: number; + opcode: string; + params: Record; +} + +interface DucktypedProjectJSON { + targets: DucktypedTarget[]; + monitors: DucktypedMonitor[]; + sideloadMonitors?: DucktypedMonitor[]; + extensions: Record | string[]; + [prop: string]: unknown; +} + +interface CCXSaveData { + projectData: DucktypedProjectJSON +} + interface DucktypedVM { initialized?: boolean; exports?: { @@ -26,8 +56,15 @@ interface DucktypedVM { ScriptTreeGenerator?: DucktypedUnsupportedAPI['ScriptTreeGenerator']; } ccExtensionManager?: { - info: Record; + info: Record; + load: Record; getExtensionLoadOrder(extensions: string[]): unknown; + getLoadedExtensions(optional: boolean): Record; + instance: { + [id: string]: { + beforeProjectSave? (data: CCXSaveData): void; + } + } } _events: { [eventName: string]: ((...args: unknown[]) => unknown) | ((...args: unknown[]) => unknown)[] @@ -58,7 +95,7 @@ interface DucktypedVM { renderer: any; } toJSON(optTargetId?: string): string; - deserializeProject(projectJSON: Record, zip: unknown, extensionCallback?: unknown): Promise; + deserializeProject(projectJSON: DucktypedProjectJSON, zip: unknown, extensionCallback?: unknown): Promise; _loadExtensions?(extensionIDs: Set, extensionURLs: Map): Promise; setLocale(locale: string, messages: Record): Promise; getLocale(): Locales; diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 48a55f4..8c5d779 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -12,6 +12,7 @@ declare var __scratchAddonsRedux: EuRedux | undefined; interface EurekaContext { declaredIds: string[]; + idToURLMapping: Map; vm?: DucktypedVM; redux?: EuRedux; version: string;