shithub: scrax

ref: 81d1e4e01d7001e6a0107792e6167136787295d6
dir: /external.js/

View raw version
import {append, flatten} from "./model.js"
import {Forever, Variable, List, Define, TextParameter, BooleanParameter, Else, BooleanSlot, compareName} from "./core.js"
import * as core from "./core.js"
import {Stage} from "./stage.js"
import {MD5} from "./md5.js"
import {zip, unzip} from "./zip.js"

let decoder = new TextDecoder()
let missing = new Set()

export async function load(id, options = {})
{
	let response1 = await fetch(`https://trampoline.turbowarp.org/proxy/projects/${id}`)
	if (!response1.ok) return
	let {project_token, title} = await response1.json()
	
	let response2 = await fetch(`https://projects.scratch.mit.edu/${id}?token=${project_token}`)
	if (!response2.ok) return
	let project = await response2.json()
	
	let stage = await fromExternal(project, ({md5ext}) => fetch(`https://assets.scratch.mit.edu/internalapi/asset/${md5ext}/get/`).then(response => response.blob()), options)
	stage.name = title
	return stage
}

export async function fromSB3(bytes, options = {})
{
	let files = await unzip(bytes)
	let project = files?.get("project.json")
	if (!project) return
	let types = {png: "image/png", jpg: "image/jpeg", svg: "image/svg+xml"}
	return fromExternal(JSON.parse(decoder.decode(project)), ({md5ext, type}) => new Blob([files.get(md5ext)], {type: types[type] ?? "application/octet-stream"}), options)
}

async function fromExternal(project, getBlob, options = {})
{
	let stage = new Stage()
	
	for (let name of project.extensions) {
		if (!await stage.addExtension(name)) {
			console.log("could not load extension: " + name)
		}
	}
	
	for (let target of project.targets) {
		
		target = {...target, costumes: target.costumes.map(costume => ({...costume, blob: getBlob(costume)}))}
		let sprite
		
		if (target.isStage) {
			
			for (let backdrop of target.costumes) {
				stage.makeBackdrop({name: backdrop.name, blob: await backdrop.blob, x: backdrop.rotationCenterX, y: backdrop.rotationCenterY})
			}
			
			stage.backdrops[0].remove()
			stage.backdrop = stage.backdrops[target.currentCostume]
			
			for (let [name, value] of Object.values(target.variables)) {
				stage.variables.set(name, {value})
			}
			for (let [name, list] of Object.values(target.lists)) {
				stage.lists.set(name, list.slice())
			}
		}
		else {
			
			sprite = stage.makeSprite({name: target.name})
			sprite.direction = target.direction
			sprite.size = target.size
			sprite.x = target.x
			sprite.y = target.y
			if (!target.visible) sprite.hide()
			if (target.draggable) sprite.draggable = true
			
			for (let costume of target.costumes) {
				sprite.makeCostume({name: costume.name, blob: await costume.blob, x: costume.rotationCenterX, y: costume.rotationCenterY})
			}
			
			sprite.costumes[0].remove()
			sprite.costume = sprite.costumes[target.currentCostume]
			
			for (let [name, value] of Object.values(target.variables)) {
				sprite.selfVariables.set(name, {value})
			}
			
			for (let [name, list] of Object.values(target.lists)) {
				sprite.selfLists.set(name, list.slice())
			}
		}
		
		let options1 = {
			...options,
			MakeBlock: opcode => options.MakeBlock?.(opcode) ?? stageConversions[opcode]?.({stage, sprite, scratch: stage.scratch}),
			variables: (sprite ?? stage).variables, lists: (sprite ?? stage).lists,
		}
		
		let scripts = fromExternalTarget(target, options1).filter(block => block.type.hat)
		if (target.isStage) stage.scripts.push(...scripts)
		else sprite.scripts.push(...scripts)
	}
	
	let ordered = project.targets.filter(target => !target.isStage).sort((a, b) => a.layerOrder - b.layerOrder)
	for (let {name} of ordered) {
		stage.getSprite(name).gotoFront()
	}
	
	if (missing.size > 0) console.log("missing:", ...missing)
	return stage
}

function fromExternalTarget(target, options = {})
{
	let blocks = []
	
	for (let info of Object.values(target.blocks)) {
		if (!info.topLevel) continue
		let stack = []
		convert(info, target.blocks, options, stack)
		if (stack.length === 0) continue
		if (typeof stack[0] !== "object") continue
		if (stack[0].type.shape === undefined && !stack[0].type.hat) Forever(...stack)
		blocks.push(stack[0])
	}
	
	for (let block of blocks.flatMap(flatten)) {
		if (block.type.shape === undefined && !block.type.hat && block.type.category === "custom" && !blocks.some(other => other.type === Define.type && compareName(other.names[0], block.names[0]))) {
			blocks.push(Define(block.names[0].map((part, i) => typeof part === "string" ? part : {type: part.type, name: "input " + (i + 1)})))
		}
	}
	
	return blocks
}

function convert(info, infos, options, stack0)
{
	if (!info) return
	let convert1 = options.MakeBlock?.(info.opcode) ?? conversions[info.opcode]
	if (!convert1) {
		missing.add(info.opcode)
		if (info.next) convert(infos[info.next], infos, options, stack0)
		return
	}
	
	let inputs = {}
	
	for (let [name, [_type, value]] of Object.entries(info.inputs)) {
		
		if (value === null) continue
		
		if (info.opcode !== "procedures_call") name = name.toLowerCase()
		if (name.startsWith("substack")) {
			let stack = []
			convert(infos[value], infos, options, stack)
			inputs[name] = stack
			continue
		}
		
		if (value instanceof Array) {
			if (value[0] === 12) {
				inputs[name] = Variable(toName(value[1]))
				continue
			}
			if (value[0] === 13) {
				inputs[name] = List(toName(value[1]))
				continue
			}
			inputs[name] = String(value[1])
			continue
		}
		
		let stack = []
		convert(infos[value], infos, options, stack)
		if (stack.length !== 1 || typeof stack[0] === "object" && stack[0].type.shape === undefined) {
			inputs[name] = ""
			continue
		}
		
		inputs[name] = stack[0]
	}
	
	for (let [name, [value]] of Object.entries(info.fields)) inputs[name.toLowerCase()] = value
	
	let block = convert1(inputs, info, infos)
	stack0.push(block)
	
	if (typeof block === "object" && block.type.hat) {
		let stack = []
		if (info.next) convert(infos[info.next], infos, options, stack)
		for (let other of stack) append(block, other)
		return
	}
	
	if (!info.next || typeof block === "object" && block.type.cap) return
	convert(infos[info.next], infos, options, stack0)
}

function toName(name)
{
	return name.normalize().replace(/\s+/ug, " ").trim()
}

function customName(name, names)
{
	let i = 0
	return name.split(/(%b|%s|%n)/g).map(part =>
	{
		if (part === "%b") return {type: "boolean", name: toName(names[i++] ?? "")}
		if (part === "%s") return {type: "text", name: toName(names[i++] ?? "")}
		if (part === "%n") return {type: "text", name: toName(names[i++] ?? "")}
		return toName(part)
	}).filter(Boolean)
}

export async function toSB3(stage)
{
	let missing = new Set()
	let files = []
	let result = {monitors: [], extensions: Object.keys(stage.extensions), meta: {semver: "3.0.0", vm: "0.0.0", agent: "Scrax"}, targets: []}
	let fns = []
	fns.push(async () => result.targets[0] = await toExternalStage(stage, files, missing))
	for (let [i, sprite] of stage.sprites.entries()) fns.push(async () => result.targets[i + 1] = await toExternalSprite(sprite, files, missing))
	await Promise.all(fns.map(fn => fn()))
	files.push(new File([JSON.stringify(result)], "project.json"))
	if (missing.size > 0) console.log("missing:", ...missing)
	return zip(files)
}

function toExternalSprite(sprite, files, missing)
{
	let escaped = sprite.name.replaceAll("|", "||").replaceAll("/", "|")
	
	let blocks = {}
	let every = sprite.scripts.flatMap(flatten)
	for (let block of every) toExternalBlock(block, blocks, {scripts: sprite.scripts, variables: sprite.selfVariables, lists: sprite.selfLists, name: escaped, missing, conversions: spriteConversions(sprite)})
	
	let variables = {}
	let lists = {}
	let broadcasts = {}
	let costumes = []
	let sounds = []
	
	let fns = []
	for (let [i, costume] of sprite.costumes.entries()) fns.push(async () => costumes[i] = await toExternalCostume(costume, files))
	for (let [name, {value}] of sprite.selfVariables) variables[escaped + "/var/" + name] = [name, value]
	for (let [name, list] of sprite.selfLists) lists[escaped + "/list/" + name] = [name, list.slice()]
	
	let result = {
		isStage: false,
		name: sprite.name,
		variables, lists, blocks,
		broadcasts, costumes, sounds,
		comments: {},
		currentCostume: sprite.costumes.indexOf(sprite.costume),
		volume: sprite.volume,
		visible: sprite.visible,
		draggable: sprite.draggable,
		x: sprite.x, y: sprite.y,
		size: sprite.size,
		direction: sprite.direction,
		rotationStyle: {full: "all around", none: "don't rotate", "left-right": "left-right"}[sprite.rotationStyle],
	}
	
	return Promise.all(fns.map(fn => fn())).then(() => result)
}

function toExternalStage(stage, files, missing)
{
	let blocks = {}
	let every = stage.scripts.flatMap(flatten)
	for (let block of every) toExternalBlock(block, blocks, {scripts: stage.scripts, missing, conversions: stageConversions1(stage)})
	
	let variables = {}
	let lists = {}
	let broadcasts = {}
	let costumes = []
	let sounds = []
	
	let fns = []
	for (let [i, backdrop] of stage.backdrops.entries()) fns.push(async () => costumes[i] = await toExternalCostume(backdrop, files))
	for (let [name, {value}] of stage.variables) variables["stage/var/" + name] = [name, value]
	for (let [name, list] of stage.lists) lists["stage/list/" + name] = [name, list.slice()]
	
	let result = {
		isStage: true,
		name: "Stage",
		variables, lists, blocks,
		broadcasts, costumes, sounds,
		comments: {},
		currentCostume: stage.backdrops.indexOf(stage.backdrop),
		volume: stage.volume,
		tempo: stage.extensions.music?.tempo ?? 60,
	}
	
	return Promise.all(fns.map(fn => fn())).then(() => result)
}

async function toExternalCostume(costume, files)
{
	let md5 = MD5(new Uint8Array(await costume.blob.arrayBuffer()))
	let ext = {"image/png": "png", "image/jpeg": "jpg", "image/svg+xml": "svg"}[costume.blob.type] ?? "bin"
	files.push(new File([costume.blob], md5 + "." + ext))
	return {
		name: costume.name,
		assetId: md5,
		md5ext: md5 + "." + ext,
		dataFormat: ext,
		rotationCenterX: costume.x,
		rotationCenterY: costume.y,
	}
}

function toExternalBlock(block, blocks, {scripts, variables, lists, name, missing, conversions})
{
	if (block.type === Variable.type) return
	if (block.type === List.type) return
	if (block.type.shape === "slot") return
	
	if (block.type === Define.type) {
		
		let inputs = {}
		let values = block.names[0].filter(part => typeof part !== "string")
		for (let [i, part] of values.entries()) {
			inputs[`param/${part.type}/${part.name}`] = [1, block.id + "/" + i]
			blocks[block.id + "/" + i] = {
				opcode: part.type === "boolean" ? "argument_reporter_boolean" : "argument_reporter_string_number",
				parent: block.id + "/x",
				inputs: {},
				fields: {VALUE: [part.name, null]},
				shadow: true,
				topLevel: false,
			}
		}
		
		blocks[block.id] = {opcode: "procedures_definition", inputs: {custom_block: [1, block.id + "/x"]}, fields: {}, topLevel: true, parent: null}
		blocks[block.id + "/x"] = {
			opcode: "procedures_prototype",
			inputs, fields: {},
			parent: String(block.id),
			shadow: true,
			topLevel: true,
			mutation: {
				tagName: "mutation",
				children: [],
				proccode: block.names[0].map(part => typeof part === "string" ? part : part.type === "boolean" ? "%b" : "%s").join(" "),
				argumentids: JSON.stringify(values.map(part => `param/${part.type}/${part.name}`)),
				argumentnames: JSON.stringify(values.map(part => part.name)),
				argumentdefaults: JSON.stringify(values.map(() => "")),
				warp: JSON.stringify(block.atomic),
			},
		}
		
		if (block.stack.length > 0) blocks[block.id].next = String(block.stack[0].id)
		
		return
	}
	
	function toVariable(name1)
	{
		return [name1, (variables?.has(name1) ? name + "/var/" : "stage/var/") + name1]
	}
	
	function toList(name1)
	{
		return [name1, (lists?.has(name1) ? name + "/list/" : "stage/list/") + name1]
	}
	
	let previous = block.parent?.stack?.[block.parent.stack.indexOf(block) - 1]
	previous ??= block.parent
	
	if (block.type.shape === undefined && !block.type.hat && block.type.category === "custom") {
		
		let block0 = scripts.find(block0 => block0.type === Define.type && compareName(block0.names[0], block.names[0]))
		
		let values = block0.names[0].filter(part => typeof part !== "string")
		let ids = values.map(part => `param/${part.type}/${part.name}`)
		let inputs = {}
		blocks[block.id] = {
			opcode: "procedures_call",
			inputs, fields: {},
			topLevel: false,
			mutation: {
				tagName: "mutation",
				children: [],
				proccode: block.names[0].map(part => typeof part === "string" ? part : part.type === "boolean" ? "%b" : "%s").join(" "),
				argumentids: JSON.stringify(ids),
				warp: JSON.stringify(block0.atomic),
			},
			parent: String(previous.id),
		}
		
		for (let [i, input] of block.inputs.entries()) {
			inputs[ids[i]] = [3, String(input.id), [10, ""]]
			if (input.type === Variable.type) inputs[ids[i]] = [3, [12, ...toVariable(input.names[0])], [10, ""]]
			if (input.type === List.type) inputs[ids[i]] = [3, [13, ...toList(input.names[0])], [10, ""]]
			if (input.type.shape === "slot") inputs[ids[i]] = [1, [10, input.value]]
			if (input.type.shape === "slot" && values[i].type === "boolean") delete inputs[ids[i]]
		}
		
		let next = block.parent.stack[block.parent.stack.indexOf(block) + 1]
		if (next) blocks[block.id].next = String(next.id)
		
		return
	}
	
	if (block.type === TextParameter.type || block.type === BooleanParameter.type) {
		let opcode = block.type === BooleanParameter.type ? "argument_reporter_boolean" : "argument_reporter_string_number"
		blocks[block.id] = {opcode, parent: String(block.parent.id), inputs: {}, fields: {VALUE: [block.names[0], null]}, topLevel: false}
		return
	}
	
	let conversion = conversions1.get(block.type) ?? conversions.get(block.type)
	
	if (!conversion) {
		missing.add(block.type)
		return
	}
	
	let [opcode, ...args] = conversion
	let inputs = {}
	let fields = {}
	
	blocks[block.id] = {opcode, inputs, fields, topLevel: block.type.hat, parent: null}
	
	if (block.type.hat) {
		if (block.stack.length > 0) blocks[block.id].next = String(block.stack[0].id)
	}
	else {
		if (block.stack) {
			if (block.stack.length > 0) inputs.SUBSTACK = [2, String(block.stack[0].id)]
		}
		blocks[block.id].parent = String(previous.id)
		if (block.type.shape === undefined) {
			if (block.complement?.type === Else.type) {
				blocks[block.id].opcode = "control_if_else"
				if (block.complement.stack.length > 0) inputs.SUBSTACK2 = [2, String(block.complement.stack[0].id)]
			}
			let next = block.parent.stack[block.parent.stack.indexOf(block) + 1]
			if (next) blocks[block.id].next = String(next.id)
		}
	}
	
	for (let [i, reference] of block.type.references?.entries() ?? []) {
		if (reference === "variable") fields.VARIABLE = toVariable(block.names[i])
		if (reference === "list") fields.LIST = toList(block.names[i])
	}
	
	let i = 0
	for (let arg of args) {
		
		if (typeof arg === "string") {
			
			let block1 = block.inputs[i]
			if (arg.endsWith(":u")) {
				fields[arg.slice(0, -2)] = [block1.value.toUpperCase(), null]
				i++
				continue
			}
			
			inputs[arg] = [3, String(block1.id), [10, ""]]
			if (block1.type === Variable.type) inputs[arg] = [3, [12, ...toVariable(block1.names[0])], [10, ""]]
			if (block1.type === List.type) inputs[arg] = [3, [13, ...toList(block1.names[0])], [10, ""]]
			if (block1.type.shape === "slot") inputs[arg] = [1, [10, block1.value]]
			if (block1.type.shape === "slot" && block.type.slots[i].type === BooleanSlot.type) delete inputs[arg]
			
			i++
			continue
		}
		
		if (arg.length === 1) {
			let block1 = block.inputs[i]
			if (block1.type.shape === "slot") fields[arg[0]] = [block1.value, null]
			else fields[arg[0]] = ["", null]
			i++
			continue
		}
		
		if (arg.length === 2) {
			fields[arg[0]] = [arg[1], null]
			continue
		}
		
		if (arg.length === 4) {
			inputs[arg[0]] = [1, block.id + "/x"]
			blocks[block.id + "/x"] = {opcode: arg[1], fields: {[arg[2]]: [arg[3], null]}, topLevel: false, shadow: true}
			continue
		}
		
		let block1 = block.inputs[i]
		inputs[arg[0]] = [3, String(block1.id), [10, ""]]
		if (block1.type === Variable.type) inputs[arg[0]] = [3, [12, ...toVariable(block1.names[0])], [10, ""]]
		if (block1.type === List.type) inputs[arg[0]] = [3, [13, ...toList(block1.names[0])], [10, ""]]
		if (block1.type.shape === "slot" && block.type.slots[i].type === BooleanSlot.type) delete inputs[arg[0]]
		if (block1.type.shape === "slot" && block.type.slots[i].type !== BooleanSlot.type) {
			inputs[arg[0]] = [1, String(block1.id)]
			blocks[block1.id] = {opcode: arg[1], fields: {[arg[2]]: [block1.value, null]}, topLevel: false, shadow: true}
		}
		
		i++
	}
}

let conversions = {
	
	event_whenflagclicked: () => core.WhenFlagClicked(),
	control_forever: inputs => core.Forever(...inputs.substack ?? []),
	control_repeat: inputs => core.Repeat(inputs.times, ...inputs.substack ?? []),
	control_if: inputs => core.If(inputs.condition, ...inputs.substack ?? []),
	control_if_else: inputs =>
	{
		let block = core.IfElse(inputs.condition, ...inputs.substack ?? [])
		append(block.complement, ...inputs.substack2 ?? [])
		return block
	},
	control_wait_until: inputs => core.WaitUntil(inputs.condition),
	control_repeat_until: inputs => core.RepeatUntil(inputs.condition, ...inputs.substack ?? []),
	operator_add: inputs => core.Add(inputs.num1, inputs.num2),
	operator_subtract: inputs => core.Subtract(inputs.num1, inputs.num2),
	operator_multiply: inputs => core.Multiply(inputs.num1, inputs.num2),
	operator_divide: inputs => core.Divide(inputs.num1, inputs.num2),
	operator_lt: inputs => core.LessThan(inputs.operand1, inputs.operand2),
	operator_equals: inputs => core.Equals(inputs.operand1, inputs.operand2),
	operator_gt: inputs => core.GreaterThan(inputs.operand1, inputs.operand2),
	operator_and: inputs => core.And(inputs.operand1, inputs.operand2),
	operator_or: inputs => core.Or(inputs.operand1, inputs.operand2),
	operator_not: inputs => core.Not(inputs.operand),
	operator_join: inputs => core.Join(inputs.string1, inputs.string2),
	operator_letter_of: inputs => core.TextIndex(inputs.string, inputs.letter),
	operator_length: inputs => core.TextLength(inputs.string),
	operator_contains: inputs => core.TextContains(inputs.string1, inputs.string2),
	operator_mod: inputs => core.Modulo(inputs.num1, inputs.num2),
	data_setvariableto: inputs => core.SetVariable(toName(inputs.variable), inputs.value),
	data_changevariableby: inputs => core.ChangeVariable(toName(inputs.variable), inputs.value),
	data_addtolist: inputs => core.Append(toName(inputs.list), inputs.item),
	data_deleteoflist: inputs =>
	{
		if (inputs.index === "all") return core.Clear(toName(inputs.list))
		if (inputs.index === "last") return core.Delete(toName(inputs.list), core.Length(toName(inputs.list)))
		return core.Delete(toName(inputs.list), inputs.index)
	},
	data_deletealloflist: inputs => core.Clear(toName(inputs.list)),
	data_insertatlist: inputs => core.Insert(toName(inputs.list), inputs.index, inputs.item),
	data_replaceitemoflist: inputs => core.Replace(toName(inputs.list), inputs.index, inputs.item),
	data_itemoflist: inputs => core.Index(toName(inputs.list), inputs.index),
	data_itemnumoflist: inputs => core.Find(toName(inputs.list), inputs.item),
	data_lengthoflist: inputs => core.Length(toName(inputs.list)),
	data_listcontainsitem: inputs => core.Contains(toName(inputs.list), inputs.item),
	procedures_definition: (_inputs, info, infos) =>
	{
		let {mutation} = infos[info.inputs.custom_block[1]]
		let block = core.Define(customName(mutation.proccode, JSON.parse(mutation.argumentnames)))
		if (mutation.warp === "true") block.atomic = true
		return block
	},
	procedures_call: (inputs, info) => core.Custom(customName(info.mutation.proccode, []), ...JSON.parse(info.mutation.argumentids).map(n => inputs[n])),
	procedures_prototype: () => { },
	argument_reporter_string_number: inputs => core.TextParameter(toName(inputs.value)),
	argument_reporter_boolean:  inputs => core.BooleanParameter(toName(inputs.value)),
}

let stageConversions = {
	control_wait: ({scratch}) => inputs => scratch.Wait(inputs.duration),
	sensing_timer: ({scratch}) => () => scratch.Timer(),
	sensing_resettimer: ({scratch}) => () => scratch.ResetTimer(),
	sensing_current: ({scratch}) => inputs =>
	{
		if (inputs.currentmenu === "YEAR") return scratch.Year()
		if (inputs.currentmenu === "MONTH") return scratch.Month()
		if (inputs.currentmenu === "DATE") return scratch.Day()
		if (inputs.currentmenu === "DAYOFWEEK") return scratch.DayOfWeek()
		if (inputs.currentmenu === "HOUR") return scratch.Hour()
		if (inputs.currentmenu === "MINUTE") return scratch.Minute()
		if (inputs.currentmenu === "SECOND") return scratch.Second()
	},
	sensing_dayssince2000: ({scratch}) => () => scratch.DaysSince2000(),
	sensing_username: ({scratch}) => () => scratch.Username(),
	operator_random: ({scratch}) => inputs => scratch.PickRandom(inputs.from, inputs.to),
	operator_round: ({scratch}) => inputs => scratch.Round(inputs.num),
	operator_mathop: ({scratch}) => inputs =>
	{
		let ops = {abs: scratch.Abs, ceiling: scratch.Ceiling, floor: scratch.Floor, sqrt: scratch.Sqrt, sin: scratch.Sin, cos: scratch.Cos, tan: scratch.Tan, asin: scratch.ArcSin, acos: scratch.ArcCos, atan: scratch.ArcTan, ln: scratch.Ln, log: scratch.Log, "e ^": scratch.Exp, "10 ^": scratch.Exp10}
		return ops[inputs.operator]?.(inputs.num)
	},
	control_stop: ({scratch}) => inputs =>
	{
		if (inputs.stop_option === "this script") return core.Stop()
		if (inputs.stop_option === "all") return scratch.StopAll()
		if (inputs.stop_option === "other scripts in sprite") return scratch.StopOther()
	},
	motion_movesteps: ({sprite}) => inputs => sprite.Move(inputs.steps),
	motion_turnright: ({sprite}) => inputs => sprite.RotateRight(inputs.degrees),
	motion_turnleft: ({sprite}) => inputs => sprite.RotateLeft(inputs.degrees),
	motion_pointindirection: ({sprite}) => inputs => sprite.SetDirection(inputs.direction),
	// motion_pointtowards: ...,
	motion_gotoxy: ({sprite}) => inputs => sprite.GotoXY(inputs.x, inputs.y),
	// motion_goto: ...,
	// motion_glidesecstoxy: ...,
	// motion_glideto: ...,
	motion_changexby: ({sprite}) => inputs => sprite.ChangeX(inputs.dx),
	motion_setx: ({sprite}) => inputs => sprite.SetX(inputs.x),
	motion_changeyby: ({sprite}) => inputs => sprite.ChangeY(inputs.dy),
	motion_sety: ({sprite}) => inputs => sprite.SetY(inputs.y),
	// motion_ifonedgebounce: ...,
	motion_setrotationstyle: ({sprite}) => inputs =>
	{
		if (inputs.style === "don't rotate") return sprite.DisableRotation()
		if (inputs.style === "all around") return sprite.EnableRotation()
		if (inputs.style === "left-right") return sprite.EnableFlipping()
	},
	motion_xposition: ({sprite}) => () => sprite.GetX(),
	motion_yposition: ({sprite}) => () => sprite.GetY(),
	motion_direction: ({sprite}) => () => sprite.GetDirection(),
	looks_say: ({sprite}) => inputs => sprite.Say(inputs.message),
	looks_think: ({sprite}) => inputs => sprite.Think(inputs.message),
	looks_sayforsecs: ({sprite}) => inputs => sprite.SayFor(inputs.message, inputs.secs),
	looks_thinkforsecs: ({sprite}) => inputs => sprite.ThinkFor(inputs.message, inputs.secs),
	looks_show: ({sprite}) => () => sprite.Show(),
	looks_hide: ({sprite}) => () => sprite.Hide(),
	looks_changeeffectby: ({sprite}) => inputs => sprite.ChangeEffect(inputs.effect.toLowerCase(), inputs.change),
	looks_seteffectto: ({sprite}) => inputs => sprite.SetEffect(inputs.effect.toLowerCase(), inputs.value),
	looks_cleargraphiceffects: ({sprite}) => () => sprite.ClearEffects(),
	looks_changesizeby: ({sprite}) => inputs => sprite.ChangeSize(inputs.change),
	looks_setsizeto: ({sprite}) => inputs => sprite.SetSize(inputs.size),
	looks_size: ({sprite}) => () => sprite.GetSize(),
	looks_gotofrontback: ({sprite}) => inputs =>
	{
		if (inputs.front_back === "front") return sprite.GotoFront()
		if (inputs.front_back === "back") return sprite.GotoBack()
	},
	looks_goforwardbackwardlayers: ({sprite}) => inputs =>
	{
		if (inputs.forward_backward === "forward") return sprite.GoForward(inputs.num)
		if (inputs.forward_backward === "backward") return sprite.GoBackward(inputs.num)
	},
	looks_switchcostumeto: ({sprite}) => inputs => sprite.SetCostume(inputs.costume),
	looks_nextcostume: ({sprite}) => () => sprite.NextCostume(),
	looks_costumenumbername: ({sprite}) => inputs =>
	{
		if (inputs.number_name === "number") return sprite.CostumeNumber()
		if (inputs.number_name === "name") return sprite.CostumeName()
	},
	looks_costume: () => inputs => String(inputs.costume),
	looks_switchbackdropto: ({stage}) => inputs => stage.SetBackdrop(inputs.backdrop),
	// looks_switchbackdroptoandwait: ...,
	looks_backdrops: () => inputs => String(inputs.backdrop),
	looks_nextbackdrop: ({stage}) => () => stage.NextBackdrop(),
	looks_backdropnumbername: ({stage}) => inputs =>
	{
		if (inputs.number_name === "number") return stage.BackdropNumber()
		if (inputs.number_name === "name") return stage.BackdropName()
	},
	// sound_play: ...,
	// sound_playuntildone: ...,
	// sound_stopallsounds: ...,
	// sound_seteffectto: ...,
	// sound_changeeffectby: ...,
	// sound_cleareffects: ...,
	// sound_changevolumeby: ...,
	// sound_setvolumeto: ...,
	// sound_volume: ...,
	event_whenthisspriteclicked: ({sprite}) => () => sprite.WhenClicked(),
	event_whenstageclicked: ({stage}) => () => stage.WhenClicked(),
	event_whenbroadcastreceived: ({stage}) => inputs => stage.WhenReceived(inputs.broadcast_option),
	// event_whengreaterthan: ...,
	// event_whenbackdropswitchesto: ...,
	event_broadcast: ({stage}) => inputs => stage.Broadcast(inputs.broadcast_input),
	event_broadcastandwait: ({stage}) => inputs => stage.BroadcastAndWait(inputs.broadcast_input),
	event_whenkeypressed: ({stage}) => inputs => stage.WhenKeyPressed(inputs.key_option),
	control_start_as_clone: ({sprite}) => () => sprite.WhenCloned(),
	control_create_clone_of: ({stage, sprite}) => inputs =>
	{
		if (inputs.clone_option === "_myself_") return sprite.Clone()
		return stage.CloneSprite(inputs.clone_option)
	},
	control_create_clone_of_menu: () => inputs => inputs.clone_option,
	control_delete_this_clone: ({sprite}) => () => sprite.Delete(),
	sensing_touchingobject: ({sprite}) => inputs =>
	{
		// todo: allow sprite selection
		if (inputs.touchingobjectmenu === "_mouse_") return sprite.TouchingMouse()
	},
	sensing_touchingobjectmenu: () => inputs => inputs.touchingobjectmenu,
	// sensing_touchingcolor: ...,
	// sensing_coloristouchingcolor: ...,
	sensing_distanceto: ({sprite}) => inputs => sprite.DistanceToSprite(inputs.distancetomenu),
	sensing_distancetomenu: () => inputs => inputs.distancetomenu,
	// sensing_askandwait: ...,
	// sensing_answer: ...,
	sensing_keypressed: ({stage}) => inputs => stage.KeyPressed(inputs.key_option),
	sensing_mousedown: ({stage}) => () => stage.MouseDown(),
	sensing_mousex: ({stage}) => () => stage.GetMouseX(),
	sensing_mousey: ({stage}) => () => stage.GetMouseY(),
	// sensing_setdragmode: ...,
	// sensing_loudness: ...,
	// sensing_of: ...,
	sensing_keyoptions: () => inputs => String(inputs.key_option),
	// data_showlist: ...,
	// data_hidelist: ...,
	
	pen_clear: ({sprite}) => () => sprite.extensions.pen.blocks.Clear(),
	pen_stamp: ({sprite}) => () => sprite.extensions.pen.blocks.Stamp(),
	pen_penDown: ({sprite}) => () => sprite.extensions.pen.blocks.PenDown(),
	pen_penUp: ({sprite}) => () => sprite.extensions.pen.blocks.PenUp(),
	pen_setPenColorToColor: ({sprite}) => inputs => sprite.extensions.pen.blocks.SetPenColor(inputs.color),
	pen_changePenSizeBy: ({sprite}) => inputs => sprite.extensions.pen.blocks.ChangePenSize(inputs.size),
	pen_setPenSizeTo: ({sprite}) => inputs => sprite.extensions.pen.blocks.SetPenSize(inputs.size),
	// pen_setPenColorParamTo: ...,
	// pen_changePenColorParamBy: ...,
	// pen_menu_colorParam: ...,
	
	// music_playDrumForBeats: ...,
	// music_restForBeats: ...,
	music_playNoteForBeats: ({sprite, stage}) => inputs => (sprite ?? stage).extensions.music.blocks.PlayNote(inputs.note, inputs.beats),
	// music_setInstrument: ...,
	// music_setTempo: ...,
	// music_changeTempo: ...,
	note: () => inputs => String(inputs.note),
	// music_getTempo: ...,
	
	// videoSensing_whenMotionGreaterThan: ...,
	// videoSensing_videoOn: ...,
	// videoSensing_videoToggle: ...,
	// videoSensing_setVideoTransparency: ...,
	// text2speech_speakAndWait: ...,
	// text2speech_setVoice: ...,
	// text2speech_setLanguage: ...,
	// translate_getTranslate: ...,
	// translate_getViewerLanguage: ...,
	// boost_motorOnFor: ...,
	// boost_motorOnForRotation: ...,
	// boost_motorOn: ...,
	// boost_motorOff: ...,
	// boost_setMotorPower: ...,
	// boost_setMotorDirection: ...,
	// boost_getMotorPosition: ...,
	// boost_whenColor: ...,
	// boost_seeingColor: ...,
	// boost_whenTilted: ...,
	// boost_getTiltAngle: ...,
	// boost_setLightHue: ...,
	// ev3_motorTurnClockwise: ...,
	// ev3_motorTurnCounterClockwise: ...,
	// ev3_motorSetPower: ...,
	// ev3_getMotorPosition: ...,
	// ev3_whenButtonPressed: ...,
	// ev3_whenDistanceLessThan: ...,
	// ev3_whenBrightnessLessThan: ...,
	// ev3_buttonPressed: ...,
	// ev3_getDistance: ...,
	// ev3_getBrightness: ...,
	// ev3_beep: ...,
	// gdxfor_whenGesture: ...,
	// gdxfor_whenForcePushedOrPulled: ...,
	// gdxfor_getForce: ...,
	// gdxfor_whenTilted: ...,
	// gdxfor_isTilted: ...,
	// gdxfor_getTilt: ...,
	// gdxfor_isFreeFalling: ...,
	// gdxfor_getSpinSpeed: ...,
	// gdxfor_getAcceleration: ...,
	// makeymakey_whenMakeyKeyPressed: ...,
	// makeymakey_whenCodePressed: ...,
	// microbit_whenButtonPressed: ...,
	// microbit_isButtonPressed: ...,
	// microbit_whenGesture: ...,
	// microbit_displaySymbol: ...,
	// microbit_displayText: ...,
	// microbit_displayClear: ...,
	// microbit_whenTilted: ...,
	// microbit_isTilted: ...,
	// microbit_getTiltAngle: ...,
	// microbit_whenPinConnected: ...,
	// wedo2_motorOnFor: ...,
	// wedo2_motorOn: ...,
	// wedo2_motorOff: ...,
	// wedo2_startMotorPower: ...,
	// wedo2_setMotorDirection: ...,
	// wedo2_setLightHue: ...,
	// wedo2_playNoteFor: ...,
	// wedo2_whenDistance: ...,
	// wedo2_whenTilted: ...,
	// wedo2_getDistance: ...,
	// wedo2_isTilted: ...,
	// wedo2_getTiltAngle: ...,
}

let conversions1 = new Map([
	[core.WhenFlagClicked.type, ["event_whenflagclicked"]],
	[core.If.type, ["control_if", "CONDITION"]],
	[core.Forever.type, ["control_forever"]],
	[core.WaitUntil.type, ["control_wait_until", "CONDITION"]],
	[core.RepeatUntil.type, ["control_repeat_until", "CONDITION"]],
	[core.Repeat.type, ["control_repeat", "TIMES"]],
	[core.Stop.type, ["control_stop", ["STOP_OPTION", "this script"]]],
	[core.Add.type, ["operator_add", "NUM1", "NUM2"]],
	[core.Subtract.type, ["operator_subtract", "NUM1", "NUM2"]],
	[core.Multiply.type, ["operator_multiply", "NUM1", "NUM2"]],
	[core.Divide.type, ["operator_divide", "NUM1", "NUM2"]],
	[core.Modulo.type, ["operator_mod", "NUM1", "NUM2"]],
	[core.LessThan.type, ["operator_lt", "OPERAND1", "OPERAND2"]],
	[core.GreaterThan.type, ["operator_gt", "OPERAND1", "OPERAND2"]],
	[core.Equals.type, ["operator_equals", "OPERAND1", "OPERAND2"]],
	[core.And.type, ["operator_and", "OPERAND1", "OPERAND2"]],
	[core.Or.type, ["operator_or", "OPERAND1", "OPERAND2"]],
	[core.Not.type, ["operator_not", "OPERAND"]],
	[core.Join.type, ["operator_join", "STRING1", "STRING2"]],
	[core.TextIndex.type, ["operator_letter_of", "LETTER", "STRING"]],
	[core.TextLength.type, ["operator_length", "STRING"]],
	[core.TextContains.type, ["operator_contains", "STRING1", "STRING2"]],
	[core.SetVariable.type, ["data_setvariableto", "VALUE"]],
	[core.ChangeVariable.type, ["data_changevariableby", "VALUE"]],
	[core.Append.type, ["data_addtolist", "ITEM"]],
	[core.Delete.type, ["data_deleteoflist", "INDEX"]],
	[core.Clear.type, ["data_deletealloflist"]],
	[core.Insert.type, ["data_insertatlist", "INDEX", "ITEM"]],
	[core.Replace.type, ["data_replaceitemoflist", "INDEX", "ITEM"]],
	[core.Index.type, ["data_itemoflist", "INDEX"]],
	[core.Find.type, ["data_itemnumoflist", "ITEM"]],
	[core.Length.type, ["data_lengthoflist"]],
	[core.Contains.type, ["data_listcontainsitem", "ITEM"]],
])

let stageConversions1 = stage => new Map([
	[stage.scratch.Wait.type, ["control_wait", "DURATION"]],
	[stage.scratch.Timer.type, ["sensing_timer"]],
	[stage.scratch.ResetTimer.type, ["sensing_resettimer"]],
	[stage.scratch.Username.type, ["sensing_username"]],
	[stage.scratch.Round.type, ["operator_round", "NUM"]],
	[stage.scratch.DaysSince2000.type, ["sensing_dayssince2000"]],
	[stage.scratch.Abs.type, ["operator_mathop", "NUM", ["OPERATOR", "abs"]]],
	[stage.scratch.Ceiling.type, ["operator_mathop", "NUM", ["OPERATOR", "ceiling"]]],
	[stage.scratch.Floor.type, ["operator_mathop", "NUM", ["OPERATOR", "floor"]]],
	[stage.scratch.Sqrt.type, ["operator_mathop", "NUM", ["OPERATOR", "sqrt"]]],
	[stage.scratch.Sin.type, ["operator_mathop", "NUM", ["OPERATOR", "sin"]]],
	[stage.scratch.Cos.type, ["operator_mathop", "NUM", ["OPERATOR", "cos"]]],
	[stage.scratch.Tan.type, ["operator_mathop", "NUM", ["OPERATOR", "tan"]]],
	[stage.scratch.ArcSin.type, ["operator_mathop", "NUM", ["OPERATOR", "asin"]]],
	[stage.scratch.ArcCos.type, ["operator_mathop", "NUM", ["OPERATOR", "acos"]]],
	[stage.scratch.ArcTan.type, ["operator_mathop", "NUM", ["OPERATOR", "atan"]]],
	[stage.scratch.Ln.type, ["operator_mathop", "NUM", ["OPERATOR", "ln"]]],
	[stage.scratch.Log.type, ["operator_mathop", "NUM", ["OPERATOR", "log"]]],
	[stage.scratch.Exp.type, ["operator_mathop", "NUM", ["OPERATOR", "e ^"]]],
	[stage.scratch.Exp10.type, ["operator_mathop", "NUM", ["OPERATOR", "10 ^"]]],
	[stage.scratch.Year.type, ["sensing_current", ["CURRENTMENU", "YEAR"]]],
	[stage.scratch.Month.type, ["sensing_current", ["CURRENTMENU", "MONTH"]]],
	[stage.scratch.Day.type, ["sensing_current", ["CURRENTMENU", "DATE"]]],
	[stage.scratch.DayOfWeek.type, ["sensing_current", ["CURRENTMENU", "DAYOFWEEK"]]],
	[stage.scratch.Hour.type, ["sensing_current", ["CURRENTMENU", "HOUR"]]],
	[stage.scratch.Minute.type, ["sensing_current", ["CURRENTMENU", "MINUTE"]]],
	[stage.scratch.Second.type, ["sensing_current", ["CURRENTMENU", "SECOND"]]],
	[stage.scratch.PickRandom.type, ["operator_random", "FROM", "TO"]],
	[stage.scratch.StopOther.type, ["control_stop", ["STOP_OPTION", "other scripts in sprite"]]],
	[stage.scratch.StopAll.type, ["control_stop", ["STOP_OPTION", "all"]]],
	[stage.Broadcast.type, ["event_broadcast", ["BROADCAST_INPUT", "event_broadcast_menu", "BROADCAST_OPTION"]]],
	[stage.BroadcastAndWait.type, ["event_broadcastandwait", ["BROADCAST_INPUT", "event_broadcast_menu", "BROADCAST_OPTION"]]],
	[stage.MouseDown.type, ["sensing_mousedown"]],
	[stage.getMouseX.type, ["sensing_mousex"]],
	[stage.getMouseY.type, ["sensing_mousey"]],
	[stage.SpriteX.type, ["sensing_of", ["PROPERTY", "x position"], ["OBJECT", "sensing_of_object_menu", "OBJECT"]]],
	[stage.SpriteY.type, ["sensing_of", ["PROPERTY", "y position"], ["OBJECT", "sensing_of_object_menu", "OBJECT"]]],
	[stage.SpriteDirection.type,  ["sensing_of", ["PROPERTY", "direction"], ["OBJECT", "sensing_of_object_menu", "OBJECT"]]],
	[stage.SpriteSize.type, ["sensing_of", ["PROPERTY", "size"], ["OBJECT", "sensing_of_object_menu", "OBJECT"]]],
	[stage.SpriteCostumeName.type, ["sensing_of", ["PROPERTY", "costume name"], ["OBJECT", "sensing_of_object_menu", "OBJECT"]]],
	[stage.SpriteCostumeNumber.type, ["sensing_of", ["PROPERTY", "costume #"], ["OBJECT", "sensing_of_object_menu", "OBJECT"]]],
])

let spriteConversions = sprite => new Map([
	...stageConversions1(sprite.stage),
	[sprite.Move.type, ["motion_movesteps", "STEPS"]],
	[sprite.RotateRight.type, ["motion_turnleft", "DEGREES"]],
	[sprite.RotateLeft.type, ["motion_turnright", "DEGREES"]],
	[sprite.GotoXY.type, ["motion_gotoxy", "X", "Y"]],
	[sprite.SetDirection.type, ["motion_pointindirection", "DIRECTION"]],
	[sprite.SetX.type, ["motion_setx", "X"]],
	[sprite.SetY.type, ["motion_sety", "Y"]],
	[sprite.ChangeX.type, ["motion_changexby", "DX"]],
	[sprite.ChangeY.type, ["motion_changeyby", "DY"]],
	[sprite.SetSize.type, ["looks_setsizeto", "SIZE"]],
	[sprite.ChangeSize.type, ["looks_changesizeby", "CHANGE"]],
	[sprite.GetX.type, ["motion_xposition"]],
	[sprite.GetY.type, ["motion_yposition"]],
	[sprite.GetDirection.type, ["motion_direction"]],
	[sprite.GetSize.type, ["looks_size"]],
	[sprite.Show.type, ["looks_show"]],
	[sprite.Hide.type, ["looks_hide"]],
	[sprite.SetCostume.type, ["looks_switchcostumeto", ["COSTUME", "looks_costume", "COSTUME"]]],
	[sprite.NextCostume.type, ["looks_nextcostume"]],
	[sprite.CostumeName.type, ["looks_costumenumbername", ["NUMBER_NAME", "name"]]],
	[sprite.CostumeNumber.type, ["looks_costumenumbername", ["NUMBER_NAME", "number"]]],
	[sprite.GotoFront.type, ["looks_gotofrontback", ["FRONT_BACK", "front"]]],
	[sprite.GotoBack.type, ["looks_gotofrontback", ["FRONT_BACK", "back"]]],
	[sprite.GoForward.type, ["looks_goforwardbackwardlayers", ["FORWARD_BACKWARD", "forward"]]],
	[sprite.GoBackward.type, ["looks_goforwardbackwardlayers", ["FORWARD_BACKWARD", "backward"]]],
	[sprite.TouchingMouse.type, ["sensing_touchingobject", ["TOUCHINGOBJECTMENU", "sensing_touchingobjectmenu", "TOUCHINGOBJECTMENU", "_mouse_"]]],
	[sprite.SetEffect.type, ["looks_seteffectto", "EFFECT:u", "VALUE"]],
	[sprite.ChangeEffect.type, ["looks_changeeffectby", "EFFECT:u", "CHANGE"]],
	[sprite.ClearEffects.type, ["looks_cleargraphiceffects"]],
	[sprite.EnableRotation.type, ["motion_setrotationstyle", ["STYLE", "all around"]]],
	[sprite.EnableFlipping.type, ["motion_setrotationstyle", ["STYLE", "left-right"]]],
	[sprite.DisableRotation.type, ["motion_setrotationstyle", ["STYLE", "don't rotate"]]],
	[sprite.Say.type, ["looks_say", "MESSAGE"]],
	[sprite.Think.type, ["looks_think", "MESSAGE"]],
	[sprite.SayFor.type, ["looks_sayforsecs", "MESSAGE", "SECS"]],
	[sprite.ThinkFor.type, ["looks_thinkforsecs", "MESSAGE", "SECS"]],
	[sprite.DistanceToSprite.type, ["sensing_distanceto", ["DISTANCETOMENU", "sensing_distancetomenu", "DISTANCETOMENU"]]],
	[sprite.Clone.type, ["control_create_clone_of", ["CLONE_OPTION", "control_create_clone_of_menu", "CLONE_OPTION", "_myself_"]]],
	[sprite.WhenCloned.type, ["control_start_as_clone"]],
	[sprite.extensions.pen?.blocks.Clear.type, ["pen_clear"]],
	[sprite.extensions.pen?.blocks.PenDown.type, ["pen_penDown"]],
	[sprite.extensions.pen?.blocks.PenUp.type, ["pen_penUp"]],
	[sprite.extensions.pen?.blocks.SetPenColor.type, ["pen_setPenColorToColor", "COLOR"]],
	[sprite.extensions.pen?.blocks.SetPenSize.type, ["pen_setPenSizeTo", "SIZE"]],
	[sprite.extensions.pen?.blocks.Stamp.type, ["pen_stamp"]],
])