Skip to content

Commit

Permalink
[CRT-24] Use composition instead of patching VM
Browse files Browse the repository at this point in the history
  • Loading branch information
Tyratox committed Oct 28, 2024
1 parent 4c5e111 commit b1afd74
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 79 deletions.
37 changes: 27 additions & 10 deletions apps/scratch/src/containers/Gui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ErrorBoundaryHOC from "@scratch-submodule/scratch-gui/src/lib/error-bound
import {
getIsError,
getIsShowingProject,
onFetchedProjectData,
} from "@scratch-submodule/scratch-gui/src/reducers/project-state";
import {
activateTab,
Expand All @@ -25,11 +26,7 @@ import {

import FontLoaderHOC from "@scratch-submodule/scratch-gui/src/lib/font-loader-hoc.jsx";
import LocalizationHOC from "@scratch-submodule/scratch-gui/src/lib/localization-hoc.jsx";
import SBFileUploaderHOC from "@scratch-submodule/scratch-gui/src/lib/sb-file-uploader-hoc.jsx";
import ProjectFetcherHOC from "@scratch-submodule/scratch-gui/src/lib/project-fetcher-hoc.jsx";
import TitledHOC from "@scratch-submodule/scratch-gui/src/lib/titled-hoc.jsx";
import ProjectSaverHOC from "@scratch-submodule/scratch-gui/src/lib/project-saver-hoc.jsx";
import QueryParserHOC from "@scratch-submodule/scratch-gui/src/lib/query-parser-hoc.jsx";
import storage from "@scratch-submodule/scratch-gui/src/lib/storage";
import vmListenerHOC from "@scratch-submodule/scratch-gui/src/lib/vm-listener-hoc.jsx";
import vmManagerHOC from "@scratch-submodule/scratch-gui/src/lib/vm-manager-hoc.jsx";
Expand All @@ -39,6 +36,7 @@ import { setIsScratchDesktop } from "@scratch-submodule/scratch-gui/src/lib/isSc
import { StageSizeMode } from "@scratch-submodule/scratch-gui/src/lib/screen-utils";
import { AppStateHOC } from "@scratch-submodule/scratch-gui/src";
import HashParserHOC from "@scratch-submodule/scratch-gui/src/lib/hash-parser-hoc";
import { loadCrtProject } from "../vm/load-crt-project";

const { RequestMetadata, setMetadata, unsetMetadata } = storage.scratchFetch;

Expand All @@ -65,6 +63,7 @@ interface Props {
isScratchDesktop: boolean;
isShowingProject?: boolean;
isTotallyNormal: boolean;
isCreatingNew: boolean;
loadingStateVisible?: boolean;
onProjectLoaded?: () => void;
onSeeCommunity?: () => void;
Expand All @@ -73,11 +72,12 @@ interface Props {
onVmInit?: (vm: VM) => void;
projectHost?: string;
projectId: string | number;
telemetryModalVisible?: boolean;
isRtl: boolean;
isFullScreen: boolean;
basePath: string;

onFetchedProjectData: (projectData: unknown, loadingState: unknown) => void;

backpackHost: string | null;
backpackVisible: boolean;
blocksId: string;
Expand Down Expand Up @@ -134,14 +134,32 @@ interface ReduxState {
class GUI extends React.Component<Props> {
componentDidMount() {
setIsScratchDesktop(this.props.isScratchDesktop);

if (this.props.onStorageInit) {
this.props.onStorageInit(storage);
}

if (this.props.onVmInit) {
this.props.onVmInit(this.props.vm);
}

setProjectIdMetadata(this.props.projectId);

storage
.load(storage.AssetType.Project, "0", storage.DataFormat.JSON)
.then((projectAsset) => {
if (projectAsset) {
loadCrtProject(
this.props.vm,
(projectAsset as unknown as { data: object }).data,
);
} else {
throw new Error("Could not load default project");
}
})
.catch((err) => {
console.error(err);
});
}

componentDidUpdate(prevProps: Props) {
Expand Down Expand Up @@ -182,6 +200,8 @@ class GUI extends React.Component<Props> {
onVmInit,
projectHost,
projectId,
isCreatingNew,
onFetchedProjectData,
/* eslint-enable @typescript-eslint/no-unused-vars */
children,
fetchingProject,
Expand Down Expand Up @@ -227,7 +247,6 @@ const mapStateToProps = (state: ReduxState) => {
state.scratchGui.targets.stage &&
state.scratchGui.targets.stage.id ===
state.scratchGui.targets.editingTarget,
telemetryModalVisible: state.scratchGui.modals.telemetryModal,
tipsLibraryVisible: state.scratchGui.modals.tipsLibrary,
vm: state.scratchGui.vm,
};
Expand All @@ -240,6 +259,8 @@ const mapDispatchToProps = (dispatch: Dispatch<Action>) => ({
onActivateSoundsTab: () => dispatch(activateTab(SOUNDS_TAB_INDEX)),
onRequestCloseBackdropLibrary: () => dispatch(closeBackdropLibrary()),
onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()),
onFetchedProjectData: (projectData: unknown, loadingState: unknown) =>
dispatch(onFetchedProjectData(projectData, loadingState) as Action),
});

const ConnectedGUI = injectIntl(
Expand All @@ -253,13 +274,9 @@ const WrappedGui = compose(
LocalizationHOC,
ErrorBoundaryHOC("Top Level App"),
FontLoaderHOC,
QueryParserHOC,
ProjectFetcherHOC,
TitledHOC,
ProjectSaverHOC,
vmListenerHOC,
vmManagerHOC,
SBFileUploaderHOC,
systemPreferencesHOC,
)(ConnectedGUI);

Expand Down
7 changes: 4 additions & 3 deletions apps/scratch/src/pages/Solve.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
AppIFrameResponse,
} from "../../../../frontend/src/types/app-iframe-message";
import { patchScratchVm } from "../vm";
import { saveCrtProject } from "../vm/save-crt-project";
import { loadCrtProject } from "../vm/load-crt-project";

const respondToMessageEvent = (
event: MessageEvent,
Expand Down Expand Up @@ -79,7 +81,7 @@ export const Solve = () => {
break;
case "getTask":
if (vm) {
vm.saveProjectSb3().then((content) => {
saveCrtProject(vm).then((content) => {
respondToMessageEvent(event, {
procedure: "getTask",
result: content,
Expand All @@ -91,7 +93,7 @@ export const Solve = () => {
if (vm) {
const sb3Project = await message.arguments.arrayBuffer();

vm.loadProject(sb3Project).then(() => {
loadCrtProject(vm, sb3Project).then(() => {
respondToMessageEvent(event, {
procedure: "loadTask",
});
Expand Down Expand Up @@ -122,7 +124,6 @@ export const Solve = () => {

return (
<Gui
canEditTask={true}
onStorageInit={(storageInstance: {
addOfficialScratchWebStores: () => void;
}) => storageInstance.addOfficialScratchWebStores()}
Expand Down
5 changes: 5 additions & 0 deletions apps/scratch/src/vm/default-crt-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ScratchCrtConfig } from "scratch-vm";

export const defaultCrtConfig: ScratchCrtConfig = {
allowedBlocks: {},
};
67 changes: 1 addition & 66 deletions apps/scratch/src/vm/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import VM, { ScratchCrtConfig } from "scratch-vm";
import JSZip from "jszip";
import VM from "scratch-vm";
import { ExtensionId } from "../extensions";
import ExampleExtension from "../extensions/example";

const defaultCrtConfig: ScratchCrtConfig = {
allowedBlocks: {},
};

export const patchScratchVm = (vm: VM): void => {
// patch extension manager load function with a custom implementation
vm.extensionManager.loadExtensionURL = async (
Expand All @@ -31,66 +26,6 @@ export const patchScratchVm = (vm: VM): void => {
return Promise.resolve(0);
};

// modify the loadProject function to parse our additional project data
const originalLoadProject = vm.loadProject.bind(vm);

/**
* Load a Scratch project from a .sb, .sb2, .sb3 or json string.
* @param input A json string, object, or ArrayBuffer representing the project to load.
* @return Promise that resolves after targets are installed.
*/
vm.loadProject = async (
input: ArrayBufferView | ArrayBuffer | string | object,
): Promise<void> => {
// overwrite any existing config
vm.crtConfig = {
...defaultCrtConfig,
};

if (input instanceof ArrayBuffer) {
const zip = new JSZip();
await zip.loadAsync(input);

const configFile = zip.file("crt.json");

if (configFile) {
// if the project contains a crt.json file, we parse it
const config: ScratchCrtConfig = await configFile
.async("text")
.then((text) => JSON.parse(text));

vm.crtConfig = config;
}
}

return originalLoadProject(input);
};

// modify the saveProjectSb3 function to include our additional project data
const originalSaveProjectSb3 = vm.saveProjectSb3.bind(vm);

/**
* @returns Project in a Scratch 3.0 JSON representation.
*/
vm.saveProjectSb3 = async (): Promise<Blob> => {
const blob = await originalSaveProjectSb3();

const zip = new JSZip();
await zip.loadAsync(blob);

zip.file("crt.json", JSON.stringify(vm.crtConfig));

return zip.generateAsync({
// options consistent with https://github.com/scratchfoundation/scratch-vm/blob/766c767c7a2f3da432480ade515de0a9f98804ba/src/virtual-machine.js#L400C19-L407C12
type: "blob",
mimeType: "application/x.scratch.sb3",
compression: "DEFLATE",
compressionOptions: {
level: 6,
},
});
};

// add custom callback to when the greenFlag event is triggered
vm.runtime.on("PROJECT_START", () => {
console.log("Green flag clicked");
Expand Down
36 changes: 36 additions & 0 deletions apps/scratch/src/vm/load-crt-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import VM, { ScratchCrtConfig } from "scratch-vm";
import { defaultCrtConfig } from "./default-crt-config";
import JSZip from "jszip";

/**
* Load a Scratch project from a .sb, .sb2, .sb3 or json string.
* @param input A json string, object, or ArrayBuffer representing the project to load.
* @return Promise that resolves after targets are installed.
*/
export const loadCrtProject = async (
vm: VM,
input: ArrayBufferView | ArrayBuffer | string | object,
): Promise<void> => {
// overwrite any existing config
vm.crtConfig = {
...defaultCrtConfig,
};

if (input instanceof ArrayBuffer) {
const zip = new JSZip();
await zip.loadAsync(input);

const configFile = zip.file("crt.json");

if (configFile) {
// if the project contains a crt.json file, we parse it
const config: ScratchCrtConfig = await configFile
.async("text")
.then((text) => JSON.parse(text));

vm.crtConfig = config;
}
}

return vm.loadProject(input);
};
24 changes: 24 additions & 0 deletions apps/scratch/src/vm/save-crt-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import VM from "scratch-vm";
import JSZip from "jszip";

/**
* @returns Project in a Scratch 3.0 JSON representation.
*/
export const saveCrtProject = async (vm: VM): Promise<Blob> => {
const blob = await vm.saveProjectSb3();

const zip = new JSZip();
await zip.loadAsync(blob);

zip.file("crt.json", JSON.stringify(vm.crtConfig));

return zip.generateAsync({
// options consistent with https://github.com/scratchfoundation/scratch-vm/blob/766c767c7a2f3da432480ade515de0a9f98804ba/src/virtual-machine.js#L400C19-L407C12
type: "blob",
mimeType: "application/x.scratch.sb3",
compression: "DEFLATE",
compressionOptions: {
level: 6,
},
});
};

0 comments on commit b1afd74

Please sign in to comment.