Skip to content

Commit

Permalink
🐛 fix: codingclip sideload issues
Browse files Browse the repository at this point in the history
Signed-off-by: SimonShiki <[email protected]>
  • Loading branch information
SimonShiki committed Nov 26, 2024
1 parent 0278ebe commit ebedac1
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 30 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||||||
Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/main/ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ import { version } from '../../package.json';

export const eureka: EurekaContext = {
declaredIds: [],
idToURLMapping: new Map<string, string>(),
version
};
11 changes: 9 additions & 2 deletions src/main/dashboard/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand All @@ -496,7 +503,7 @@ document.addEventListener('DOMContentLoaded', () => {
<Dashboard />
</div>
), document.body);
});
}

eureka.openDashboard = (status: Exclude<DashboardStatus, DashboardStatus.NONE> = DashboardStatus.LOADER) => {
setModalStatus(status);
Expand Down
3 changes: 3 additions & 0 deletions src/main/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
119 changes: 96 additions & 23 deletions src/main/patches/applier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import * as l10n from '../util/l10n';
import formatMessage from 'format-message';

const settings = settingsAgent.getSettings();
const idToURLMapping = new Map<string, string>();

/**
* Utility function to determine if a value is a Promise.
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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<string, string> = {};
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
Expand Down
2 changes: 2 additions & 0 deletions src/main/util/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
41 changes: 39 additions & 2 deletions src/types/ducktypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,36 @@ interface DucktypedToolbox {
refreshSelection(): void;
}

interface DucktypedTarget {
isStage: boolean;
blocks: {
[id: string]: {
opcode: string;
inputs: Record<string, any>;
mutation?: Record<string, any>;
}
};
}

interface DucktypedMonitor {
id: string;
mode: number;
opcode: string;
params: Record<string, any>;
}

interface DucktypedProjectJSON {
targets: DucktypedTarget[];
monitors: DucktypedMonitor[];
sideloadMonitors?: DucktypedMonitor[];
extensions: Record<string, string> | string[];
[prop: string]: unknown;
}

interface CCXSaveData {
projectData: DucktypedProjectJSON
}

interface DucktypedVM {
initialized?: boolean;
exports?: {
Expand All @@ -26,8 +56,15 @@ interface DucktypedVM {
ScriptTreeGenerator?: DucktypedUnsupportedAPI['ScriptTreeGenerator'];
}
ccExtensionManager?: {
info: Record<string, {api: number}>;
info: Record<string, {api: number, optional?: boolean}>;
load: Record<string, { api: number, optional?: boolean }>;
getExtensionLoadOrder(extensions: string[]): unknown;
getLoadedExtensions(optional: boolean): Record<string, string>;
instance: {
[id: string]: {
beforeProjectSave? (data: CCXSaveData): void;
}
}
}
_events: {
[eventName: string]: ((...args: unknown[]) => unknown) | ((...args: unknown[]) => unknown)[]
Expand Down Expand Up @@ -58,7 +95,7 @@ interface DucktypedVM {
renderer: any;
}
toJSON(optTargetId?: string): string;
deserializeProject(projectJSON: Record<string, unknown>, zip: unknown, extensionCallback?: unknown): Promise<void>;
deserializeProject(projectJSON: DucktypedProjectJSON, zip: unknown, extensionCallback?: unknown): Promise<void>;
_loadExtensions?(extensionIDs: Set<string>, extensionURLs: Map<string, string>): Promise<void[]>;
setLocale(locale: string, messages: Record<string, string>): Promise<void>;
getLocale(): Locales;
Expand Down
1 change: 1 addition & 0 deletions src/types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ declare var __scratchAddonsRedux: EuRedux | undefined;

interface EurekaContext {
declaredIds: string[];
idToURLMapping: Map<string, string>;
vm?: DucktypedVM;
redux?: EuRedux;
version: string;
Expand Down

0 comments on commit ebedac1

Please sign in to comment.