moonscratch 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/index.d.mts +1173 -0
- package/dist/index.mjs +27135 -0
- package/package.json +6 -1
- package/.agents/skills/moonbit-agent-guide/LICENSE +0 -202
- package/.agents/skills/moonbit-agent-guide/SKILL.mbt.md +0 -1126
- package/.agents/skills/moonbit-agent-guide/SKILL.md +0 -1126
- package/.agents/skills/moonbit-agent-guide/ide.md +0 -116
- package/.agents/skills/moonbit-agent-guide/references/advanced-moonbit-build.md +0 -106
- package/.agents/skills/moonbit-agent-guide/references/moonbit-language-fundamentals.mbt.md +0 -422
- package/.agents/skills/moonbit-agent-guide/references/moonbit-language-fundamentals.md +0 -422
- package/.agents/skills/moonbit-practice/SKILL.md +0 -258
- package/.agents/skills/moonbit-practice/assets/ci.yaml +0 -25
- package/.agents/skills/moonbit-practice/reference/agents.md +0 -1469
- package/.agents/skills/moonbit-practice/reference/configuration.md +0 -228
- package/.agents/skills/moonbit-practice/reference/ffi.md +0 -229
- package/.agents/skills/moonbit-practice/reference/ide.md +0 -189
- package/.agents/skills/moonbit-practice/reference/performance.md +0 -217
- package/.agents/skills/moonbit-practice/reference/refactor.md +0 -154
- package/.agents/skills/moonbit-practice/reference/stdlib.md +0 -351
- package/.agents/skills/moonbit-practice/reference/testing.md +0 -228
- package/.agents/skills/moonbit-refactoring/LICENSE +0 -21
- package/.agents/skills/moonbit-refactoring/SKILL.md +0 -323
- package/.githooks/README.md +0 -23
- package/.githooks/pre-commit +0 -3
- package/.github/workflows/copilot-setup-steps.yml +0 -40
- package/AGENTS.md +0 -91
- package/PLAN.md +0 -64
- package/TODO.md +0 -120
- package/benchmarks/calc.bench.ts +0 -144
- package/benchmarks/draw.bench.ts +0 -215
- package/benchmarks/load.bench.ts +0 -28
- package/benchmarks/render.bench.ts +0 -53
- package/benchmarks/run.bench.ts +0 -8
- package/benchmarks/types.d.ts +0 -15
- package/docs/scratch-vm-specs/eventloop.md +0 -103
- package/docs/scratch-vm-specs/moonscratch-time-separation.md +0 -50
- package/index.html +0 -91
- package/js/AGENTS.md +0 -5
- package/js/a.ts +0 -52
- package/js/assets/AGENTS.md +0 -5
- package/js/assets/base64.test.ts +0 -14
- package/js/assets/base64.ts +0 -21
- package/js/assets/build-asset.test.ts +0 -26
- package/js/assets/build-asset.ts +0 -28
- package/js/assets/create.test.ts +0 -142
- package/js/assets/create.ts +0 -122
- package/js/assets/index.test.ts +0 -15
- package/js/assets/index.ts +0 -2
- package/js/assets/types.ts +0 -26
- package/js/assets/validation.test.ts +0 -34
- package/js/assets/validation.ts +0 -25
- package/js/assets.test.ts +0 -14
- package/js/assets.ts +0 -1
- package/js/index.test.ts +0 -26
- package/js/index.ts +0 -3
- package/js/render/index.test.ts +0 -65
- package/js/render/index.ts +0 -13
- package/js/render/sharp.ts +0 -87
- package/js/render/svg.ts +0 -68
- package/js/render/types.ts +0 -35
- package/js/render/utils.ts +0 -108
- package/js/render/webgl.ts +0 -274
- package/js/sharp-optional.d.ts +0 -16
- package/js/test/helpers.ts +0 -116
- package/js/test/hikkaku-sample.test.ts +0 -37
- package/js/test/rubik-components.input-motion.test.ts +0 -60
- package/js/test/rubik-components.lists.test.ts +0 -49
- package/js/test/rubik-components.operators.test.ts +0 -104
- package/js/test/rubik-components.pen.test.ts +0 -112
- package/js/test/rubik-components.procedures-loops.test.ts +0 -72
- package/js/test/rubik-components.variables-branches.test.ts +0 -57
- package/js/test/rubik-components.visibility-entry.test.ts +0 -31
- package/js/test/test-projects.ts +0 -598
- package/js/test/variable.ts +0 -200
- package/js/test/warp.test.ts +0 -59
- package/js/vm/AGENTS.md +0 -6
- package/js/vm/README.md +0 -183
- package/js/vm/bindings.test.ts +0 -13
- package/js/vm/bindings.ts +0 -5
- package/js/vm/compare-operators.test.ts +0 -145
- package/js/vm/constants.test.ts +0 -11
- package/js/vm/constants.ts +0 -4
- package/js/vm/effect-guards.test.ts +0 -68
- package/js/vm/effect-guards.ts +0 -44
- package/js/vm/factory.test.ts +0 -486
- package/js/vm/factory.ts +0 -615
- package/js/vm/headless-vm.test.ts +0 -131
- package/js/vm/headless-vm.ts +0 -342
- package/js/vm/index.test.ts +0 -28
- package/js/vm/index.ts +0 -5
- package/js/vm/internal-types.ts +0 -32
- package/js/vm/json.test.ts +0 -40
- package/js/vm/json.ts +0 -273
- package/js/vm/normalize.test.ts +0 -48
- package/js/vm/normalize.ts +0 -65
- package/js/vm/options.test.ts +0 -30
- package/js/vm/options.ts +0 -55
- package/js/vm/pen-transparency.test.ts +0 -115
- package/js/vm/program-wasm.ts +0 -322
- package/js/vm/scheduler-render.test.ts +0 -401
- package/js/vm/scratch-assets.test.ts +0 -136
- package/js/vm/scratch-assets.ts +0 -202
- package/js/vm/types.ts +0 -358
- package/js/vm/value-guards.test.ts +0 -25
- package/js/vm/value-guards.ts +0 -18
- package/moon.mod.json +0 -10
- package/scripts/preinstall.ts +0 -4
- package/src/AGENTS.md +0 -6
- package/src/api.mbt +0 -161
- package/src/api_aot_commands.mbt +0 -184
- package/src/api_effects_json.mbt +0 -72
- package/src/api_options.mbt +0 -60
- package/src/api_program_wasm.mbt +0 -1647
- package/src/api_program_wat.mbt +0 -2206
- package/src/api_snapshot_json.mbt +0 -44
- package/src/cmd/AGENTS.md +0 -5
- package/src/cmd/main/AGENTS.md +0 -5
- package/src/cmd/main/main.mbt +0 -29
- package/src/cmd/main/moon.pkg +0 -7
- package/src/cmd/main/pkg.generated.mbti +0 -13
- package/src/json_helpers.mbt +0 -176
- package/src/moon.pkg +0 -65
- package/src/moonscratch.mbt +0 -3
- package/src/moonscratch_wbtest.mbt +0 -40
- package/src/parser_sb3.mbt +0 -890
- package/src/pkg.generated.mbti +0 -479
- package/src/runtime_eval.mbt +0 -2844
- package/src/runtime_exec.mbt +0 -3850
- package/src/runtime_render.mbt +0 -2550
- package/src/runtime_state.mbt +0 -870
- package/src/test/AGENTS.md +0 -3
- package/src/test/projects/AGENTS.md +0 -6
- package/src/test/projects/moon.pkg +0 -4
- package/src/test/projects/moonscratch_compat_test.mbt +0 -642
- package/src/test/projects/moonscratch_core_test.mbt +0 -1332
- package/src/test/projects/moonscratch_runtime_test.mbt +0 -1087
- package/src/test/projects/pkg.generated.mbti +0 -13
- package/src/test/projects/test_support.mbt +0 -35
- package/src/types_effects.mbt +0 -20
- package/src/types_error.mbt +0 -4
- package/src/types_options.mbt +0 -31
- package/src/types_runtime_structs.mbt +0 -254
- package/src/types_vm.mbt +0 -109
- package/tsconfig.json +0 -29
- package/viewer/index.ts +0 -399
- package/viewer/vite.d.ts +0 -1
- package/viewer/worker.ts +0 -161
- package/vite.config.ts +0 -61
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test, vi } from 'vite-plus/test'
|
|
2
|
-
import { renderWithSVG } from '../render/index.ts'
|
|
3
|
-
import {
|
|
4
|
-
EXAMPLE_PROJECT,
|
|
5
|
-
getStageVariables,
|
|
6
|
-
INPUT_EVENT_PROJECT,
|
|
7
|
-
stepMany,
|
|
8
|
-
TEXT_TO_SPEECH_TRANSLATE_PROJECT,
|
|
9
|
-
} from '../test/test-projects.ts'
|
|
10
|
-
import { createHeadlessVM, createProgramModuleFromProject } from './factory.ts'
|
|
11
|
-
import type { CreateHeadlessVMOptions, ProjectJson } from './types.ts'
|
|
12
|
-
|
|
13
|
-
describe('moonscratch/js/vm/headless-vm.ts', () => {
|
|
14
|
-
const createVm = (
|
|
15
|
-
projectJson: ProjectJson,
|
|
16
|
-
overrides: Omit<CreateHeadlessVMOptions, 'program'> = {},
|
|
17
|
-
) => {
|
|
18
|
-
const program = createProgramModuleFromProject({ projectJson })
|
|
19
|
-
return createHeadlessVM({
|
|
20
|
-
program,
|
|
21
|
-
...overrides,
|
|
22
|
-
})
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
test('runs project and normalizes frame values', () => {
|
|
26
|
-
const vm = createVm(EXAMPLE_PROJECT, {
|
|
27
|
-
initialNowMs: 0,
|
|
28
|
-
})
|
|
29
|
-
vm.greenFlag()
|
|
30
|
-
expect(getStageVariables(vm).var_score).toBe(42)
|
|
31
|
-
vm.setTime(17)
|
|
32
|
-
|
|
33
|
-
const first = vm.stepFrame()
|
|
34
|
-
const second = vm.stepFrame()
|
|
35
|
-
|
|
36
|
-
expect(first).toEqual({
|
|
37
|
-
activeThreads: 0,
|
|
38
|
-
ticks: 0,
|
|
39
|
-
ops: 0,
|
|
40
|
-
emittedEffects: 0,
|
|
41
|
-
stopReason: 'finished',
|
|
42
|
-
shouldRender: true,
|
|
43
|
-
isInWarp: false,
|
|
44
|
-
})
|
|
45
|
-
expect(second.stopReason).toBe('finished')
|
|
46
|
-
expect(getStageVariables(vm).var_score).toBe(42)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
test('renders current scene with renderFrame and renderWithSVG', () => {
|
|
50
|
-
const vm = createVm(EXAMPLE_PROJECT)
|
|
51
|
-
const frame = vm.renderFrame()
|
|
52
|
-
const svg = renderWithSVG(frame)
|
|
53
|
-
|
|
54
|
-
expect(frame.width).toBeGreaterThan(0)
|
|
55
|
-
expect(frame.height).toBeGreaterThan(0)
|
|
56
|
-
expect(frame.pixels.length).toBe(frame.width * frame.height * 4)
|
|
57
|
-
expect(svg).toContain('<svg')
|
|
58
|
-
expect(svg).toContain('shape-rendering="crispEdges"')
|
|
59
|
-
expect(svg).toContain('fill="rgb(255,255,255)"')
|
|
60
|
-
expect(svg).toContain('</svg>')
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
test('detects click and key hats from input state', () => {
|
|
64
|
-
const vm = createVm(INPUT_EVENT_PROJECT, {
|
|
65
|
-
initialNowMs: 0,
|
|
66
|
-
})
|
|
67
|
-
vm.greenFlag()
|
|
68
|
-
vm.setMouseState({
|
|
69
|
-
x: 0,
|
|
70
|
-
y: 0,
|
|
71
|
-
isDown: true,
|
|
72
|
-
})
|
|
73
|
-
vm.setMouseTargets({
|
|
74
|
-
stage: true,
|
|
75
|
-
targets: ['Sprite1'],
|
|
76
|
-
})
|
|
77
|
-
vm.setKeysDown(['space'])
|
|
78
|
-
stepMany(vm, 12)
|
|
79
|
-
|
|
80
|
-
expect(getStageVariables(vm).var_stage_click).toBe(1)
|
|
81
|
-
expect(getStageVariables(vm).var_sprite_click).toBe(1)
|
|
82
|
-
expect(getStageVariables(vm).var_key).toBe(1)
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
test('handleEffects dispatches handlers and caches translated text for next run', async () => {
|
|
86
|
-
const vm = createVm(TEXT_TO_SPEECH_TRANSLATE_PROJECT, {
|
|
87
|
-
viewerLanguage: 'ja',
|
|
88
|
-
})
|
|
89
|
-
const translate = vi.fn(async () => 'こんにちは')
|
|
90
|
-
const textToSpeech = vi.fn(async () => undefined)
|
|
91
|
-
const effect = vi.fn(async () => undefined)
|
|
92
|
-
|
|
93
|
-
vm.greenFlag()
|
|
94
|
-
stepMany(vm, 6)
|
|
95
|
-
|
|
96
|
-
const handled = await vm.handleEffects({ translate, textToSpeech, effect })
|
|
97
|
-
expect(handled.map((item) => item.type)).toEqual([
|
|
98
|
-
'translate_request',
|
|
99
|
-
'text_to_speech',
|
|
100
|
-
])
|
|
101
|
-
expect(translate).toHaveBeenCalledTimes(1)
|
|
102
|
-
expect(textToSpeech).toHaveBeenCalledTimes(1)
|
|
103
|
-
expect(effect).toHaveBeenCalledTimes(2)
|
|
104
|
-
|
|
105
|
-
stepMany(vm, 10)
|
|
106
|
-
expect(getStageVariables(vm).var_done).toBe(1)
|
|
107
|
-
expect(getStageVariables(vm).var_trans).toBe('hello')
|
|
108
|
-
|
|
109
|
-
vm.greenFlag()
|
|
110
|
-
stepMany(vm, 6)
|
|
111
|
-
|
|
112
|
-
const secondRunEffects = vm.takeEffects()
|
|
113
|
-
expect(
|
|
114
|
-
secondRunEffects.some((item) => item.type === 'translate_request'),
|
|
115
|
-
).toBe(false)
|
|
116
|
-
const secondRunTts = secondRunEffects.find(
|
|
117
|
-
(item): item is { type: 'text_to_speech'; waitKey: string } =>
|
|
118
|
-
item.type === 'text_to_speech' &&
|
|
119
|
-
typeof (item as { waitKey?: unknown }).waitKey === 'string',
|
|
120
|
-
)
|
|
121
|
-
expect(secondRunTts).toBeDefined()
|
|
122
|
-
if (!secondRunTts) {
|
|
123
|
-
throw new Error('text_to_speech effect was not found in second run')
|
|
124
|
-
}
|
|
125
|
-
vm.ackTextToSpeech(secondRunTts.waitKey)
|
|
126
|
-
stepMany(vm, 10)
|
|
127
|
-
|
|
128
|
-
expect(getStageVariables(vm).var_done).toBe(1)
|
|
129
|
-
expect(getStageVariables(vm).var_trans).toBe('こんにちは')
|
|
130
|
-
})
|
|
131
|
-
})
|
package/js/vm/headless-vm.ts
DELETED
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
import { moonscratch } from './bindings.ts'
|
|
2
|
-
import { DEFAULT_MAX_FRAMES } from './constants.ts'
|
|
3
|
-
import {
|
|
4
|
-
isMusicPlayDrumEffect,
|
|
5
|
-
isMusicPlayNoteEffect,
|
|
6
|
-
isTextToSpeechEffect,
|
|
7
|
-
isTranslateRequestEffect,
|
|
8
|
-
} from './effect-guards.ts'
|
|
9
|
-
import { parseJson } from './json.ts'
|
|
10
|
-
import {
|
|
11
|
-
cloneTranslateCache,
|
|
12
|
-
normalizeLanguage,
|
|
13
|
-
normalizeMaxFrames,
|
|
14
|
-
normalizeNowMs,
|
|
15
|
-
toFrameReport,
|
|
16
|
-
} from './normalize.ts'
|
|
17
|
-
import type {
|
|
18
|
-
EffectHandlers,
|
|
19
|
-
FrameReport,
|
|
20
|
-
JsonValue,
|
|
21
|
-
RenderFrame,
|
|
22
|
-
RunReport,
|
|
23
|
-
RunUntilIdleOptions,
|
|
24
|
-
TranslateCache,
|
|
25
|
-
VMEffect,
|
|
26
|
-
VMInputEvent,
|
|
27
|
-
VMSnapshot,
|
|
28
|
-
} from './types.ts'
|
|
29
|
-
|
|
30
|
-
type BoundWasmVmHandle = unknown
|
|
31
|
-
type BoundMoonscratch = {
|
|
32
|
-
vm_set_time?: (vmHandle: BoundWasmVmHandle, nowMs: number) => void
|
|
33
|
-
vm_render_frame?: (vmHandle: BoundWasmVmHandle) => unknown
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface HeadlessVMHooks {
|
|
37
|
-
afterGreenFlag?: () => void
|
|
38
|
-
beforeStepFrame?: () => void
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export class HeadlessVM {
|
|
42
|
-
private readonly vmHandle: unknown
|
|
43
|
-
private readonly hooks: HeadlessVMHooks
|
|
44
|
-
private translateCache: TranslateCache = {}
|
|
45
|
-
|
|
46
|
-
constructor(vmHandle: unknown, hooks: HeadlessVMHooks = {}) {
|
|
47
|
-
this.vmHandle = vmHandle
|
|
48
|
-
this.hooks = hooks
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
get raw(): unknown {
|
|
52
|
-
return this.vmHandle
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
start(): void {
|
|
56
|
-
moonscratch.vm_start(this.vmHandle)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
greenFlag(): void {
|
|
60
|
-
moonscratch.vm_green_flag(this.vmHandle)
|
|
61
|
-
this.hooks.afterGreenFlag?.()
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
stepFrame(): FrameReport {
|
|
65
|
-
this.hooks.beforeStepFrame?.()
|
|
66
|
-
const raw = moonscratch.vm_step_frame(this.vmHandle)
|
|
67
|
-
return toFrameReport(raw)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
runFrames(frameCount: number): RunReport {
|
|
71
|
-
const normalizedFrameCount = normalizeMaxFrames(frameCount)
|
|
72
|
-
let frames = 0
|
|
73
|
-
let ticks = 0
|
|
74
|
-
let ops = 0
|
|
75
|
-
let activeThreads = this.snapshot().activeThreads
|
|
76
|
-
while (activeThreads > 0 && frames < normalizedFrameCount) {
|
|
77
|
-
const frame = this.stepFrame()
|
|
78
|
-
frames += 1
|
|
79
|
-
ticks += frame.ticks
|
|
80
|
-
ops += frame.ops
|
|
81
|
-
activeThreads = frame.activeThreads
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
frames,
|
|
85
|
-
ticks,
|
|
86
|
-
ops,
|
|
87
|
-
activeThreads,
|
|
88
|
-
endedBy: 'frame_limit',
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
runUntilIdle(options: RunUntilIdleOptions = {}): RunReport {
|
|
93
|
-
const normalizedMaxFrames = normalizeMaxFrames(
|
|
94
|
-
options.maxFrames ?? DEFAULT_MAX_FRAMES,
|
|
95
|
-
)
|
|
96
|
-
let frames = 0
|
|
97
|
-
let ticks = 0
|
|
98
|
-
let ops = 0
|
|
99
|
-
let activeThreads = this.snapshot().activeThreads
|
|
100
|
-
while (activeThreads > 0 && frames < normalizedMaxFrames) {
|
|
101
|
-
const frame = this.stepFrame()
|
|
102
|
-
frames += 1
|
|
103
|
-
ticks += frame.ticks
|
|
104
|
-
ops += frame.ops
|
|
105
|
-
activeThreads = frame.activeThreads
|
|
106
|
-
}
|
|
107
|
-
return {
|
|
108
|
-
frames,
|
|
109
|
-
ticks,
|
|
110
|
-
ops,
|
|
111
|
-
activeThreads,
|
|
112
|
-
endedBy: activeThreads === 0 ? 'idle' : 'frame_limit',
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
postIO(device: string, payload: JsonValue): void {
|
|
117
|
-
moonscratch.vm_post_io_json(this.vmHandle, device, JSON.stringify(payload))
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
postIORawJson(device: string, payloadJson: string): void {
|
|
121
|
-
moonscratch.vm_post_io_json(this.vmHandle, device, payloadJson)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
setAnswer(answer: string): void {
|
|
125
|
-
this.dispatchInputEvent({
|
|
126
|
-
type: 'answer',
|
|
127
|
-
answer,
|
|
128
|
-
})
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
setMouseState(input: { x: number; y: number; isDown?: boolean }): void {
|
|
132
|
-
this.dispatchInputEvent({
|
|
133
|
-
type: 'mouse',
|
|
134
|
-
x: input.x,
|
|
135
|
-
y: input.y,
|
|
136
|
-
isDown: input.isDown ?? false,
|
|
137
|
-
})
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
setKeysDown(keys: string[]): void {
|
|
141
|
-
this.dispatchInputEvent({
|
|
142
|
-
type: 'keys_down',
|
|
143
|
-
keys,
|
|
144
|
-
})
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
setTouching(touching: Record<string, string[]>): void {
|
|
148
|
-
this.dispatchInputEvent({
|
|
149
|
-
type: 'touching',
|
|
150
|
-
touching,
|
|
151
|
-
})
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
setMouseTargets(input: {
|
|
155
|
-
stage?: boolean
|
|
156
|
-
target?: string
|
|
157
|
-
targets?: string[]
|
|
158
|
-
}): void {
|
|
159
|
-
this.dispatchInputEvent({
|
|
160
|
-
type: 'mouse_targets',
|
|
161
|
-
stage: input.stage,
|
|
162
|
-
target: input.target,
|
|
163
|
-
targets: input.targets,
|
|
164
|
-
})
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
setBackdrop(backdrop: string | string[]): void {
|
|
168
|
-
this.dispatchInputEvent({
|
|
169
|
-
type: 'backdrop',
|
|
170
|
-
backdrop,
|
|
171
|
-
})
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
dispatchInputEvents(events: VMInputEvent[]): void {
|
|
175
|
-
for (const event of events) {
|
|
176
|
-
this.dispatchInputEvent(event)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
dispatchInputEvent(event: VMInputEvent): void {
|
|
181
|
-
switch (event.type) {
|
|
182
|
-
case 'answer':
|
|
183
|
-
this.postIO('answer', event.answer)
|
|
184
|
-
return
|
|
185
|
-
case 'mouse':
|
|
186
|
-
this.postIO('mouse', {
|
|
187
|
-
x: event.x,
|
|
188
|
-
y: event.y,
|
|
189
|
-
isDown: event.isDown ?? false,
|
|
190
|
-
})
|
|
191
|
-
return
|
|
192
|
-
case 'keys_down':
|
|
193
|
-
this.postIO('keys_down', event.keys)
|
|
194
|
-
return
|
|
195
|
-
case 'touching':
|
|
196
|
-
this.postIO('touching', event.touching)
|
|
197
|
-
return
|
|
198
|
-
case 'mouse_targets':
|
|
199
|
-
this.postIO('mouse_targets', {
|
|
200
|
-
stage: event.stage ?? false,
|
|
201
|
-
target: event.target ?? '',
|
|
202
|
-
targets: event.targets ?? [],
|
|
203
|
-
})
|
|
204
|
-
return
|
|
205
|
-
case 'backdrop':
|
|
206
|
-
if (Array.isArray(event.backdrop)) {
|
|
207
|
-
this.postIO('backdrop', {
|
|
208
|
-
backdrops: event.backdrop,
|
|
209
|
-
})
|
|
210
|
-
return
|
|
211
|
-
}
|
|
212
|
-
this.postIO('backdrop', {
|
|
213
|
-
backdrop: event.backdrop,
|
|
214
|
-
})
|
|
215
|
-
return
|
|
216
|
-
default: {
|
|
217
|
-
const unreachable: never = event
|
|
218
|
-
throw new Error(`Unknown input event: ${String(unreachable)}`)
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
broadcast(message: string): void {
|
|
224
|
-
moonscratch.vm_broadcast(this.vmHandle, message)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
stopAll(): void {
|
|
228
|
-
moonscratch.vm_stop_all(this.vmHandle)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
setTime(nowMs: number): void {
|
|
232
|
-
const binding = moonscratch as unknown as BoundMoonscratch
|
|
233
|
-
if (typeof binding.vm_set_time === 'function') {
|
|
234
|
-
binding.vm_set_time(this.vmHandle, normalizeNowMs(nowMs))
|
|
235
|
-
return
|
|
236
|
-
}
|
|
237
|
-
throw new Error(
|
|
238
|
-
'vm_set_time is unavailable in this build. Please rebuild moonscratch JS bindings.',
|
|
239
|
-
)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
takeEffects(): VMEffect[] {
|
|
243
|
-
const effects = parseJson<unknown>(
|
|
244
|
-
moonscratch.vm_take_effects_json(this.vmHandle),
|
|
245
|
-
'vm_take_effects_json',
|
|
246
|
-
)
|
|
247
|
-
if (!Array.isArray(effects)) {
|
|
248
|
-
throw new Error('vm_take_effects_json: expected JSON array')
|
|
249
|
-
}
|
|
250
|
-
return effects as VMEffect[]
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
snapshot(): VMSnapshot {
|
|
254
|
-
const snapshot = parseJson<unknown>(
|
|
255
|
-
moonscratch.vm_snapshot_json(this.vmHandle),
|
|
256
|
-
'vm_snapshot_json',
|
|
257
|
-
)
|
|
258
|
-
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
|
|
259
|
-
throw new Error('vm_snapshot_json: expected JSON object')
|
|
260
|
-
}
|
|
261
|
-
return snapshot as VMSnapshot
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
snapshotJson(): string {
|
|
265
|
-
return moonscratch.vm_snapshot_json(this.vmHandle)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
renderFrame(): RenderFrame {
|
|
269
|
-
const binding = moonscratch as unknown as BoundMoonscratch
|
|
270
|
-
if (typeof binding.vm_render_frame !== 'function') {
|
|
271
|
-
throw new Error(
|
|
272
|
-
'vm_render_frame is unavailable in this build. Please rebuild moonscratch JS bindings.',
|
|
273
|
-
)
|
|
274
|
-
}
|
|
275
|
-
return binding.vm_render_frame(this.vmHandle) as RenderFrame
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
setViewerLanguage(language: string): void {
|
|
279
|
-
this.postIO('viewer_language', normalizeLanguage(language))
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
setTranslateResult(
|
|
283
|
-
words: string,
|
|
284
|
-
language: string,
|
|
285
|
-
translated: string,
|
|
286
|
-
): string {
|
|
287
|
-
const normalizedLanguage = normalizeLanguage(language)
|
|
288
|
-
if (!this.translateCache[normalizedLanguage]) {
|
|
289
|
-
this.translateCache[normalizedLanguage] = {}
|
|
290
|
-
}
|
|
291
|
-
this.translateCache[normalizedLanguage][String(words)] = String(translated)
|
|
292
|
-
this.syncTranslateCache()
|
|
293
|
-
return translated
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
setTranslateCache(cache: TranslateCache): void {
|
|
297
|
-
this.translateCache = cloneTranslateCache(cache)
|
|
298
|
-
this.syncTranslateCache()
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
clearTranslateCache(): void {
|
|
302
|
-
this.translateCache = {}
|
|
303
|
-
this.syncTranslateCache()
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
ackTextToSpeech(waitKey: string | null | undefined): void {
|
|
307
|
-
if (typeof waitKey !== 'string' || waitKey.length === 0) {
|
|
308
|
-
return
|
|
309
|
-
}
|
|
310
|
-
this.postIO(waitKey, true)
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
async handleEffects(handlers: EffectHandlers = {}): Promise<VMEffect[]> {
|
|
314
|
-
const effects = this.takeEffects()
|
|
315
|
-
for (const effect of effects) {
|
|
316
|
-
if (isTranslateRequestEffect(effect) && handlers.translate) {
|
|
317
|
-
const translated = await handlers.translate(effect)
|
|
318
|
-
if (typeof translated === 'string') {
|
|
319
|
-
this.setTranslateResult(effect.words, effect.language, translated)
|
|
320
|
-
}
|
|
321
|
-
} else if (isTextToSpeechEffect(effect)) {
|
|
322
|
-
if (handlers.textToSpeech) {
|
|
323
|
-
await handlers.textToSpeech(effect)
|
|
324
|
-
}
|
|
325
|
-
this.ackTextToSpeech(effect.waitKey)
|
|
326
|
-
} else if (isMusicPlayNoteEffect(effect) && handlers.musicNote) {
|
|
327
|
-
await handlers.musicNote(effect)
|
|
328
|
-
} else if (isMusicPlayDrumEffect(effect) && handlers.musicDrum) {
|
|
329
|
-
await handlers.musicDrum(effect)
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (handlers.effect) {
|
|
333
|
-
await handlers.effect(effect)
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
return effects
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
private syncTranslateCache(): void {
|
|
340
|
-
this.postIO('translate_cache', this.translateCache)
|
|
341
|
-
}
|
|
342
|
-
}
|
package/js/vm/index.test.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'vite-plus/test'
|
|
2
|
-
|
|
3
|
-
import * as factory from './factory.ts'
|
|
4
|
-
import * as headless from './headless-vm.ts'
|
|
5
|
-
import * as index from './index.ts'
|
|
6
|
-
import * as scratchAssets from './scratch-assets.ts'
|
|
7
|
-
|
|
8
|
-
describe('moonscratch/js/vm/index.ts', () => {
|
|
9
|
-
test('re-exports vm runtime APIs', () => {
|
|
10
|
-
expect(index.createHeadlessVM).toBe(factory.createHeadlessVM)
|
|
11
|
-
expect(index.createHeadlessVMFromProject).toBe(
|
|
12
|
-
factory.createHeadlessVMFromProject,
|
|
13
|
-
)
|
|
14
|
-
expect(index.createHeadlessVMWithScratchAssets).toBe(
|
|
15
|
-
factory.createHeadlessVMWithScratchAssets,
|
|
16
|
-
)
|
|
17
|
-
expect(index.createVM).toBe(factory.createVM)
|
|
18
|
-
expect(index.createVMFromProject).toBe(factory.createVMFromProject)
|
|
19
|
-
expect(index.createVMWithScratchAssets).toBe(
|
|
20
|
-
factory.createVMWithScratchAssets,
|
|
21
|
-
)
|
|
22
|
-
expect(index.HeadlessVM).toBe(headless.HeadlessVM)
|
|
23
|
-
expect(index.resolveMissingScratchAssets).toBe(
|
|
24
|
-
scratchAssets.resolveMissingScratchAssets,
|
|
25
|
-
)
|
|
26
|
-
expect(index.moonscratch).toBe(factory.moonscratch)
|
|
27
|
-
})
|
|
28
|
-
})
|
package/js/vm/index.ts
DELETED
package/js/vm/internal-types.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export interface RawVMOptions {
|
|
2
|
-
turbo?: boolean
|
|
3
|
-
compatibility_30tps?: boolean
|
|
4
|
-
max_clones?: number
|
|
5
|
-
deterministic?: boolean
|
|
6
|
-
seed?: number
|
|
7
|
-
pen_width?: number
|
|
8
|
-
pen_height?: number
|
|
9
|
-
step_timeout_ticks?: number
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface RawFrameReport {
|
|
13
|
-
active_threads: number
|
|
14
|
-
tick_count: number
|
|
15
|
-
op_count: number
|
|
16
|
-
emitted_effects: number
|
|
17
|
-
stop_reason: string
|
|
18
|
-
should_render: boolean
|
|
19
|
-
is_in_warp: boolean
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface MoonOk<T> {
|
|
23
|
-
$tag: 1
|
|
24
|
-
_0: T
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface MoonErr<E> {
|
|
28
|
-
$tag: 0
|
|
29
|
-
_0: E
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type MoonResult<T, E> = MoonOk<T> | MoonErr<E>
|
package/js/vm/json.test.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'vite-plus/test'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
parseJson,
|
|
5
|
-
toJsonString,
|
|
6
|
-
toOptionalJsonString,
|
|
7
|
-
unwrapResult,
|
|
8
|
-
} from './json.ts'
|
|
9
|
-
|
|
10
|
-
describe('moonscratch/js/vm/json.ts', () => {
|
|
11
|
-
test('serializes JSON object inputs', () => {
|
|
12
|
-
expect(toJsonString({ answer: 42 }, 'projectJson', true)).toBe(
|
|
13
|
-
'{"answer":42}',
|
|
14
|
-
)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
test('rejects empty required JSON strings', () => {
|
|
18
|
-
expect(() => toJsonString(' ', 'projectJson', true)).toThrow(
|
|
19
|
-
'projectJson must be a non-empty JSON string or object',
|
|
20
|
-
)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
test('keeps optional JSON undefined', () => {
|
|
24
|
-
expect(toOptionalJsonString(undefined, 'assets')).toBeUndefined()
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('unwraps moon results', () => {
|
|
28
|
-
expect(unwrapResult({ $tag: 1, _0: 123 }, 'context')).toBe(123)
|
|
29
|
-
expect(() =>
|
|
30
|
-
unwrapResult({ $tag: 0, _0: { _0: 'boom' } }, 'context'),
|
|
31
|
-
).toThrow('context: boom')
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test('parses json and reports context on parse error', () => {
|
|
35
|
-
expect(parseJson<{ value: number }>(' {"value": 1}', 'ctx').value).toBe(1)
|
|
36
|
-
expect(() => parseJson('not-json', 'ctx')).toThrow(
|
|
37
|
-
'ctx: failed to parse JSON',
|
|
38
|
-
)
|
|
39
|
-
})
|
|
40
|
-
})
|