diff --git a/scratch-vm/index.d.ts b/scratch-vm/index.d.ts index dec4e96..a61516e 100644 --- a/scratch-vm/index.d.ts +++ b/scratch-vm/index.d.ts @@ -1,4 +1,6 @@ declare module 'scratch-vm' { + import {RuntimeCustomEvents, VirtualMachineCustomEvents} from 'scratch-vm/events'; + type Listener = (...args: Args) => void; interface TypedEmitter { @@ -11,14 +13,60 @@ declare module 'scratch-vm' { } namespace VirtualMachine { - type VariableValue = string | number | boolean | (string | number | boolean)[]; + type VariableType = '' | 'list' | 'broadcast_msg'; + + type ScalarValue = string | number | boolean; + + type VariableValue = ScalarValue | ScalarValue[]; - interface Variable { + interface Variable extends VariableInternals { id: string; name: string; - type: '' | 'list' | 'broadcast_msg'; + type: VariableType; value: VariableValue; isCloud: boolean; + toXML(isLocal?: boolean): string; + } + + interface Comment { + id: string; + text: string; + x: number; + y: number; + width: number; + height: number; + minimized: boolean; + blockId: string | null; + toXML(): string; + } + + interface Frame { + id: string; + title: string; + x: number; + y: number; + width: number; + height: number; + collapsed: boolean; + blocks: string[]; + toXML(): string; + } + + interface StorageAssetType { + contentType: string; + name: string; + runtimeFormat: string; + immutable: boolean; + } + + interface StorageAsset { + assetType: StorageAssetType; + assetId: string; + dataFormat: string; + data: Uint8Array; + clean: boolean; + decodeText(): string; + encodeDataURI(contentType?: string): string; } interface Costume { @@ -29,8 +77,11 @@ declare module 'scratch-vm' { bitmapResolution: number; rotationCenterX: number; rotationCenterY: number; - skinId?: number; + asset?: StorageAsset; + skinId?: number | null; size?: [number, number]; + textLayerAsset?: StorageAsset; + broken?: Partial; } interface Sound { @@ -41,7 +92,22 @@ declare module 'scratch-vm' { format?: string; rate: number; sampleCount: number; + asset?: StorageAsset; soundId?: string; + broken?: Partial; + } + + interface BlockInput { + name: string; + block: string | null; + shadow: string | null; + } + + interface BlockField { + name: string; + value: unknown; + id?: string; + variableType?: VariableType; } interface Block { @@ -49,31 +115,48 @@ declare module 'scratch-vm' { opcode: string; next: string | null; parent: string | null; - inputs: Record; - fields: Record; + inputs: Record; + fields: Record; shadow: boolean; topLevel: boolean; x?: number; y?: number; mutation?: Record | null; comment?: string; + isMonitored?: boolean; } - interface Blocks { - _blocks: Record; + interface BlockChange { + element: 'field' | 'mutation' | 'checkbox'; + id: string; + name?: string; + value: unknown; + } + + interface Blocks extends BlocksInternals { + runtime: Runtime; + forceNoGlow: boolean; + revision: number; getBlock(blockId: string): Block | undefined; getScripts(): string[]; - getOpcode(block: Block): string | null; - createBlock(block: Partial & {id: string; opcode: string}): void; - deleteBlock(blockId: string): void; - toXML(comments?: Record): string; + getNextBlock(id: string): string | null; + getTopLevelScript(id: string): string | null; + createBlock(block: Block): void; + changeBlock(args: BlockChange): void; + duplicate(): Blocks; + resetCache(): void; + toXML(comments?: Record): string; + blockToXML(blockId: string, comments?: Record): string | undefined; } - interface Sprite { + interface Sprite extends SpriteInternals { + runtime: Runtime; + blocks: Blocks; name: string; costumes: Costume[]; sounds: Sound[]; clones: RenderedTarget[]; + soundBank: unknown; } interface Target { @@ -81,46 +164,218 @@ declare module 'scratch-vm' { runtime: Runtime; blocks: Blocks; variables: Record; - comments: Record; - isStage: boolean; + comments: Record; + frames: Record; + extensionStorage: Record; + declRevision: number; getName(): string; lookupVariableById(id: string): Variable | undefined; - lookupVariableByNameAndType(name: string, type?: Variable['type'], skipStage?: boolean): Variable | null; - lookupOrCreateList(id: string, name: string): Variable; - getAllVariableNamesInScopeByType(type: Variable['type'], skipStage?: boolean): string[]; + lookupVariableByNameAndType(name: string, type?: VariableType, skipStage?: boolean): Variable | null; + createVariable(id: string, name: string, type: VariableType, isCloud?: boolean): void; + renameVariable(id: string, newName: string): void; + deleteVariable(id: string): void; + getAllVariableNamesInScopeByType(type?: VariableType, skipStage?: boolean): string[]; + createComment(id: string, blockId: string | null, text: string, x: number, y: number, + width: number, height: number, minimized?: boolean): void; + deleteComment(id: string, blockId?: string | null): void; + } + + type RotationStyle = 'all around' | 'left-right' | "don't rotate"; + + interface SpriteInfo { + x?: number; + y?: number; + force?: boolean; + direction?: number; + draggable?: boolean; + rotationStyle?: RotationStyle; + visible?: boolean; + size?: number; + } + + interface SpriteInfoChange { + x?: number; + y?: number; + direction?: number; + size?: number; + visible?: boolean; + rotationStyle?: RotationStyle; + } + + interface Bounds { + left: number; + right: number; + top: number; + bottom: number; + } + + interface TargetJSON { + id: string; + name: string; + isStage: boolean; + x: number; + y: number; + size: number; + direction: number; + draggable: boolean; + currentCostume: number; + costume: Costume | undefined; + costumeCount: number; + visible: boolean; + rotationStyle: RotationStyle; + comments: Record; + frames: Record; + blocks: Record; + variables: Record; + costumes: Costume[]; + sounds: Sound[]; + textToSpeechLanguage: string | null; + tempo: number; + volume: number; + videoTransparency: number; + videoState: string; } interface RenderedTarget extends Target { sprite: Sprite; + renderer: Renderer | null | undefined; + drawableID: number | null; + dragging: boolean; + isOriginal: boolean; + isStage: boolean; x: number; y: number; direction: number; - size: number; - visible: boolean; draggable: boolean; - rotationStyle: string; + visible: boolean; + size: number; currentCostume: number; + rotationStyle: RotationStyle; volume: number; - isOriginal: boolean; - drawableID: number; + tempo: number; + videoTransparency: number; + videoState: string; + textToSpeechLanguage: string | null; + isSprite(): boolean; + setVisible(visible: boolean): void; + setSize(size: number): void; + setCostume(index: number): void; getCostumes(): Costume[]; + addCostume(costumeObject: Costume, index?: number): void; + renameCostume(costumeIndex: number, newName: string): void | Promise; + deleteCostume(index: number): Costume | null; + reorderCostume(costumeIndex: number, newIndex: number): boolean; getSounds(): Sound[]; - getCurrentCostume(): Costume; - setXY(x: number, y: number, force?: boolean): void; - toJSON(): Record; + addSound(soundObject: Sound, index?: number): void; + renameSound(soundIndex: number, newName: string): void | Promise; + deleteSound(index: number): Sound | null; + reorderSound(soundIndex: number, newIndex: number): boolean; + getBounds(): Bounds | null; + goToFront(): void; + makeClone(): RenderedTarget | null; + duplicate(): Promise; + postSpriteInfo(data: SpriteInfo): void; + startDrag(): void; + stopDrag(): void; + toJSON(): TargetJSON; + dispose(): void; + } + + interface StackFrame { + isLoop: boolean; + warpMode: boolean; + justReported: unknown; + reporting: string; + reported: Record | null; + waitingReporter: unknown; + params: Record | null; + executionContext: Record | null; + op: unknown; } interface Thread { topBlock: string; - target: Target; - status: number; stack: string[]; + stackFrames: StackFrame[]; + status: 0 | 1 | 2 | 3 | 4; + isKilled: boolean; + target: RenderedTarget; + blockContainer: Blocks; + isCompiled: boolean; + warpTimer: unknown; + timer: unknown; + compatibilityStackFrame: StackFrame | null; + stackClick: boolean; + updateMonitor: boolean; + getId(): string; + peekStack(): string | undefined; + peekStackFrame(): StackFrame | undefined; + popStack(): string | undefined; + goToNextBlock(): void; + } + + interface MonitorDelta { + id?: string | null; + spriteName?: string | null; + targetId?: string | null; + opcode?: string | null; + value?: unknown; + params?: unknown; + mode?: string | null; + sliderMin?: number | null; + sliderMax?: number | null; + isDiscrete?: boolean | null; + x?: number | null; + y?: number | null; + width?: number | null; + height?: number | null; + visible?: boolean | null; + } + + type ExternalMonitorDelta = MonitorDelta | {toJS(): MonitorDelta} | Map; + + interface MonitorRecord extends MonitorRecordFields { + get(property: K): MonitorRecordFields[K]; + } + + interface MonitorRecordFields { + id: string | null; + spriteName: string | null; + targetId: string | null; + opcode: string | null; + value: unknown; + params: unknown; + mode: string; + sliderMin: number; + sliderMax: number; + isDiscrete: boolean; + x: number | null; + y: number | null; + width: number; + height: number; + visible: boolean; + } + + interface MonitorState { + map: Map; + dirty: boolean; + readonly size: number; + get(id: string): MonitorRecord | undefined; + has(id: string): boolean; + set(id: string, delta: MonitorDelta): void; + delete(id: string): void; + filter(callback: (record: MonitorRecord) => boolean): void; + empty(): boolean; + values(): MonitorRecord[]; + valueSeq(): MonitorRecord[]; + shallowClone(): MonitorState; } interface RuntimeOptions { maxClones: number; miscLimits: boolean; fencing: boolean; + caseSensitiveLists: boolean; } interface CompilerOptions { @@ -128,20 +383,148 @@ declare module 'scratch-vm' { warpTimer: boolean; } + interface ExtensionArgumentInfo { + type?: string; + menu?: string; + defaultValue?: unknown; + } + + interface ExtensionBlockInfo { + opcode?: string; + text?: string | string[]; + blockType?: string; + arguments?: Record; + branchCount?: number; + isTerminal?: boolean; + hideFromPalette?: boolean; + func?: unknown; + } + + interface CategoryBlock { + info: ExtensionBlockInfo; + json?: Record; + xml: string; + } + + interface CategoryInfo { + id: string; + name: string; + showStatusButton?: boolean; + blockIconURI?: string; + menuIconURI?: string; + color1: string; + color2: string; + color3: string; + blocks: CategoryBlock[]; + customFieldTypes: Record; + menus: unknown[]; + menuInfo?: Record; + } + + interface CategoryXML { + id: string; + xml: string; + } + + interface AddonBlockOptions { + procedureCode: string; + callback: (args: Record, util: {thread: Thread}) => unknown; + arguments?: string[]; + hidden?: boolean; + return?: 1 | 2; + displayName?: string; + } + + interface AddonBlock extends AddonBlockOptions { + namesIdsDefaults: [string[], string[], string[]]; + } + + type TestEntitlements = Record; + interface GlowData { id: string; } - interface TargetsUpdate { - readonly targetList: Record[]; - editingTarget: string | null; + interface ExtensionField { + name: string; + implementation: unknown; } - interface WorkspaceUpdate { - xml: string; + interface ExtensionReloadEntry { + id: string; + removed: string[]; + added: string[]; + changed: string[]; } - interface RuntimeEvents { + interface ExtensionReloadResult { + ids: string[]; + removed: string[]; + added: string[]; + changed: string[]; + } + + type ExtensionMode = 'worker' | 'iframe' | 'unsandboxed'; + + interface ExtensionReloadDiff extends ExtensionReloadResult { + mode: ExtensionMode; + details: unknown; + } + + interface ExtensionLoadError { + message: string; + line: number | null; + column: number | null; + } + + interface ExtensionEntry { + id: string; + url: string | null; + kind: 'builtin' | 'embedded' | 'url'; + status: 'loaded' | 'denied' | 'failed'; + mode: ExtensionMode | null; + name: string; + error: ExtensionLoadError | null; + } + + interface ExtensionConsoleMessage { + extensionId: string; + level: 'log' | 'info' | 'warn' | 'error' | 'debug'; + message: string; + line: number | null; + column: number | null; + } + + interface ExtensionErrorReport { + extensionId: string; + kind: 'load' | 'getInfo' | 'menu' | 'button' | 'block' | 'hat'; + opcode: string | null; + blockId: string | null; + targetId: string | null; + message: string; + stack: string | null; + line: number | null; + column: number | null; + count: number; + } + + interface AssetProgressDetail { + phase: 'download' | 'prepare'; + completed: number; + total: number; + overallCompleted: number; + overallTotal: number; + } + + type LoadStage = 'unzipping' | 'parsing' | 'checking' | 'building' | 'installing'; + + interface LoadProgress { + stage: LoadStage; + loaded: number | undefined; + total: number | undefined; + } + + interface RuntimeEvents extends RuntimeCustomEvents { SCRIPT_GLOW_ON: [GlowData]; SCRIPT_GLOW_OFF: [GlowData]; BLOCK_GLOW_ON: [GlowData]; @@ -157,51 +540,319 @@ declare module 'scratch-vm' { COMPILE_ERROR: [Target, unknown]; BEFORE_EXECUTE: []; AFTER_EXECUTE: []; - ASSET_PROGRESS: [number, number]; + ASSET_PROGRESS: [number, number, AssetProgressDetail | undefined]; PROJECT_START: []; PROJECT_RUN_START: []; PROJECT_RUN_STOP: []; PROJECT_STOP_ALL: []; STOP_FOR_TARGET: [Target, Thread | undefined]; - VISUAL_REPORT: [{id: string; value: unknown}]; + VISUAL_REPORT: [{id: string; value: string}]; PROJECT_LOADED: []; PROJECT_CHANGED: []; TOOLBOX_EXTENSIONS_NEED_UPDATE: []; TARGETS_UPDATE: [boolean]; - SPRITE_INFO_CHANGED: []; - MONITORS_UPDATE: [unknown]; + SPRITE_INFO_CHANGED: [RenderedTarget, SpriteInfoChange]; + MONITORS_UPDATE: [MonitorState]; BLOCK_DRAG_UPDATE: [boolean]; BLOCK_DRAG_END: [Block[], string]; - EXTENSION_ADDED: [unknown]; - EXTENSION_REMOVED: [unknown]; - EXTENSIONS_REORDERED: [unknown]; - EXTENSION_FIELD_ADDED: [string, unknown]; + EXTENSION_ADDED: [CategoryInfo]; + EXTENSION_REMOVED: [{id: string}]; + EXTENSIONS_REORDERED: [{ids: string[]}]; + EXTENSION_FIELD_ADDED: [ExtensionField]; PERIPHERAL_LIST_UPDATE: [Record]; USER_PICKED_PERIPHERAL: [Record]; PERIPHERAL_CONNECTED: []; PERIPHERAL_DISCONNECTED: []; - PERIPHERAL_REQUEST_ERROR: []; - PERIPHERAL_CONNECTION_LOST_ERROR: [{extensionId: string}]; + PERIPHERAL_REQUEST_ERROR: [{message: string; extensionId: string}]; + PERIPHERAL_CONNECTION_LOST_ERROR: [{message: string; extensionId: string}]; PERIPHERAL_SCAN_TIMEOUT: []; MIC_LISTENING: [boolean]; - BLOCKSINFO_UPDATE: [unknown]; + BLOCKSINFO_UPDATE: [CategoryInfo]; RUNTIME_STARTED: []; RUNTIME_STOPPED: []; RUNTIME_DISPOSED: []; BLOCKS_NEED_UPDATE: []; - PLATFORM_MISMATCH: [unknown, (accepted: boolean) => void]; + PLATFORM_MISMATCH: [unknown, () => void]; + THREAD_ERROR: [{thread: Thread; error: unknown}]; + THREAD_STARTED: [Thread]; + BREAKPOINT_HIT: [Thread, string]; + PRODUCTS_CHANGED: [unknown[]]; + ITEMS_CHANGED: [unknown[]]; + ENTITLEMENTS_CHANGED: [TestEntitlements]; + EXTENSION_CONSOLE: [ExtensionConsoleMessage]; + EXTENSION_ERROR: [ExtensionErrorReport]; + EXTENSION_RELOADED: [ExtensionReloadEntry]; + TARGET_SCRIPTS_REPLACED: [string]; + KEY_PRESSED: [string]; + SAY: [RenderedTarget, 'say' | 'think', unknown]; + QUESTION: [string | null]; + ANSWER: [string]; + PLAY_NOTE: [number, string]; + targetWasCreated: [RenderedTarget, RenderedTarget | undefined]; + targetWasRemoved: [RenderedTarget]; } - interface VirtualMachineEvents extends RuntimeEvents { - workspaceUpdate: [WorkspaceUpdate]; + type ForwardedRuntimeEvent = + 'SCRIPT_GLOW_ON' | 'SCRIPT_GLOW_OFF' | 'BLOCK_GLOW_ON' | 'BLOCK_GLOW_OFF' | + 'PROJECT_START' | 'PROJECT_RUN_START' | 'PROJECT_RUN_STOP' | 'PROJECT_CHANGED' | + 'VISUAL_REPORT' | 'MONITORS_UPDATE' | 'BLOCK_DRAG_UPDATE' | 'BLOCK_DRAG_END' | + 'EXTENSION_ADDED' | 'EXTENSION_REMOVED' | 'EXTENSIONS_REORDERED' | 'BLOCKSINFO_UPDATE' | + 'PERIPHERAL_LIST_UPDATE' | 'USER_PICKED_PERIPHERAL' | 'PERIPHERAL_CONNECTED' | + 'PERIPHERAL_DISCONNECTED' | 'PERIPHERAL_CONNECTION_LOST_ERROR' | 'PERIPHERAL_SCAN_TIMEOUT' | + 'MIC_LISTENING' | 'RUNTIME_STARTED' | 'RUNTIME_STOPPED' | 'HAS_CLOUD_DATA_UPDATE' | + 'RUNTIME_OPTIONS_CHANGED' | 'COMPILER_OPTIONS_CHANGED' | 'FRAMERATE_CHANGED' | + 'INTERPOLATION_CHANGED' | 'STAGE_SIZE_CHANGED' | 'COMPILE_ERROR' | + 'TURBO_MODE_ON' | 'TURBO_MODE_OFF'; + + interface TargetsUpdate { + readonly targetList: TargetJSON[]; + editingTarget: string | null; + } + + interface WorkspaceUpdate { + readonly xml: string; + headerXml: string; + blocks: { + blocks: Record; + scripts: string[]; + comments: Record; + }; + } + + interface VirtualMachineEvents extends Pick, VirtualMachineCustomEvents { + ASSET_PROGRESS: [number, number]; + EXTENSION_FIELD_ADDED: [ExtensionField, undefined]; + PERIPHERAL_REQUEST_ERROR: []; + LOAD_PROGRESS: [LoadProgress]; + EXTENSION_LOAD_ERROR: [{extensionID: string; url: string; error: unknown}]; + LOCALE_CHANGED: [string]; + LEGACY_EXTENSION_API: [string]; + CREATE_UNSANDBOXED_EXTENSION_API: [unknown]; + EDIT_COMMAND_ERROR: [unknown]; targetsUpdate: [TargetsUpdate]; + workspaceUpdate: [WorkspaceUpdate]; playgroundData: [{blocks: Blocks; threads: string}]; } - interface Runtime extends TypedEmitter { + interface DrawableExtraction { + imageData: ImageData; + x: number; + y: number; + width: number; + height: number; + } + + interface ColorExtraction { + data: Uint8Array; + width: number; + height: number; + color: {r: number; g: number; b: number; a: number}; + } + + interface RendererEvents { + UseHighQualityRenderChanged: [boolean]; + } + + interface Renderer extends TypedEmitter { + readonly canvas: HTMLCanvasElement; + readonly gl: WebGLRenderingContext; + dirty: boolean; + useRealLayerIndexes: boolean; + overlayContainer: HTMLElement; + customFonts: Record; + draw(): void; + requestSnapshot(callback: (dataURI: string) => void): void; + setUseHighQualityRender(enabled: boolean): void; + resize(pixelsWide: number, pixelsTall: number): void; + getNativeSize(): [number, number]; + getBounds(drawableID: number): Bounds; + pick(centerX: number, centerY: number, touchWidth?: number, touchHeight?: number, + candidateIDs?: number[]): number; + extractDrawableScreenSpace(drawableID: number): DrawableExtraction; + extractColor(x: number, y: number, radius: number): ColorExtraction; + } + + interface SecurityManager { + getSandboxMode(extensionURL: string): ExtensionMode | Promise; + canLoadExtensionFromProject(extensionURL: string, options?: {wholeProject: boolean}): + boolean | Promise; + canCreateUnsandboxedExtension(): boolean | Promise; + canRunUnsandboxedFromFile(fileName: string, source: string, extensionId: string): + boolean | Promise; + rewriteExtensionURL(extensionURL: string): string | Promise; + canFetch(resourceURL: string): boolean | Promise; + canOpenWindow(websiteURL: string): boolean | Promise; + canRedirect(websiteURL: string): boolean | Promise; + canRecordAudio(): boolean | Promise; + canRecordVideo(): boolean | Promise; + canReadClipboard(): boolean | Promise; + canNotify(): boolean | Promise; + canGeolocate(): boolean | Promise; + canEmbed(documentURL: string): boolean | Promise; + canDownload(resourceURL: string, name: string): boolean | Promise; + } + + interface ReloadExtensionOptions { + keepURL?: boolean; + confirm?: ((diff: ExtensionReloadDiff) => boolean | Promise) | null; + mode?: ExtensionMode | null; + } + + interface ExtensionManager extends ExtensionManagerInternals { + securityManager: SecurityManager; + isExtensionLoaded(extensionID: string): boolean; + isExtensionUnloaded(extensionId: string): boolean; + isExtensionURLLoaded(url: string): boolean; + isBuiltinExtension(extensionId: string): boolean; + isCoreExtension(extensionId: string): boolean; + loadExtensionIdSync(extensionId: string): void; + loadExtensionURL(extensionURL: string, extensionId?: string): Promise; + setExtensionOrder(orderedExtensionIds: Iterable): Promise; + getExtensionURLs(): Record; + getExtensionEntries(): ExtensionEntry[]; + getExtensionMode(id: string): ExtensionMode | null; + getExtensionSource(id: string, options?: {fetch?: boolean; fresh?: boolean}): Promise; + requestUnsandboxedAllowance(id: string, source: string, fileName: string): Promise; + reloadExtension(id: string, source: string, options?: ReloadExtensionOptions): + Promise; + createExtension(source: string, options?: {unsandboxed?: boolean}): Promise; + convertToEmbedded(id: string): Promise; + } + + interface EditCommand { + method: string; + args: unknown[]; + targetId: string | null; + itemId?: string; + beforeId?: string | null; + beforeTargetId?: string | null; + } + + interface EditTargetState { + id: string; + name: string; + isStage: boolean; + props: Record; + blocks?: Record; + variables?: Record; + comments?: Record; + frames?: Record; + costumes: Record[]; + sounds: Record[]; + } + + interface EditingCommands { + handler: ((command: EditCommand) => unknown) | null; + canLoadExtension: (url: string) => boolean | Promise; + loadingSnapshot?: boolean; + extensions: { + loadExtensionURL: ExtensionManager['loadExtensionURL']; + }; + request(method: string, args: unknown[], targetId?: string | null): unknown; + snapshot(includeCode?: boolean, targetId?: string | null): EditTargetState[]; + applyState(target: RenderedTarget, state: Partial, + getAsset: (assetId: string) => unknown, active?: () => boolean): Promise; + } + + interface Font { + system: boolean; + name: string; + family: string; + data: Uint8Array | null; + format: string | null; + } + + interface InternalFont { + system: boolean; + family: string; + fallback: string; + asset?: StorageAsset; + } + + interface FontManager extends TypedEmitter<{change: []}>, FontManagerInternals { + runtime: Runtime; + getFonts(): Font[]; + restrictFont(family: string): void; + getUnusedSystemFont(family: string): string; + getUnusedCustomFont(family: string): string; + addSystemFont(family: string, fallback: string): void; + addCustomFont(family: string, fallback: string, asset: StorageAsset): void; + deleteFont(index: number): void; + } + + interface CustomAsset { + name: string; + asset: StorageAsset; + } + + interface AssetManager extends TypedEmitter<{change: []}> { + runtime: Runtime; + assets: CustomAsset[]; + getAsset(name: string): CustomAsset | null; + renameAsset(index: number, newName: string): void; + deleteAsset(index: number): void; + getObjectURL(name: string): string; + } + + interface Sequencer { + activeThread: Thread | null; + stepThreads(): Thread[]; + stepThread(thread: Thread): void; + } + + interface FrameLoop { + framerate: number; + running: boolean; + } + + interface Clock extends ClockInternals { + pause(): void; + resume(): void; + } + + interface Cloud { + requestUpdateVariable(name: string, value: string | number): void; + } + + interface Mouse extends MouseInternals { + usesRightClickDown: boolean; + } + + interface IoDevices { + clock: Clock; + cloud: Cloud; + keyboard: unknown; + mouse: Mouse; + mouseWheel: unknown; + userData: unknown; + video: unknown; + } + + interface ToggleScriptOptions { + target?: RenderedTarget; + stackClick?: boolean; + } + + interface Runtime extends TypedEmitter, RuntimeInternals { + vm: VirtualMachine; targets: RenderedTarget[]; executableTargets: RenderedTarget[]; threads: Thread[]; + sequencer: Sequencer; + flyoutBlocks: Blocks; + monitorBlocks: Blocks; + frameLoop: FrameLoop; + ioDevices: IoDevices; + extensionManager: ExtensionManager; + fontManager: FontManager; + assetManager: AssetManager; + renderer?: Renderer; + storage?: unknown; + audioEngine?: unknown; + v2BitmapAdapter?: unknown; + extensionStorage: Record; + peripheralExtensions: Record; + cloudOptions: {limit: number}; stageWidth: number; stageHeight: number; currentMSecs: number; @@ -209,25 +860,190 @@ declare module 'scratch-vm' { turboMode: boolean; runtimeOptions: RuntimeOptions; compilerOptions: CompilerOptions; - ioDevices: Record; + totalAssetRequests: number; + finishedAssetRequests: number; + roturHost?: unknown; + mistwarpGameHost?: unknown; + hasCloudData(): boolean; + canAddCloudVariable(): boolean; + getNumberOfCloudVariables(): number; getTargetById(targetId: string): RenderedTarget | undefined; getTargetForStage(): RenderedTarget | undefined; - getSpriteTargetByName(name: string): RenderedTarget | undefined; + getSpriteTargetByName(spriteName: string): RenderedTarget | undefined; getEditingTarget(): RenderedTarget | null; - setEditingTarget(target: RenderedTarget): void; - emitProjectChanged(): void; - startHats(opcode: string, matchFields?: Record, target?: Target): Thread[] | undefined; + setEditingTarget(editingTarget: RenderedTarget): void; + getAllVarNamesOfType(varType: VariableType): string[]; + getIsEdgeActivatedHat(opcode: string): boolean | undefined; + getLabelForOpcode(extendedOpcode: string): {category: 'extension'; label: string} | undefined; + getBlocksXML(target?: RenderedTarget | null): CategoryXML[]; + getBlocksJSON(): (Record | undefined)[]; + getMonitorState(): MonitorState; + requestUpdateMonitor(delta: ExternalMonitorDelta): boolean; + startHats(requestedHatOpcode: string, optMatchFields?: Record | null, + optTarget?: Target): Thread[] | undefined; + toggleScript(topBlockId: string, opts?: ToggleScriptOptions): void; + stopForTarget(target: Target, optThreadException?: Thread): void; greenFlag(): void; stopAll(): void; + clonesAvailable(): boolean; + emitProjectChanged(): void; requestRedraw(): void; + resetProgress(): void; + setRuntimeOptions(runtimeOptions: Partial): void; + setBreakpoints(blockIds: Iterable | null): void; + getAddonBlock(procedureCode: string): AddonBlock | null; + getStoredProjectOptions(): Record | null | undefined; + getCategoryOrder(): string[]; + setCategoryOrder(categoryOrder: string[]): void; + ownsProduct(productId: string, username: string): boolean; + } + + interface RuntimeInternals { + _blockInfo: CategoryInfo[]; + _hats: Record; + _cloneCounter: number; + _monitorState: MonitorState; + _roturHostResolved?: unknown; + _mistwarpGamesHost?: unknown; + _step(): void; + _getMonitorThreadCount(threads: Thread[]): number; + } + + interface VariableInternals { + _monitorUpToDate?: boolean; + } + + interface SpriteInternals { + costumes_: Costume[]; } + + interface BlocksInternals { + _blocks: Record; + _scripts: string[]; + } + + interface FontManagerInternals { + fonts: InternalFont[]; + } + + interface ExtensionManagerInternals { + _loadedExtensions: Map; + } + + interface ClockInternals { + _paused: boolean; + _pausedTime: number | null; + } + + interface MouseInternals { + _scratchX: number; + _scratchY: number; + } + + interface VirtualMachineInternals { + _projectLoadGeneration?: number; + } + + interface LoadProjectOptions { + editSessionActive?: () => boolean; + } + + interface SerializationOptions { + allowOptimization?: boolean; + } + + interface ExtensionUsage { + id: string; + targets: { + targetId: string; + name: string; + isStage: boolean; + blocks: number; + hatScripts: number; + monitors: number; + }[]; + totalBlocks: number; + totalHatScripts: number; + totalMonitors: number; + storageKeys: number; + } + + interface FractchTargetContext { + id: string; + name: string; + isStage: boolean; + changeKey: string; + blocksRevision: number; + declRevision: number; + variables: unknown; + lists: unknown; + broadcasts: unknown; + keptLists: unknown; + blocks: Record; + comments: Record; + primitiveIds: Record>; + obscuredShadows: Record} | null)[]>; + monitors: unknown[]; + costumes: {name: string}[]; + sounds: {name: string}[]; + } + + interface ExtensionSignatureArgument { + name: string; + type: string | undefined; + menu: string | null; + } + + interface ExtensionSignatureBlock { + opcode: string; + blockType: string | undefined; + text: string; + branchCount: number; + isTerminal: boolean; + arguments: ExtensionSignatureArgument[]; + } + + interface ExtensionSignature { + blocks: ExtensionSignatureBlock[]; + menus: Record; + } + + interface FractchContext { + generation: number; + extensionSignatureHash: string; + extensionSignatures: Record; + targets: FractchTargetContext[]; + } + + type RestoreFunction = () => Promise; + + interface ZipOutputs { + base64: string; + string: string; + text: string; + binarystring: string; + array: number[]; + uint8array: Uint8Array; + arraybuffer: ArrayBuffer; + blob: Blob; + nodebuffer: Uint8Array; + } + + type ZipType = keyof ZipOutputs; } - class VirtualMachine implements TypedEmitter { + class VirtualMachine implements TypedEmitter, + VirtualMachine.VirtualMachineInternals { constructor(); runtime: VirtualMachine.Runtime; editingTarget: VirtualMachine.RenderedTarget | null; - renderer: unknown; + readonly renderer: VirtualMachine.Renderer | undefined; + readonly assets: VirtualMachine.StorageAsset[]; + extensionManager: VirtualMachine.ExtensionManager; + securityManager: VirtualMachine.SecurityManager; + editingCommands: VirtualMachine.EditingCommands; + exports: Record; + _projectLoadGeneration?: number; on( event: K, listener: Listener): this; @@ -249,31 +1065,180 @@ declare module 'scratch-vm' { stopAll(): void; clear(): void; setTurboMode(turboModeOn: boolean): void; + setCompatibilityMode(compatibilityModeOn: boolean): void; setFramerate(framerate: number): void; setInterpolation(interpolationEnabled: boolean): void; setRuntimeOptions(runtimeOptions: Partial): void; setCompilerOptions(compilerOptions: Partial): void; setStageSize(width: number, height: number): void; - loadProject(input: ArrayBuffer | ArrayBufferView | string | object, options?: { - editSessionActive?: () => boolean; - }): Promise; - saveProjectSb3(type?: string, options?: Record): Promise; - toJSON(optTargetId?: string, serializationOptions?: Record): string; + convertToPackagedRuntime(): void; + addAddonBlock(options: VirtualMachine.AddonBlockOptions): void; + getAddonBlock(procedureCode: string): VirtualMachine.AddonBlock | null; + storeProjectOptions(extraOptions?: Record | null): void; + getProducts(): unknown[]; + setProducts(products: unknown[]): void; + getItems(): unknown[]; + setItems(items: unknown[]): void; + getTestEntitlements(): VirtualMachine.TestEntitlements; + grantProduct(productId: string, username: string): boolean; + revokeProduct(productId: string, username: string): boolean; + ownsProduct(productId: string, username: string): boolean; + handleExtensionButtonPress(buttonData: string): void; + postIOData(device: string, data: unknown): void; + setVideoProvider(videoProvider: unknown): void; + setCloudProvider(cloudProvider: unknown): void; + scanForPeripheral(extensionId: string): void; + connectPeripheral(extensionId: string, peripheralId: number | string): void; + disconnectPeripheral(extensionId: string): void; + getPeripheralIsConnected(extensionId: string): boolean; + loadProject(input: ArrayBuffer | ArrayBufferView | string | object, + options?: VirtualMachine.LoadProjectOptions): Promise; + saveProjectSb3(type?: T, options?: VirtualMachine.SerializationOptions): + Promise; + saveProjectSb3Stream(type?: VirtualMachine.ZipType, options?: VirtualMachine.SerializationOptions): unknown; + saveProjectSb3DontZip(options?: VirtualMachine.SerializationOptions): Record; + exportSprite(targetId: string, optZipType?: T): + Promise; + toJSON(optTargetId?: string | null, serializationOptions?: VirtualMachine.SerializationOptions): string; + deserializeProject(projectJSON: object, zip: unknown, active?: () => boolean): Promise; + addSprite(input: ArrayBuffer | ArrayBufferView | string | object): Promise; + addCostume(md5ext: string, costumeObject: object, optTargetId?: string | null, + optVersion?: number | null): Promise; + addCostumeFromLibrary(md5ext: string, costumeObject: object): Promise; + duplicateCostume(costumeIndex: number): Promise; + renameCostume(costumeIndex: number, newName: string): void; + deleteCostume(costumeIndex: number): VirtualMachine.RestoreFunction | Promise | null; + addSound(soundObject: object, optTargetId?: string | null): Promise; + duplicateSound(soundIndex: number): Promise; + renameSound(soundIndex: number, newName: string): void; + getSoundBuffer(soundIndex: number): AudioBuffer | null; + updateSoundBuffer(soundIndex: number, newBuffer: AudioBuffer, soundEncoding?: ArrayBuffer | Uint8Array): void | Promise; + deleteSound(soundIndex: number): VirtualMachine.RestoreFunction | Promise | null; + getCostume(costumeIndex: number): string | null; + getExportedCostume(costumeObject: VirtualMachine.Costume): Uint8Array; + getExportedCostumeBase64(costumeObject: VirtualMachine.Costume): string; + updateBitmap(costumeIndex: number, bitmap: ImageData, rotationCenterX: number, rotationCenterY: number, + bitmapResolution: number): Promise | undefined; + updateSvg(costumeIndex: number, svg: string, rotationCenterX: number, rotationCenterY: number): void | Promise; + addBackdrop(md5ext: string, backdropObject: object): Promise; + renameSprite(targetId: string, newName: string): void | Promise; + renameVariable(variableId: string, newName: string): boolean; + renameBroadcast(broadcastId: string, newName: string): boolean; + deleteSprite(targetId: string): VirtualMachine.RestoreFunction; + duplicateSprite(targetId: string): Promise; + attachAudioEngine(audioEngine: unknown): void; + attachRenderer(renderer: VirtualMachine.Renderer): void; + attachV2BitmapAdapter(bitmapAdapter: unknown): void; + attachStorage(storage: unknown): void; + setLocale(locale: string, messages: Record): Promise; + getLocale(): string; + blockListener(e: object): void; + flyoutBlockListener(e: object): void; + monitorBlockListener(e: object): void; + clearFlyoutBlocks(): void; setEditingTarget(targetId: string): void; + exportStandaloneBlocks(blockObjects: VirtualMachine.Block[]): unknown; + shareBlocksToTarget(blocks: unknown, targetId: string, optFromTargetId?: string | null): Promise; + shareCostumeToTarget(costumeIndex: number, targetId: string): Promise; + shareSoundToTarget(soundIndex: number, targetId: string): Promise; + refreshWorkspace(): void; emitTargetsUpdate(triggerProjectChange?: boolean): void; emitWorkspaceUpdate(): void; - postIOData(device: string, data: unknown): void; + getFractchContext(targetId?: string): VirtualMachine.FractchContext; + getCodeStateHash(targetId: string): string | null; + replaceTargetScripts(targetId: string, change: unknown): unknown; + getExtensionUsage(id: string): VirtualMachine.ExtensionUsage; + removeExtensionFromProject(ids: string | string[]): void; getTargetIdForDrawableId(drawableId: number): string | null; - renameSprite(targetId: string, newName: string): void; - deleteSprite(targetId: string): () => Promise; - duplicateSprite(targetId: string): Promise; - renameCostume(costumeIndex: number, newName: string): void; - renameSound(soundIndex: number, newName: string): void; - deleteCostume(costumeIndex: number): () => void; - deleteSound(soundIndex: number): () => void; - setVariableValue(targetId: string, variableId: string, value: VirtualMachine.VariableValue): boolean; + reorderTarget(targetIndex: number, newIndex: number): boolean | Promise; + reorderCostume(targetId: string, costumeIndex: number, newIndex: number): boolean | Promise; + reorderSound(targetId: string, soundIndex: number, newIndex: number): boolean | Promise; + startDrag(targetId: string): void; + stopDrag(targetId: string): void; + postSpriteInfo(data: VirtualMachine.SpriteInfo): void | Promise; + setVariableValue(targetId: string, variableId: string, value: VirtualMachine.VariableValue): + boolean | Promise; getVariableValue(targetId: string, variableId: string): VirtualMachine.VariableValue | null; } export = VirtualMachine; } + +declare module 'scratch-vm/src/extension-support/argument-type' { + const ArgumentType: { + readonly ANGLE: 'angle'; + readonly BOOLEAN: 'Boolean'; + readonly COLOR: 'color'; + readonly NUMBER: 'number'; + readonly STRING: 'string'; + readonly MATRIX: 'matrix'; + readonly NOTE: 'note'; + readonly IMAGE: 'image'; + readonly COSTUME: 'costume'; + readonly SOUND: 'sound'; + }; + export = ArgumentType; +} + +declare module 'scratch-vm/src/extension-support/block-type' { + const BlockType: { + readonly BOOLEAN: 'Boolean'; + readonly BUTTON: 'button'; + readonly LABEL: 'label'; + readonly COMMAND: 'command'; + readonly CONDITIONAL: 'conditional'; + readonly EVENT: 'event'; + readonly HAT: 'hat'; + readonly LOOP: 'loop'; + readonly REPORTER: 'reporter'; + readonly XML: 'xml'; + }; + export = BlockType; +} + +declare module 'scratch-vm/src/serialization/fractch-context' { + import VirtualMachine from 'scratch-vm'; + + export function hashString(string: string): string; + export function getExtensionSignatures(runtime: VirtualMachine.Runtime): + Record; + export function getExtensionSignatureHash(runtime: VirtualMachine.Runtime): string; + export function getChangeKey(runtime: VirtualMachine.Runtime, target: VirtualMachine.Target, + signatureHash?: string): string; +} + +declare module 'scratch-vm/src/extensions/rotur/core' { + import VirtualMachine from 'scratch-vm'; + + export function scopesUsedByProject(runtime: VirtualMachine.Runtime | null | undefined): string[]; +} + +declare module 'scratch-vm/src/engine/opcodes.generated.json' { + interface OpcodeVariadic { + prefix: string; + minItems: number; + collapsed: boolean; + } + + interface OpcodeInfo { + inputs: string[]; + fields: string[]; + primitive: boolean; + hat: boolean; + definition: boolean; + variadic?: OpcodeVariadic; + mutationInputs?: 'argumentids'; + } + + const opcodeTable: { + sources: string[]; + opcodes: Record; + }; + export = opcodeTable; +} + +declare module 'scratch-vm/events' { + export interface RuntimeCustomEvents {} + + export interface VirtualMachineCustomEvents {} +} diff --git a/test/scratch-vm-events.ts b/test/scratch-vm-events.ts new file mode 100644 index 0000000..9f68b0e --- /dev/null +++ b/test/scratch-vm-events.ts @@ -0,0 +1,35 @@ +import VirtualMachine from 'scratch-vm'; + +declare module 'scratch-vm/events' { + interface VirtualMachineCustomEvents { + TRIGGER_MANUAL_RESTORE_POINT: []; + RESTORE_POINT_START: []; + RESTORE_POINT_END: []; + } + + interface RuntimeCustomEvents { + RUNTIME_PAUSED: []; + RUNTIME_UNPAUSED: []; + } +} + +const vm = new VirtualMachine(); + +vm.on('TRIGGER_MANUAL_RESTORE_POINT', () => undefined); +vm.emit('RESTORE_POINT_START'); +vm.emit('RESTORE_POINT_END'); +vm.runtime.emit('RUNTIME_PAUSED'); +vm.runtime.on('RUNTIME_UNPAUSED', () => undefined); +vm.on('PROJECT_CHANGED', () => undefined); + +// @ts-expect-error consumer events keep their declared payloads +vm.emit('RESTORE_POINT_END', 'done'); + +// @ts-expect-error runtime custom events are not VM events +vm.on('RUNTIME_PAUSED', () => undefined); + +// @ts-expect-error VM custom events are not runtime events +vm.runtime.on('TRIGGER_MANUAL_RESTORE_POINT', () => undefined); + +// @ts-expect-error names nobody declared are still rejected +vm.emit('RESTORE_POINT_MIDDLE'); diff --git a/test/scratch-vm.ts b/test/scratch-vm.ts index 5f8fb26..b1b0126 100644 --- a/test/scratch-vm.ts +++ b/test/scratch-vm.ts @@ -1,28 +1,289 @@ import VirtualMachine from 'scratch-vm'; +import ArgumentType from 'scratch-vm/src/extension-support/argument-type'; +import BlockType from 'scratch-vm/src/extension-support/block-type'; +import { + getChangeKey, + getExtensionSignatureHash, + getExtensionSignatures, + hashString +} from 'scratch-vm/src/serialization/fractch-context'; +import {scopesUsedByProject} from 'scratch-vm/src/extensions/rotur/core'; +import opcodeTable from 'scratch-vm/src/engine/opcodes.generated.json'; const vm = new VirtualMachine(); +const runtime = vm.runtime; vm.on('targetsUpdate', update => { const editing: string | null = update.editingTarget; - const count: number = update.targetList.length; - return [editing, count]; + const names: string[] = update.targetList.map(target => target.name); + const costumeCount: number | undefined = update.targetList[0]?.costumeCount; + return [editing, names, costumeCount]; +}); + +vm.on('workspaceUpdate', data => { + const xml: string = data.xml; + const header: string = data.headerXml; + const scripts: string[] = data.blocks.scripts; + return [xml, header, scripts]; }); vm.on('PROJECT_CHANGED', () => undefined); +vm.on('LOAD_PROGRESS', ({stage, loaded, total}) => { + const named: VirtualMachine.LoadStage = stage; + return [named, loaded ?? 0, total ?? 0]; +}); +vm.on('EXTENSION_LOAD_ERROR', ({extensionID, url}) => `${extensionID} ${url}`); +vm.on('ASSET_PROGRESS', (finished, total) => finished / total); +vm.on('EXTENSION_FIELD_ADDED', (field, missing) => [field.name, missing]); +vm.on('PERIPHERAL_REQUEST_ERROR', () => undefined); +vm.on('EDIT_COMMAND_ERROR', error => error); +vm.on('CREATE_UNSANDBOXED_EXTENSION_API', scratch => scratch); +vm.on('MONITORS_UPDATE', monitors => monitors.values().map(monitor => monitor.id)); +vm.on('BLOCK_DRAG_END', (blocks, topBlockId) => blocks.some(block => block.id === topBlockId)); +vm.on('EXTENSION_ADDED', category => category.blocks.map(block => block.info.opcode)); +vm.on('EXTENSIONS_REORDERED', ({ids}) => ids.length); +vm.on('VISUAL_REPORT', ({id, value}) => `${id}${value}`); -vm.runtime.on('STAGE_SIZE_CHANGED', (width, height) => width * height); +runtime.on('STAGE_SIZE_CHANGED', (width, height) => width * height); +runtime.on('ASSET_PROGRESS', (finished, total, detail) => (detail ? detail.phase : finished + total)); +runtime.on('THREAD_STARTED', thread => thread.topBlock); +runtime.on('THREAD_ERROR', ({thread, error}) => [thread.isCompiled, error]); +runtime.on('BREAKPOINT_HIT', (thread, blockId) => thread.peekStack() === blockId); +runtime.on('EXTENSION_RELOADED', entry => entry.changed.concat(entry.added, entry.removed)); +runtime.on('EXTENSION_ERROR', report => report.kind === 'block' && report.count > 1); +runtime.on('EXTENSION_CONSOLE', message => message.level); +runtime.on('ITEMS_CHANGED', items => items.length); +runtime.on('ENTITLEMENTS_CHANGED', entitlements => Object.keys(entitlements)); +runtime.on('PLATFORM_MISMATCH', (platform, accept) => { + accept(); + return platform; +}); +runtime.on('QUESTION', question => question ?? ''); +runtime.on('targetWasCreated', (clone, original) => clone.isOriginal || original === undefined); +runtime.on('SPRITE_INFO_CHANGED', (target, change) => [target.id, change.x]); +runtime.on('PERIPHERAL_CONNECTION_LOST_ERROR', ({extensionId}) => extensionId); +runtime.emit('ANSWER', 'yes'); +runtime.emit('PLAY_NOTE', 60, 'music'); -const stage = vm.runtime.getTargetForStage(); +const stage = runtime.getTargetForStage(); const list = stage?.lookupVariableByNameAndType('output', 'list'); const firstValue: VirtualMachine.VariableValue | undefined = list?.value; +const editing = vm.editingTarget; +if (editing) { + const id: string = editing.id; + const costumes: VirtualMachine.Costume[] = editing.getCostumes(); + const sounds: VirtualMachine.Sound[] = editing.sprite.sounds; + const block: VirtualMachine.Block | undefined = editing.blocks.getBlock(id); + const top: string | null = editing.blocks.getTopLevelScript(id); + const allBlocks: VirtualMachine.Block[] = Object.values(editing.blocks._blocks); + const scripts: string[] = editing.blocks._scripts; + const json: VirtualMachine.TargetJSON = editing.toJSON(); + const bounds: VirtualMachine.Bounds | null = editing.getBounds(); + editing.setCostume(costumes.length - 1); + editing.createComment('c', null, 'note', 0, 0, 200, 200, false); + editing.postSpriteInfo({x: 1, rotationStyle: 'left-right'}); + void [costumes, sounds, block, top, allBlocks, scripts, json, bounds]; +} + +const threads: VirtualMachine.Thread[] | undefined = runtime.startHats('event_whenkeypressed', {KEY_OPTION: 'space'}); +runtime.toggleScript('top', {target: stage, stackClick: true}); +runtime.requestUpdateMonitor({id: 'monitor', visible: false}); +runtime.requestUpdateMonitor({toJS: () => ({id: 'monitor', x: 5})}); +const monitor: VirtualMachine.MonitorRecord | undefined = runtime.getMonitorState().get('monitor'); +const monitorVisible: boolean | undefined = monitor?.get('visible'); +const monitorX: number | null | undefined = monitor?.get('x'); +const xml: VirtualMachine.CategoryXML[] = runtime.getBlocksXML(stage); +const label = runtime.getLabelForOpcode('pen_clear'); +const labelText: string | undefined = label?.label; +const addon: VirtualMachine.AddonBlock | null = runtime.getAddonBlock('breakpoint'); +const categories: string[] = runtime.getCategoryOrder(); +const playing: boolean = runtime.turboMode && runtime.runtimeOptions.caseSensitiveLists; +const framerate: number = runtime.frameLoop.framerate; +const active: VirtualMachine.Thread | null = runtime.sequencer.activeThread; +runtime.ioDevices.clock.pause(); +runtime.ioDevices.cloud.requestUpdateVariable('☁ score', 10); +runtime.setBreakpoints(new Set(['a'])); +runtime.setBreakpoints(null); +const fonts: VirtualMachine.Font[] = runtime.fontManager.getFonts(); +runtime.fontManager.on('change', () => undefined); +const assetURL: string = runtime.assetManager.getObjectURL('image.png'); +const assetCount: number = runtime.assetManager.assets.length; +const fontFallbacks: string[] = runtime.fontManager.fonts.map(font => font.fallback); +const dirtyAssets = vm.assets.filter(asset => !asset.clean).map(asset => ({ + type: asset.assetType.contentType, + id: asset.assetId, + size: asset.data.byteLength +})); +const costumeURI: string | undefined = vm.editingTarget?.getCostumes()[0]?.asset?.encodeDataURI(); +const costumeText: string | undefined = vm.editingTarget?.getCostumes()[0]?.asset?.decodeText(); +const renderedBy: VirtualMachine.Renderer | null | undefined = vm.editingTarget?.renderer; +const rightClick: boolean = runtime.ioDevices.mouse.usesRightClickDown; +export const zips: [Promise, Promise, Promise, Promise, Promise] = [ + vm.saveProjectSb3(), + vm.saveProjectSb3('arraybuffer'), + vm.saveProjectSb3('base64', {allowOptimization: false}), + vm.exportSprite('sprite'), + vm.exportSprite('sprite', 'uint8array') +]; +export const collaborationLoad: Promise = vm.editingCommands.extensions.loadExtensionURL('pen'); +const renamed: void | Promise = vm.renameSprite('sprite', 'Sprite2'); + +const internals = { + blockInfo: runtime._blockInfo.map(category => category.id), + clones: runtime._cloneCounter, + hats: Object.keys(runtime._hats), + monitors: runtime._monitorState.size, + loaded: Array.from(vm.extensionManager._loadedExtensions.keys()), + generation: vm._projectLoadGeneration ?? 0, + paused: runtime.ioDevices.clock._paused +}; +const originalStep = runtime._step; +runtime._step = function () { + originalStep.call(this); +}; + +const renderer = vm.renderer; +if (renderer) { + renderer.draw(); + renderer.requestSnapshot(dataURI => dataURI.length); + renderer.setUseHighQualityRender(true); + renderer.on('UseHighQualityRenderChanged', enabled => !enabled); + const [nativeWidth, nativeHeight] = renderer.getNativeSize(); + const drawable = renderer.pick(nativeWidth / 2, nativeHeight / 2); + const extraction: VirtualMachine.DrawableExtraction = renderer.extractDrawableScreenSpace(drawable); + const color = renderer.extractColor(1, 1, 3).color; + renderer.useRealLayerIndexes = true; + void [extraction, color, renderer.canvas.toDataURL(), renderer.overlayContainer.className]; +} + +const extensions = vm.extensionManager; +const loaded: boolean = extensions.isExtensionLoaded('pen'); +const urls: Record = extensions.getExtensionURLs(); +const mode: VirtualMachine.ExtensionMode | null = extensions.getExtensionMode('pen'); +const entries: VirtualMachine.ExtensionEntry[] = extensions.getExtensionEntries(); +export const extensionWork: Promise[] = [ + extensions.loadExtensionURL('https://example.com/extension.js'), + extensions.getExtensionSource('pen', {fetch: true}), + extensions.reloadExtension('pen', 'source', {confirm: diff => diff.mode === 'iframe'}), + extensions.createExtension('source', {unsandboxed: true}), + extensions.setExtensionOrder(['pen', 'music']) +]; +vm.securityManager.canLoadExtensionFromProject = (url, options) => url.length > 0 && options?.wholeProject === true; +vm.securityManager.getSandboxMode = () => 'unsandboxed'; + +vm.editingCommands.handler = command => command.method; +vm.editingCommands.request('selectCostume', [0]); +const snapshot: VirtualMachine.EditTargetState[] = vm.editingCommands.snapshot(false); -export const loaded: Promise = vm.loadProject(new ArrayBuffer(0)).then(() => { +const restore: VirtualMachine.RestoreFunction = vm.deleteSprite('sprite'); +const restoreCostume = vm.deleteCostume(0); +if (typeof restoreCostume === 'function') { + restoreCostume().then(() => undefined); +} +const reordered: boolean | Promise = vm.reorderCostume('sprite', 0, 1); +const usage: VirtualMachine.ExtensionUsage = vm.getExtensionUsage('pen'); +const context: VirtualMachine.FractchContext = vm.getFractchContext(); +const hash: string | null = vm.getCodeStateHash('sprite'); +const files: Record = vm.saveProjectSb3DontZip(); +const drawableTarget: string | null = vm.getTargetIdForDrawableId(1); +vm.addAddonBlock({ + procedureCode: 'breakpoint', + arguments: [], + callback: (args, util) => util.thread.topBlock, + displayName: 'breakpoint' +}); +vm.postIOData('mouse', {x: 0, y: 0, isDown: true}); +vm.setRuntimeOptions({maxClones: Infinity}); +vm.setCompilerOptions({warpTimer: true}); + +const argumentType: 'string' = ArgumentType.STRING; +const blockType: 'reporter' = BlockType.REPORTER; +const signatures = getExtensionSignatures(runtime); +const signatureArguments = Object.values(signatures).flatMap(signature => + signature.blocks.map(block => block.arguments.length)); +const changeKey: string = getChangeKey(runtime, stage ?? runtime.targets[0]!, getExtensionSignatureHash(runtime)); +const textHash: string = hashString('text'); +const scopes: string[] = scopesUsedByProject(runtime); +const addInfo = opcodeTable.opcodes.operator_add; +const variadicPrefix: string | undefined = addInfo?.variadic?.prefix; + +export const loadedProject: Promise = vm.loadProject(new ArrayBuffer(0)).then(() => { vm.setEditingTarget(stage?.id ?? ''); return firstValue === undefined ? undefined : undefined; }); +export const used = [ + threads, monitor, xml, labelText, addon, categories, playing, framerate, active, fonts, assetURL, + assetCount, fontFallbacks, dirtyAssets, costumeURI, costumeText, renderedBy, rightClick, renamed, + monitorVisible, monitorX, internals, loaded, urls, mode, entries, snapshot, restore, reordered, usage, context, + hash, files, drawableTarget, argumentType, blockType, signatureArguments, changeKey, textHash, + scopes, variadicPrefix +]; + // @ts-expect-error unknown events are rejected vm.on('not-an-event', () => undefined); +// @ts-expect-error the VM does not forward PROJECT_LOADED, listen on the runtime instead +vm.on('PROJECT_LOADED', () => undefined); + +// @ts-expect-error the VM forwards PERIPHERAL_REQUEST_ERROR without a payload +vm.on('PERIPHERAL_REQUEST_ERROR', (error: {message: string}) => error); + +// @ts-expect-error workspace updates are VM events, not runtime events +runtime.on('workspaceUpdate', () => undefined); + // @ts-expect-error listeners must match the payload -vm.runtime.on('MIC_LISTENING', (listening: string) => listening); +runtime.on('MIC_LISTENING', (listening: string) => listening); + +// @ts-expect-error the project's platform is not validated, so it is unknown +runtime.on('PLATFORM_MISMATCH', (platform: {name: string}) => platform.name); + +// @ts-expect-error the runtime has no projectName +export const projectName = runtime.projectName; + +// @ts-expect-error startHats upper-cases match fields, so they must be strings +runtime.startHats('event_whenkeypressed', {KEY_OPTION: 32}); + +// @ts-expect-error toggleScript takes a target, not a target id +runtime.toggleScript('top', {target: 'stage'}); + +// @ts-expect-error getTargetById can miss +export const missing: VirtualMachine.RenderedTarget = runtime.getTargetById('sprite'); + +// @ts-expect-error deleteCostume can return a rejected promise or null instead of a restore function +export const restoreDirect: VirtualMachine.RestoreFunction = vm.deleteCostume(0); + +// @ts-expect-error reorder results are promises while a collaboration handler is set +export const reorderedDirect: boolean = vm.reorderSound('sprite', 0, 1); + +// @ts-expect-error the renderer declaration only covers what scratch-gui calls +renderer?.updateSVGSkin(1, ''); + +// @ts-expect-error extension modes are a closed set +vm.securityManager.getSandboxMode = () => 'sandboxed'; + +// @ts-expect-error ArgumentType is read-only +ArgumentType.STRING = 'text'; + +// @ts-expect-error rotation styles are a closed set +editing?.postSpriteInfo({rotationStyle: 'sideways'}); + +// @ts-expect-error monitor records only answer for their own fields +monitor?.get('colour'); + +// @ts-expect-error saveProjectSb3 resolves to a Blob by default +export const notABuffer: Promise = vm.saveProjectSb3(); + +// @ts-expect-error unknown zip output types are rejected +vm.exportSprite('sprite', 'zip'); + +// @ts-expect-error renames only return a promise while a collaboration handler is set +vm.renameCostume(0, 'costume').then(() => undefined); + +// @ts-expect-error setXY is not part of the declared surface +editing?.setXY(0, 0); + +export const packagerSurface = (machine: VirtualMachine): unknown[] => { + machine.convertToPackagedRuntime(); + return [Object.keys(machine.runtime.peripheralExtensions), machine.runtime.v2BitmapAdapter]; +};