diff --git a/src/app/Config.ts b/src/app/Config.ts index f1e92638..7f799e5f 100644 --- a/src/app/Config.ts +++ b/src/app/Config.ts @@ -1,5 +1,5 @@ import config from '../config.json' -import type { VersionId } from './services/Schemas.js' +import type { VersionId } from './services/Versions.js' export interface ConfigLanguage { code: string, diff --git a/src/app/components/customized/CustomizedGenerator.ts b/src/app/components/customized/CustomizedGenerator.ts index 55f2d03a..eec763ff 100644 --- a/src/app/components/customized/CustomizedGenerator.ts +++ b/src/app/components/customized/CustomizedGenerator.ts @@ -1,7 +1,7 @@ import { Identifier } from 'deepslate' -import { deepClone, deepEqual } from '../../Utils.js' import { fetchAllPresets, fetchBlockStates } from '../../services/DataFetcher.js' -import type { VersionId } from '../../services/Schemas.js' +import type { VersionId } from '../../services/Versions.js' +import { deepClone, deepEqual } from '../../Utils.js' import type { CustomizedOreModel } from './CustomizedModel.js' import { CustomizedModel } from './CustomizedModel.js' diff --git a/src/app/components/customized/CustomizedModel.ts b/src/app/components/customized/CustomizedModel.ts index 68f2ded0..da9851e4 100644 --- a/src/app/components/customized/CustomizedModel.ts +++ b/src/app/components/customized/CustomizedModel.ts @@ -1,4 +1,4 @@ -import type { VersionId } from '../../services/Schemas.js' +import type { VersionId } from '../../services/Versions.js' export interface CustomizedOreModel { size: number, diff --git a/src/app/components/generator/FileCreation.tsx b/src/app/components/generator/FileCreation.tsx index 740bbab8..bd349105 100644 --- a/src/app/components/generator/FileCreation.tsx +++ b/src/app/components/generator/FileCreation.tsx @@ -1,18 +1,18 @@ +import type { DocAndNode } from '@spyglassmc/core' import { useState } from 'preact/hooks' import { Analytics } from '../../Analytics.js' import { useLocale, useProject } from '../../contexts/index.js' -import type { FileModel } from '../../services/index.js' import { Btn } from '../Btn.js' import { TextInput } from '../forms/index.js' import { Modal } from '../Modal.js' interface Props { - model: FileModel, + docAndNode: DocAndNode, id: string, method: string, onClose: () => void, } -export function FileCreation({ model, id, method, onClose }: Props) { +export function FileCreation({ docAndNode, id, method, onClose }: Props) { const { locale } = useLocale() const { projects, project, updateFile } = useProject() const [fileId, setFileId] = useState(id === 'pack_mcmeta' ? 'pack' : '') @@ -29,7 +29,8 @@ export function FileCreation({ model, id, method, onClose }: Props) { return } Analytics.saveProjectFile(id, projects.length, project.files.length, method as any) - updateFile(id, undefined, { type: id, id: fileId, data: model.data }) + const data = JSON.parse(docAndNode.doc.getText()) + updateFile(id, undefined, { type: id, id: fileId, data }) onClose() } diff --git a/src/app/components/generator/GeneratorCard.tsx b/src/app/components/generator/GeneratorCard.tsx index 0ddc039a..dc03e8e6 100644 --- a/src/app/components/generator/GeneratorCard.tsx +++ b/src/app/components/generator/GeneratorCard.tsx @@ -2,8 +2,8 @@ import { useMemo } from 'preact/hooks' import type { ConfigGenerator } from '../../Config.js' import config from '../../Config.js' import { useLocale } from '../../contexts/Locale.jsx' -import type { VersionId } from '../../services/Schemas.js' -import { checkVersion } from '../../services/Schemas.js' +import type { VersionId } from '../../services/Versions.js' +import { checkVersion } from '../../services/Versions.js' import { cleanUrl } from '../../Utils.js' import { Badge, Card, Icons, ToolCard } from '../index.js' diff --git a/src/app/components/generator/GeneratorList.tsx b/src/app/components/generator/GeneratorList.tsx index 7faf0cb9..13addca2 100644 --- a/src/app/components/generator/GeneratorList.tsx +++ b/src/app/components/generator/GeneratorList.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'preact/hooks' import type { ConfigGenerator } from '../../Config.js' import config from '../../Config.js' import { useLocale, useVersion } from '../../contexts/index.js' -import { checkVersion } from '../../services/Schemas.js' +import { checkVersion } from '../../services/Versions.js' import { GeneratorCard, TextInput, VersionSwitcher } from '../index.js' interface Props { diff --git a/src/app/components/generator/PreviewPanel.tsx b/src/app/components/generator/PreviewPanel.tsx index c2576361..f8222fcc 100644 --- a/src/app/components/generator/PreviewPanel.tsx +++ b/src/app/components/generator/PreviewPanel.tsx @@ -1,59 +1,59 @@ +import type { DocAndNode } from '@spyglassmc/core' import { useVersion } from '../../contexts/Version.jsx' -import type { FileModel } from '../../services/index.js' import { checkVersion } from '../../services/index.js' import { BiomeSourcePreview, BlockStatePreview, DecoratorPreview, DensityFunctionPreview, LootTablePreview, ModelPreview, NoisePreview, NoiseSettingsPreview, RecipePreview, StructureSetPreview } from '../previews/index.js' -export const HasPreview = ['loot_table', 'recipe', 'dimension', 'worldgen/density_function', 'worldgen/noise', 'worldgen/noise_settings', 'worldgen/configured_feature', 'worldgen/placed_feature', 'worldgen/structure_set', 'block_definition', 'model'] +export const HasPreview = ['loot_table', 'recipe', 'dimension', 'worldgen/density_function', 'worldgen/noise', 'worldgen/noise_settings', 'worldgen/configured_feature', 'worldgen/placed_feature', 'worldgen/structure_set', 'block_definition', 'docAndNode'] type PreviewPanelProps = { - model: FileModel | undefined, + docAndNode: DocAndNode | undefined, id: string, shown: boolean, onError: (message: string) => unknown, } -export function PreviewPanel({ model, id, shown }: PreviewPanelProps) { +export function PreviewPanel({ docAndNode, id, shown }: PreviewPanelProps) { const { version } = useVersion() - if (!model) return <>> + if (!docAndNode) return <>> if (id === 'loot_table') { - return + return } if (id === 'recipe') { - return + return } - if (id === 'dimension' && model.data.generator?.type?.endsWith('noise')) { - return + if (id === 'dimension' && JSON.parse(docAndNode.doc.getText()).generator?.type?.endsWith('noise')) { + return } if (id === 'worldgen/density_function') { - return + return } if (id === 'worldgen/noise') { - return + return } if (id === 'worldgen/noise_settings' && checkVersion(version, '1.18')) { - return + return } if ((id === 'worldgen/placed_feature' || (id === 'worldgen/configured_feature' && checkVersion(version, '1.16', '1.17')))) { - return + return } if (id === 'worldgen/structure_set' && checkVersion(version, '1.19')) { - return + return } if (id === 'block_definition') { - return + return } if (id === 'model') { - return + return } return <>> diff --git a/src/app/components/generator/SchemaGenerator.tsx b/src/app/components/generator/SchemaGenerator.tsx index b3c4663b..c017d42b 100644 --- a/src/app/components/generator/SchemaGenerator.tsx +++ b/src/app/components/generator/SchemaGenerator.tsx @@ -5,11 +5,11 @@ import type { ConfigGenerator } from '../../Config.js' import config from '../../Config.js' import { DRAFT_PROJECT, useLocale, useProject, useVersion } from '../../contexts/index.js' import { AsyncCancel, useActiveTimeout, useAsync, useSearchParam } from '../../hooks/index.js' -import type { FileModel, VersionId } from '../../services/index.js' -import { checkVersion, createMockFileModel, fetchPreset, fetchRegistries, getSnippet, shareSnippet } from '../../services/index.js' -import { setupSpyglass } from '../../services/Spyglass.js' +import type { VersionId } from '../../services/index.js' +import { checkVersion, fetchPreset, fetchRegistries, getSnippet, shareSnippet } from '../../services/index.js' +import { Spyglass } from '../../services/Spyglass.js' import { Store } from '../../Store.js' -import { cleanUrl, deepEqual, genPath } from '../../Utils.js' +import { cleanUrl, genPath } from '../../Utils.js' import { Ad, Btn, BtnMenu, ErrorPanel, FileCreation, FileRenaming, Footer, HasPreview, Octicon, PreviewPanel, ProjectCreation, ProjectDeletion, ProjectPanel, SearchList, SourcePanel, TextInput, Tree, VersionSwitcher } from '../index.js' export const SHARE_KEY = 'share' @@ -31,10 +31,15 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { useEffect(() => Store.visitGenerator(gen.id), [gen.id]) - useEffect(() => { - setupSpyglass(version) + const { value: spyglass, loading: spyglassLoading } = useAsync(() => { + return Spyglass.initialize(version) }, [version]) + const uri = useMemo(() => { + // TODO: return different uri when project file is open + return spyglass?.getUnsavedFileUri(gen) + }, [spyglass, gen.id]) + const [currentPreset, setCurrentPreset] = useSearchParam('preset') const [sharedSnippetId, setSharedSnippetId] = useSearchParam(SHARE_KEY) const backup = useMemo(() => Store.getBackup(gen.id), [gen.id]) @@ -45,7 +50,10 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { } } - const {} = useAsync(async () => { + const { value: docAndNode } = useAsync(async () => { + if (spyglassLoading || !spyglass || !uri) { + return AsyncCancel + } let data: unknown = undefined if (currentPreset && sharedSnippetId) { setSharedSnippetId(undefined) @@ -83,14 +91,12 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { } data = file.data } - if (data) { - // TODO: set file contents to data - } + const docAndNode = await spyglass.setFileContents(uri, JSON.stringify(data ?? {})) Analytics.setGenerator(gen.id) - return {} - }, [gen.id, version, sharedSnippetId, currentPreset, project.name, file?.id]) + return docAndNode + }, [gen.id, version, sharedSnippetId, currentPreset, project.name, file?.id, spyglass, spyglassLoading]) - const model: FileModel = createMockFileModel() + const { doc } = docAndNode ?? {} // TODO: when contents of file change: // - remove preset and share id from url @@ -184,13 +190,13 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { setShareShown(true) copySharedId() } else { - // TODO: get contents from file, and compare to default of type - if (deepEqual(model.data, {})) { + // TODO: check if files hasn't been modified compared to the default + if (false) { setShareUrl(`${location.origin}/${gen.url}/?version=${version}`) setShareShown(true) - } else { + } else if (doc) { setShareLoading(true) - shareSnippet(gen.id, version, model.data, previewShown) + shareSnippet(gen.id, version, JSON.parse(doc.getText()), previewShown) .then(({ id, length, compressed, rate }) => { Analytics.createSnippet(gen.id, id, version, length, compressed, rate) const url = `${location.origin}/${gen.url}/?${SHARE_KEY}=${id}` @@ -306,7 +312,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { {error && setError(null)} />} - + {docAndNode && } @@ -327,10 +333,10 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { - + - + @@ -346,7 +352,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) { {projectCreating && setProjectCreating(false)} />} {projectDeleting && setprojectDeleting(false)} />} - {model && fileSaving && setFileSaving(undefined)} />} + {docAndNode && fileSaving && setFileSaving(undefined)} />} {fileRenaming && setFileRenaming(undefined)} />} > } diff --git a/src/app/components/generator/SourcePanel.tsx b/src/app/components/generator/SourcePanel.tsx index 809866ff..d22a162a 100644 --- a/src/app/components/generator/SourcePanel.tsx +++ b/src/app/components/generator/SourcePanel.tsx @@ -1,8 +1,10 @@ +import type { DocAndNode } from '@spyglassmc/core' +import { fileUtil } from '@spyglassmc/core' import { useCallback, useEffect, useRef, useState } from 'preact/hooks' import { useLocale } from '../../contexts/index.js' import { useLocalStorage } from '../../hooks/index.js' -import type { FileModel } from '../../services/index.js' import { getSourceFormats, getSourceIndent, getSourceIndents, parseSource, sortData, stringifySource } from '../../services/index.js' +import type { Spyglass } from '../../services/Spyglass.js' import { Store } from '../../Store.js' import { message } from '../../Utils.js' import { Btn, BtnMenu } from '../index.js' @@ -15,15 +17,15 @@ interface Editor { } type SourcePanelProps = { - name: string, - model: FileModel | undefined, + spyglass: Spyglass | undefined, + docAndNode: DocAndNode | undefined, doCopy?: number, doDownload?: number, doImport?: number, copySuccess: () => unknown, onError: (message: string | Error) => unknown, } -export function SourcePanel({ name, model, doCopy, doDownload, doImport, copySuccess, onError }: SourcePanelProps) { +export function SourcePanel({ spyglass, docAndNode, doCopy, doDownload, doImport, copySuccess, onError }: SourcePanelProps) { const { locale } = useLocale() const [indent, setIndent] = useState(Store.getIndent()) const [format, setFormat] = useState(Store.getFormat()) @@ -37,20 +39,23 @@ export function SourcePanel({ name, model, doCopy, doDownload, doImport, copySuc const textarea = useRef(null) const editor = useRef() - const getSerializedOutput = useCallback((model: FileModel) => { - let data = model.data + const getSerializedOutput = useCallback((text: string) => { + let data = JSON.parse(text) if (sort === 'alphabetically') { data = sortData(data) } return stringifySource(data, format, indent) }, [indent, format, sort]) + const text = docAndNode?.doc.getText() + useEffect(() => { retransform.current = () => { - if (!editor.current) return - if (!model) return + if (!editor.current || text === undefined) { + return + } try { - const output = getSerializedOutput(model) + const output = getSerializedOutput(text) editor.current.setValue(output) } catch (e) { if (e instanceof Error) { @@ -68,9 +73,10 @@ export function SourcePanel({ name, model, doCopy, doDownload, doImport, copySuc if (!editor.current) return const value = editor.current.getValue() if (value.length === 0) return + if (!spyglass || !docAndNode) return try { - await parseSource(value, format) - // TODO: import + const data = await parseSource(value, format) + await spyglass.setFileContents(docAndNode.doc.uri, JSON.stringify(data)) } catch (e) { if (e instanceof Error) { e.message = `Error importing: ${e.message}` @@ -81,7 +87,7 @@ export function SourcePanel({ name, model, doCopy, doDownload, doImport, copySuc console.error(e) } } - }, [model, indent, format, sort, highlighting]) + }, [spyglass, docAndNode, text, indent, format, sort, highlighting]) useEffect(() => { if (highlighting) { @@ -144,9 +150,10 @@ export function SourcePanel({ name, model, doCopy, doDownload, doImport, copySuc // TODO: when file contents change, retransform useEffect(() => { - if (!retransform.current) return - if (model) retransform.current() - }, [model]) + if (retransform.current && text !== undefined) { + retransform.current() + } + }, [text]) useEffect(() => { if (!editor.current || !retransform.current) return @@ -157,18 +164,18 @@ export function SourcePanel({ name, model, doCopy, doDownload, doImport, copySuc }, [indent, format, sort, highlighting, braceLoaded]) useEffect(() => { - if (doCopy && model) { - navigator.clipboard.writeText(getSerializedOutput(model)).then(() => { + if (doCopy && text !== undefined) { + navigator.clipboard.writeText(getSerializedOutput(text)).then(() => { copySuccess() }) } - }, [doCopy]) + }, [doCopy, text]) useEffect(() => { - if (doDownload && model && download.current) { - const content = encodeURIComponent(getSerializedOutput(model)) + if (doDownload && docAndNode && text !== undefined && download.current) { + const content = encodeURIComponent(getSerializedOutput(text)) download.current.setAttribute('href', `data:text/json;charset=utf-8,${content}`) - const fileName = name === 'pack_mcmeta' ? 'pack.mcmeta' : `${name}.${format}` + const fileName = fileUtil.basename(docAndNode.doc.uri) download.current.setAttribute('download', fileName) download.current.click() } diff --git a/src/app/components/generator/Tree.tsx b/src/app/components/generator/Tree.tsx index 3b9c662f..01837026 100644 --- a/src/app/components/generator/Tree.tsx +++ b/src/app/components/generator/Tree.tsx @@ -1,14 +1,14 @@ +import type { DocAndNode } from '@spyglassmc/core' import { useErrorBoundary } from 'preact/hooks' import { useLocale } from '../../contexts/index.js' -import type { FileModel } from '../../services/index.js' type TreePanelProps = { - model: FileModel | undefined, + docAndNode: DocAndNode, onError: (message: string) => unknown, } -export function Tree({ model, onError }: TreePanelProps) { +export function Tree({ onError }: TreePanelProps) { const { lang } = useLocale() - if (!model || lang === 'none') return <>> + if (lang === 'none') return <>> const [error] = useErrorBoundary(e => { onError(`Error rendering the tree: ${e.message}`) diff --git a/src/app/components/previews/BiomeSourcePreview.tsx b/src/app/components/previews/BiomeSourcePreview.tsx index 4fe0bed8..f03e06ea 100644 --- a/src/app/components/previews/BiomeSourcePreview.tsx +++ b/src/app/components/previews/BiomeSourcePreview.tsx @@ -3,7 +3,7 @@ import { mat3 } from 'gl-matrix' import { useCallback, useRef, useState } from 'preact/hooks' import { getProjectData, useLocale, useProject, useStore, useVersion } from '../../contexts/index.js' import { useAsync } from '../../hooks/index.js' -import { checkVersion } from '../../services/Schemas.js' +import { checkVersion } from '../../services/Versions.js' import { Store } from '../../Store.js' import { iterateWorld2D, randomSeed, stringToColor } from '../../Utils.js' import { Btn, BtnMenu, NumberInput } from '../index.js' @@ -20,7 +20,7 @@ type Layer = typeof LAYERS[number] const DETAIL_DELAY = 300 const DETAIL_SCALE = 2 -export const BiomeSourcePreview = ({ model, shown }: PreviewProps) => { +export const BiomeSourcePreview = ({ docAndNode, shown }: PreviewProps) => { const { locale } = useLocale() const { version } = useVersion() const { project } = useProject() @@ -31,18 +31,19 @@ export const BiomeSourcePreview = ({ model, shown }: PreviewProps) => { const [focused, setFocused] = useState([]) const [focused2, setFocused2] = useState([]) - const state = JSON.stringify(model.data) - const type: string = model.data?.generator?.biome_source?.type?.replace(/^minecraft:/, '') ?? '' + const text = docAndNode.doc.getText() + const data = JSON.parse(text) + const type: string = data?.generator?.biome_source?.type?.replace(/^minecraft:/, '') ?? '' const hasRandomness = type === 'multi_noise' || type === 'the_end' const { value } = useAsync(async function loadBiomeSource() { await DEEPSLATE.loadVersion(version, getProjectData(project)) - await DEEPSLATE.loadChunkGenerator(model.data?.generator?.settings, model.data?.generator?.biome_source, seed) + await DEEPSLATE.loadChunkGenerator(data?.generator?.settings, data?.generator?.biome_source, seed) return { biomeSource: { loaded: true }, noiseRouter: checkVersion(version, '1.19') ? DEEPSLATE.getNoiseRouter() : undefined, } - }, [state, seed, project, version]) + }, [text, seed, project, version]) const { biomeSource, noiseRouter } = value ?? {} const actualLayer = noiseRouter ? layer : 'biomes' diff --git a/src/app/components/previews/BlockStatePreview.tsx b/src/app/components/previews/BlockStatePreview.tsx index adb99d4b..d10e92ff 100644 --- a/src/app/components/previews/BlockStatePreview.tsx +++ b/src/app/components/previews/BlockStatePreview.tsx @@ -10,14 +10,15 @@ import { InteractiveCanvas3D } from './InteractiveCanvas3D.jsx' const PREVIEW_ID = Identifier.parse('misode:preview') -export const BlockStatePreview = ({ model, shown }: PreviewProps) => { +export const BlockStatePreview = ({ docAndNode, shown }: PreviewProps) => { const { version } = useVersion() - const serializedData = JSON.stringify(model.data) + + const text = docAndNode.doc.getText() const { value: resources } = useAsync(async () => { if (!shown) return AsyncCancel const resources = await getResources(version) - const definition = BlockDefinition.fromJson(model.data) + const definition = BlockDefinition.fromJson(JSON.parse(text)) const wrapper = new ResourceWrapper(resources, { getBlockDefinition(id) { if (id.equals(PREVIEW_ID)) return definition @@ -25,7 +26,7 @@ export const BlockStatePreview = ({ model, shown }: PreviewProps) => { }, }) return wrapper - }, [shown, version, serializedData]) + }, [shown, version, text]) const renderer = useRef(undefined) diff --git a/src/app/components/previews/DecoratorPreview.tsx b/src/app/components/previews/DecoratorPreview.tsx index 86031dff..5ac9c333 100644 --- a/src/app/components/previews/DecoratorPreview.tsx +++ b/src/app/components/previews/DecoratorPreview.tsx @@ -9,11 +9,12 @@ import { decorateChunk } from './Decorator.js' import type { PreviewProps } from './index.js' import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx' -export const DecoratorPreview = ({ model, shown }: PreviewProps) => { +export const DecoratorPreview = ({ docAndNode, shown }: PreviewProps) => { const { locale } = useLocale() const { version } = useVersion() const [seed, setSeed] = useState(randomSeed()) - const state = JSON.stringify(model.data) + + const text = docAndNode.doc.getText() const { context, chunkFeatures } = useMemo(() => { const random = new LegacyRandom(seed) @@ -32,7 +33,7 @@ export const DecoratorPreview = ({ model, shown }: PreviewProps) => { context, chunkFeatures: new Map(), } - }, [state, version, seed]) + }, [text, version, seed]) const ctx = useRef() const imageData = useRef() @@ -49,10 +50,10 @@ export const DecoratorPreview = ({ model, shown }: PreviewProps) => { }, []) const onDraw = useCallback(function onDraw(transform: mat3) { if (!ctx.current || !imageData.current || !shown) return - + const data = JSON.parse(text) iterateWorld2D(imageData.current, transform, (x, y) => { const pos = ChunkPos.create(Math.floor(x / 16), Math.floor(-y / 16)) - const features = computeIfAbsent(chunkFeatures, `${pos[0]} ${pos[1]}`, () => decorateChunk(pos, model.data, context)) + const features = computeIfAbsent(chunkFeatures, `${pos[0]} ${pos[1]}`, () => decorateChunk(pos, data, context)) return features.find(f => f.pos[0] === x && f.pos[2] == -y) ?? { pos: BlockPos.create(x, 0, -y) } }, (feature) => { if ('color' in feature) { diff --git a/src/app/components/previews/DensityFunctionPreview.tsx b/src/app/components/previews/DensityFunctionPreview.tsx index 886a8541..3f470a0b 100644 --- a/src/app/components/previews/DensityFunctionPreview.tsx +++ b/src/app/components/previews/DensityFunctionPreview.tsx @@ -18,7 +18,7 @@ import { InteractiveCanvas3D } from './InteractiveCanvas3D.jsx' const MODES = ['side', 'top', '3d'] as const -export const DensityFunctionPreview = ({ model, shown }: PreviewProps) => { +export const DensityFunctionPreview = ({ docAndNode, shown }: PreviewProps) => { const { locale } = useLocale() const { project } = useProject() const { version } = useVersion() @@ -28,13 +28,14 @@ export const DensityFunctionPreview = ({ model, shown }: PreviewProps) => { const [seed, setSeed] = useState(randomSeed()) const [minY] = useState(0) const [height] = useState(256) - const serializedData = JSON.stringify(model.data) + + const text = docAndNode.doc.getText() const { value: df } = useAsync(async () => { await DEEPSLATE.loadVersion(version, getProjectData(project)) - const df = DEEPSLATE.loadDensityFunction(model.data, minY, height, seed) + const df = DEEPSLATE.loadDensityFunction(JSON.parse(text), minY, height, seed) return df - }, [version, project, minY, height, seed, serializedData]) + }, [version, project, minY, height, seed, text]) // === 2D === const imageData = useRef() diff --git a/src/app/components/previews/LootTable.ts b/src/app/components/previews/LootTable.ts index 0d876178..39c1d900 100644 --- a/src/app/components/previews/LootTable.ts +++ b/src/app/components/previews/LootTable.ts @@ -3,7 +3,7 @@ import type { Random } from 'deepslate/core' import { Identifier, ItemStack, LegacyRandom } from 'deepslate/core' import { NbtCompound, NbtInt, NbtList, NbtString, NbtTag } from 'deepslate/nbt' import { ResolvedItem } from '../../services/ResolvedItem.js' -import type { VersionId } from '../../services/Schemas.js' +import type { VersionId } from '../../services/Versions.js' import { clamp, getWeightedRandom, isObject, jsonToNbt } from '../../Utils.js' export interface SlottedItem { diff --git a/src/app/components/previews/LootTable1204.ts b/src/app/components/previews/LootTable1204.ts index 27aa7501..c485112c 100644 --- a/src/app/components/previews/LootTable1204.ts +++ b/src/app/components/previews/LootTable1204.ts @@ -1,7 +1,7 @@ import type { Random } from 'deepslate-1.20.4/core' import { Enchantment, Identifier, ItemStack, LegacyRandom } from 'deepslate-1.20.4/core' import { NbtCompound, NbtInt, NbtList, NbtShort, NbtString, NbtTag, NbtType } from 'deepslate-1.20.4/nbt' -import type { VersionId } from '../../services/Schemas.js' +import type { VersionId } from '../../services/Versions.js' import { clamp, getWeightedRandom, isObject } from '../../Utils.js' export interface SlottedItem { diff --git a/src/app/components/previews/LootTablePreview.tsx b/src/app/components/previews/LootTablePreview.tsx index 1205ba96..837f41c8 100644 --- a/src/app/components/previews/LootTablePreview.tsx +++ b/src/app/components/previews/LootTablePreview.tsx @@ -11,7 +11,7 @@ import type { PreviewProps } from './index.js' import { generateLootTable } from './LootTable.js' import { generateLootTable as generateLootTable1204 } from './LootTable1204.js' -export const LootTablePreview = ({ model }: PreviewProps) => { +export const LootTablePreview = ({ docAndNode }: PreviewProps) => { const { locale } = useLocale() const { version } = useVersion() const use1204 = !checkVersion(version, '1.20.5') @@ -34,13 +34,13 @@ export const LootTablePreview = ({ model }: PreviewProps) => { ]) }, [version]) - const table = model.data - const state = JSON.stringify(table) + const text = docAndNode.doc.getText() const items = useMemo(() => { if (dependencies === undefined || loading) { return [] } const [itemTags, lootTables, itemComponents, enchantments, enchantmentTags] = dependencies + const table = JSON.parse(text) if (use1204) { return generateLootTable1204(table, { version, seed, luck, daytime, weather, @@ -60,7 +60,7 @@ export const LootTablePreview = ({ model }: PreviewProps) => { getEnchantmentTag: (id) => (enchantmentTags?.get(id.replace(/^minecraft:/, '')) as any)?.values ?? [], getBaseComponents: (id) => new Map([...(itemComponents?.get(Identifier.parse(id).toString()) ?? new Map()).entries()].map(([k, v]) => [k, jsonToNbt(v)])), }) - }, [version, seed, luck, daytime, weather, mixItems, state, dependencies, loading]) + }, [version, seed, luck, daytime, weather, mixItems, text, dependencies, loading]) return <> diff --git a/src/app/components/previews/ModelPreview.tsx b/src/app/components/previews/ModelPreview.tsx index 0b7b31c4..1b958c73 100644 --- a/src/app/components/previews/ModelPreview.tsx +++ b/src/app/components/previews/ModelPreview.tsx @@ -11,14 +11,15 @@ import { InteractiveCanvas3D } from './InteractiveCanvas3D.jsx' const PREVIEW_ID = Identifier.parse('misode:preview') const PREVIEW_DEFINITION = new BlockDefinition({ '': { model: PREVIEW_ID.toString() }}, undefined) -export const ModelPreview = ({ model, shown }: PreviewProps) => { +export const ModelPreview = ({ docAndNode, shown }: PreviewProps) => { const { version } = useVersion() - const serializedData = JSON.stringify(model.data) + + const text = docAndNode.doc.getText() const { value: resources } = useAsync(async () => { if (!shown) return AsyncCancel const resources = await getResources(version) - const blockModel = BlockModel.fromJson(model.data) + const blockModel = BlockModel.fromJson(JSON.parse(text)) blockModel.flatten(resources) const wrapper = new ResourceWrapper(resources, { getBlockDefinition(id) { @@ -31,7 +32,7 @@ export const ModelPreview = ({ model, shown }: PreviewProps) => { }, }) return wrapper - }, [shown, version, serializedData]) + }, [shown, version, text]) const renderer = useRef(undefined) diff --git a/src/app/components/previews/NoisePreview.tsx b/src/app/components/previews/NoisePreview.tsx index 7d12be0b..a0c40322 100644 --- a/src/app/components/previews/NoisePreview.tsx +++ b/src/app/components/previews/NoisePreview.tsx @@ -11,16 +11,17 @@ import { ColormapSelector } from './ColormapSelector.jsx' import type { PreviewProps } from './index.js' import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx' -export const NoisePreview = ({ model, shown }: PreviewProps) => { +export const NoisePreview = ({ docAndNode, shown }: PreviewProps) => { const { locale } = useLocale() const [seed, setSeed] = useState(randomSeed()) - const state = JSON.stringify(model.data) + + const text = docAndNode.doc.getText() const noise = useMemo(() => { const random = XoroshiroRandom.create(seed) - const params = NoiseParameters.fromJson(model.data) + const params = NoiseParameters.fromJson(JSON.parse(text)) return new NormalNoise(random, params) - }, [state, seed]) + }, [text, seed]) const imageData = useRef() const ctx = useRef() diff --git a/src/app/components/previews/NoiseSettingsPreview.tsx b/src/app/components/previews/NoiseSettingsPreview.tsx index e8752b69..a1d4a289 100644 --- a/src/app/components/previews/NoiseSettingsPreview.tsx +++ b/src/app/components/previews/NoiseSettingsPreview.tsx @@ -15,24 +15,25 @@ import { DEEPSLATE } from './Deepslate.js' import type { PreviewProps } from './index.js' import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx' -export const NoiseSettingsPreview = ({ model, shown }: PreviewProps) => { +export const NoiseSettingsPreview = ({ docAndNode, shown }: PreviewProps) => { const { locale } = useLocale() const { version } = useVersion() const { project } = useProject() const [seed, setSeed] = useState(randomSeed()) const [biome, setBiome] = useState('minecraft:plains') const [layer, setLayer] = useState('terrain') - const state = JSON.stringify(model.data) + + const text = docAndNode.doc.getText() const { value, error } = useAsync(async () => { - const unwrapped = model.data + const data = JSON.parse(text) await DEEPSLATE.loadVersion(version, getProjectData(project)) const biomeSource = { type: 'fixed', biome } - await DEEPSLATE.loadChunkGenerator(unwrapped, biomeSource, seed) + await DEEPSLATE.loadChunkGenerator(data, biomeSource, seed) const noiseSettings = DEEPSLATE.getNoiseSettings() - const finalDensity = DEEPSLATE.loadDensityFunction(unwrapped?.noise_router?.final_density, noiseSettings.minY, noiseSettings.height, seed) + const finalDensity = DEEPSLATE.loadDensityFunction(data?.noise_router?.final_density, noiseSettings.minY, noiseSettings.height, seed) return { noiseSettings, finalDensity } - }, [state, seed, version, project, biome]) + }, [text, seed, version, project, biome]) const { noiseSettings, finalDensity } = value ?? {} const imageData = useRef() diff --git a/src/app/components/previews/RecipePreview.tsx b/src/app/components/previews/RecipePreview.tsx index 2451ca1b..350b0f3a 100644 --- a/src/app/components/previews/RecipePreview.tsx +++ b/src/app/components/previews/RecipePreview.tsx @@ -11,7 +11,7 @@ import type { PreviewProps } from './index.js' const ANIMATION_TIME = 1000 -export const RecipePreview = ({ model }: PreviewProps) => { +export const RecipePreview = ({ docAndNode }: PreviewProps) => { const { locale } = useLocale() const { version } = useVersion() const [advancedTooltips, setAdvancedTooltips] = useState(true) @@ -29,11 +29,11 @@ export const RecipePreview = ({ model }: PreviewProps) => { return () => clearInterval(interval) }, []) - const recipe = model.data - const state = JSON.stringify(recipe) + const text = docAndNode.doc.getText() + const recipe = JSON.parse(text) const items = useMemo>(() => { return placeItems(version, recipe, animation, itemTags ?? new Map()) - }, [state, animation, itemTags]) + }, [text, animation, itemTags]) const gui = useMemo(() => { const type = recipe.type?.replace(/^minecraft:/, '') @@ -46,7 +46,7 @@ export const RecipePreview = ({ model }: PreviewProps) => { } else { return '/images/crafting_table.png' } - }, [state]) + }, [text]) return <> diff --git a/src/app/components/previews/StructureSetPreview.tsx b/src/app/components/previews/StructureSetPreview.tsx index 80f10b56..1c26cf17 100644 --- a/src/app/components/previews/StructureSetPreview.tsx +++ b/src/app/components/previews/StructureSetPreview.tsx @@ -12,17 +12,18 @@ import { DEEPSLATE } from './Deepslate.js' import type { PreviewProps } from './index.js' import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx' -export const StructureSetPreview = ({ model, shown }: PreviewProps) => { +export const StructureSetPreview = ({ docAndNode, shown }: PreviewProps) => { const { locale } = useLocale() const { version } = useVersion() const [seed, setSeed] = useState(randomSeed()) - const state = JSON.stringify(model.data) + + const text = docAndNode.doc.getText() const { value: structureSet } = useAsync(async () => { await DEEPSLATE.loadVersion(version) - const structureSet = DEEPSLATE.loadStructureSet(model.data, seed) + const structureSet = DEEPSLATE.loadStructureSet(JSON.parse(text), seed) return structureSet - }, [state, version, seed]) + }, [text, version, seed]) const { chunkStructures, structureColors } = useMemo(() => { return { diff --git a/src/app/components/previews/index.ts b/src/app/components/previews/index.ts index b139e825..70e3ada0 100644 --- a/src/app/components/previews/index.ts +++ b/src/app/components/previews/index.ts @@ -1,4 +1,4 @@ -import type { FileModel } from '../../services/index.js' +import type { DocAndNode } from '@spyglassmc/core' export * from './BiomeSourcePreview.js' export * from './BlockStatePreview.jsx' @@ -12,6 +12,6 @@ export * from './RecipePreview.jsx' export * from './StructureSetPreview.jsx' export interface PreviewProps { - model: FileModel + docAndNode: DocAndNode shown: boolean } diff --git a/src/app/pages/Customized.tsx b/src/app/pages/Customized.tsx index 55acc870..f69846eb 100644 --- a/src/app/pages/Customized.tsx +++ b/src/app/pages/Customized.tsx @@ -1,11 +1,11 @@ import { useEffect, useErrorBoundary, useMemo } from 'preact/hooks' -import config from '../Config.js' import { CustomizedPanel } from '../components/customized/CustomizedPanel.jsx' import { ErrorPanel, Footer, Octicon, VersionSwitcher } from '../components/index.js' +import config from '../Config.js' import { useLocale, useTitle, useVersion } from '../contexts/index.js' import { useSearchParam } from '../hooks/index.js' -import type { VersionId } from '../services/Schemas.js' -import { checkVersion } from '../services/Schemas.js' +import type { VersionId } from '../services/Versions.js' +import { checkVersion } from '../services/Versions.js' const MIN_VERSION = '1.20' const Tabs = ['basic', 'biomes', 'structures', 'ores'] diff --git a/src/app/services/DataFetcher.ts b/src/app/services/DataFetcher.ts index 66ea62ab..b68b53c2 100644 --- a/src/app/services/DataFetcher.ts +++ b/src/app/services/DataFetcher.ts @@ -1,8 +1,8 @@ import config from '../Config.js' import { Store } from '../Store.js' import { message } from '../Utils.js' -import type { VersionId } from './Schemas.js' -import { checkVersion } from './Schemas.js' +import type { VersionId } from './Versions.js' +import { checkVersion } from './Versions.js' const CACHE_NAME = 'misode-v2' const CACHE_LATEST_VERSION = 'cached_latest_version' diff --git a/src/app/services/Resources.ts b/src/app/services/Resources.ts index adf8d89b..8a7ea5fa 100644 --- a/src/app/services/Resources.ts +++ b/src/app/services/Resources.ts @@ -3,7 +3,7 @@ import { BlockDefinition, BlockModel, Identifier, ItemRenderer, TextureAtlas, up import config from '../Config.js' import { message } from '../Utils.js' import { fetchLanguage, fetchResources } from './DataFetcher.js' -import type { VersionId } from './Schemas.js' +import type { VersionId } from './Versions.js' const Resources: Record> = {} diff --git a/src/app/services/Resources1204.ts b/src/app/services/Resources1204.ts index cc7493c8..bdf889b2 100644 --- a/src/app/services/Resources1204.ts +++ b/src/app/services/Resources1204.ts @@ -3,7 +3,7 @@ import { BlockDefinition, BlockModel, Identifier, ItemRenderer, TextureAtlas, up import config from '../Config.js' import { message } from '../Utils.js' import { fetchLanguage, fetchResources } from './DataFetcher.js' -import type { VersionId } from './Schemas.js' +import type { VersionId } from './Versions.js' const Resources: Record> = {} diff --git a/src/app/services/Schemas.ts b/src/app/services/Schemas.ts deleted file mode 100644 index 752ddb04..00000000 --- a/src/app/services/Schemas.ts +++ /dev/null @@ -1,57 +0,0 @@ -import config from '../Config.js' -import { message } from '../Utils.js' -import type { BlockStateData } from './DataFetcher.js' -import { fetchBlockStates, fetchRegistries } from './DataFetcher.js' - -export const VersionIds = ['1.15', '1.16', '1.17', '1.18', '1.18.2', '1.19', '1.19.3', '1.19.4', '1.20', '1.20.2', '1.20.3', '1.20.5', '1.21', '1.21.2'] as const -export type VersionId = typeof VersionIds[number] - -export const DEFAULT_VERSION: VersionId = '1.21' - -interface VersionData { - registries: Map - blockStates: Map -} - -const Versions: Record> = {} - -async function getVersion(id: VersionId): Promise { - if (!Versions[id]) { - Versions[id] = (async () => { - try { - const registries = await fetchRegistries(id) - const blockStates= await fetchBlockStates(id) - Versions[id] = { registries, blockStates } - return Versions[id] - } catch (e) { - throw new Error(`Cannot get version "${id}": ${message(e)}`) - } - })() - return Versions[id] - } - return Versions[id] -} - -export async function getBlockStates(version: VersionId): Promise> { - const versionData = await getVersion(version) - return versionData.blockStates -} - -export function checkVersion(versionId: string, minVersionId: string | undefined, maxVersionId?: string) { - const version = config.versions.findIndex(v => v.id === versionId) - const minVersion = minVersionId ? config.versions.findIndex(v => v.id === minVersionId) : 0 - const maxVersion = maxVersionId ? config.versions.findIndex(v => v.id === maxVersionId) : config.versions.length - 1 - return minVersion <= version && version <= maxVersion -} - -export interface FileModel { - get text(): string - get data(): any -} - -export function createMockFileModel(): FileModel { - return { - text: '{}', - data: {}, - } -} diff --git a/src/app/services/Sharing.ts b/src/app/services/Sharing.ts index 028cc931..597c6ffd 100644 --- a/src/app/services/Sharing.ts +++ b/src/app/services/Sharing.ts @@ -1,5 +1,5 @@ import lz from 'lz-string' -import type { VersionId } from './Schemas.js' +import type { VersionId } from './Versions.js' const API_PREFIX = 'https://snippets.misode.workers.dev' diff --git a/src/app/services/Spyglass.ts b/src/app/services/Spyglass.ts index f38e45c1..a0aab5c2 100644 --- a/src/app/services/Spyglass.ts +++ b/src/app/services/Spyglass.ts @@ -9,61 +9,87 @@ import { localize } from '@spyglassmc/locales' import * as mcdoc from '@spyglassmc/mcdoc' import * as nbt from '@spyglassmc/nbt' import * as zip from '@zip.js/zip.js' -import type { ConfigVersion } from '../Config.js' +import type { ConfigGenerator, ConfigVersion } from '../Config.js' import siteConfig from '../Config.js' -import type { VersionId } from './index.js' -import { fetchBlockStates, fetchRegistries, fetchVanillaMcdoc, getVersionChecksum } from './index.js' +import { genPath } from '../Utils.js' +import { fetchBlockStates, fetchRegistries, fetchVanillaMcdoc, getVersionChecksum } from './DataFetcher.js' +import type { VersionId } from './Versions.js' -const externals: core.Externals = { - ...BrowserExternals, - archive: { - ...BrowserExternals.archive, - async decompressBall(buffer, { stripLevel } = {}) { - const reader = new zip.ZipReader(new zip.BlobReader(new Blob([buffer]))) - const entries = await reader.getEntries() - return await Promise.all(entries.map(async e => { - const data = await e.getData?.(new zip.Uint8ArrayWriter()) - const path = stripLevel === 1 ? e.filename.substring(e.filename.indexOf('/') + 1) : e.filename - const type = e.directory ? 'dir' : 'file' - return { data, path, mtime: '', type, mode: 0 } - })) - }, - }, +export class Spyglass { + private static readonly INSTANCES = new Map>() + + private constructor( + private readonly service: core.Service, + private readonly version: ConfigVersion, + ) {} + + public async setFileContents(uri: string, contents: string) { + await this.service.project.onDidOpen(uri, 'json', 1, contents) + const docAndNode = await this.service.project.ensureClientManagedChecked(uri) + if (!docAndNode) { + throw new Error('[Spyglass setFileContents] Cannot get doc and node') + } + return docAndNode + } + + public getFile(uri: string): Partial { + return this.service.project.getClientManaged(uri) ?? {} + } + + public getUnsavedFileUri(gen: ConfigGenerator) { + return `file:project/data/draft/${genPath(gen, this.version.id)}/unsaved.json` + } + + public static async initialize(versionId: VersionId) { + const instance = this.INSTANCES.get(versionId) + if (instance) { + return instance + } + const promise = (async () => { + const version = siteConfig.versions.find(v => v.id === versionId)! + const service = new core.Service({ + logger: console, + profilers: new core.ProfilerFactory(console, [ + 'project#init', + 'project#ready', + ]), + project: { + cacheRoot: 'file:cache/', + projectRoots: ['file:project/'], + externals: { + ...BrowserExternals, + archive: { + ...BrowserExternals.archive, + decompressBall, + }, + }, + defaultConfig: core.ConfigService.merge(core.VanillaConfig, { + env: { + gameVersion: version.ref ?? version.id, + dependencies: ['@vanilla-mcdoc'], + }, + }), + initializers: [mcdoc.initialize, initialize], + }, + }) + await service.project.ready() + await service.project.cacheService.save() + return new Spyglass(service, version) + })() + this.INSTANCES.set(versionId, promise) + return promise + } } -export async function setupSpyglass(versionId: VersionId) { - const version = siteConfig.versions.find(v => v.id === versionId)! - const gameVersion = version.ref ?? version.id - const logger: core.Logger = console - const profilers = new core.ProfilerFactory(logger, [ - 'project#init', - 'project#ready', - 'misode#setup', - ]) - const profiler = profilers.get('misode#setup') - const service = new core.Service({ - logger, - profilers, - project: { - cacheRoot: 'file:cache/', - projectRoots: ['file:project/'], - externals: externals, - defaultConfig: core.ConfigService.merge(core.VanillaConfig, { - env: { - gameVersion: gameVersion, - dependencies: ['@vanilla-mcdoc'], - }, - }), - initializers: [mcdoc.initialize, initialize], - }, - }) - await service.project.ready() - profiler.task('Project ready') - await service.project.cacheService.save() - profiler.task('Save cache') - profiler.finalize() - - service.logger.info(service.project.symbols.global) +const decompressBall: core.Externals['archive']['decompressBall'] = async (buffer, options) => { + const reader = new zip.ZipReader(new zip.BlobReader(new Blob([buffer]))) + const entries = await reader.getEntries() + return await Promise.all(entries.map(async e => { + const data = await e.getData?.(new zip.Uint8ArrayWriter()) + const path = options?.stripLevel === 1 ? e.filename.substring(e.filename.indexOf('/') + 1) : e.filename + const type = e.directory ? 'dir' : 'file' + return { data, path, mtime: '', type, mode: 0 } + })) } const initialize: core.ProjectInitializer = async (ctx) => { diff --git a/src/app/services/Versions.ts b/src/app/services/Versions.ts new file mode 100644 index 00000000..7f37c0b1 --- /dev/null +++ b/src/app/services/Versions.ts @@ -0,0 +1,13 @@ +import config from '../Config.js' + +export const VersionIds = ['1.15', '1.16', '1.17', '1.18', '1.18.2', '1.19', '1.19.3', '1.19.4', '1.20', '1.20.2', '1.20.3', '1.20.5', '1.21', '1.21.2'] as const +export type VersionId = typeof VersionIds[number] + +export const DEFAULT_VERSION: VersionId = '1.21' + +export function checkVersion(versionId: string, minVersionId: string | undefined, maxVersionId?: string) { + const version = config.versions.findIndex(v => v.id === versionId) + const minVersion = minVersionId ? config.versions.findIndex(v => v.id === minVersionId) : 0 + const maxVersion = maxVersionId ? config.versions.findIndex(v => v.id === maxVersionId) : config.versions.length - 1 + return minVersion <= version && version <= maxVersion +} diff --git a/src/app/services/index.ts b/src/app/services/index.ts index f8fb6b8a..a51c7e67 100644 --- a/src/app/services/index.ts +++ b/src/app/services/index.ts @@ -1,5 +1,5 @@ export * from './Article.js' export * from './DataFetcher.js' -export * from './Schemas.js' export * from './Sharing.js' export * from './Source.js' +export * from './Versions.js'