import { DataModel, Path } from '@mcschema/core' import { getCurrentUrl, route } from 'preact-router' import { useEffect, useErrorBoundary, useState } from 'preact/hooks' import config from '../../config.json' import { Analytics } from '../Analytics' import { Ad, Btn, BtnMenu, ErrorPanel, HasPreview, Octicon, PreviewPanel, SearchList, SourcePanel, TextInput, Tree } from '../components' import { useLocale, useProject, useTitle, useVersion } from '../contexts' import { useActiveTimeout, useModel } from '../hooks' import { getOutput } from '../schema/transformOutput' import type { BlockStateRegistry, VersionId } from '../services' import { checkVersion, fetchPreset, getBlockStates, getCollections, getModel, getSnippet, shareSnippet, SHARE_KEY } from '../services' import { cleanUrl, deepEqual, getGenerator, getSearchParams, message, setSeachParams } from '../Utils' interface Props { default?: true, } export function Generator({}: Props) { const { locale } = useLocale() const { version, changeVersion } = useVersion() const { project, file, updateFile, openFile, closeFile } = useProject() const [error, setError] = useState(null) const [errorBoundary, errorRetry] = useErrorBoundary() if (errorBoundary) { errorBoundary.message = `Something went wrong rendering the generator: ${errorBoundary.message}` return
} const gen = getGenerator(getCurrentUrl()) if (!gen) { return
} const allowedVersions = config.versions .filter(v => checkVersion(v.id, gen.minVersion, gen.maxVersion)) .map(v => v.id as VersionId) useTitle(locale('title.generator', locale(gen.id)), allowedVersions) if (!checkVersion(version, gen.minVersion)) { setError(`The minimum version for this generator is ${gen.minVersion}`) } if (!checkVersion(version, undefined, gen.maxVersion)) { setError(`This generator is not available in versions above ${gen.maxVersion}`) } const searchParams = getSearchParams(getCurrentUrl()) const currentPreset = searchParams.get('preset') const sharedSnippetId = searchParams.get(SHARE_KEY) useEffect(() => { if (model && currentPreset) { loadPreset(currentPreset).then(preset => { model.reset(DataModel.wrapLists(preset), false) setSeachParams({ version, preset: currentPreset, [SHARE_KEY]: undefined }) }) } else if (model && sharedSnippetId) { getSnippet(sharedSnippetId).then(s => loadSnippet(model, s)) } }, [currentPreset, sharedSnippetId]) const loadSnippet = (model: DataModel, snippet: any) => { if (snippet.version && snippet.version !== version) { changeVersion(snippet.version, false) } if (snippet.type && snippet.type !== gen.id) { const snippetGen = config.generators.find(g => g.id === snippet.type) if (snippetGen) { route(`${cleanUrl(snippetGen.url)}?${SHARE_KEY}=${snippet.id}`) } } if (snippet.show_preview && !previewShown) { setPreviewShown(true) setSourceShown(false) } model.reset(DataModel.wrapLists(snippet.data), false) } const [model, setModel] = useState(null) const [blockStates, setBlockStates] = useState(null) useEffect(() => { setError(null) setModel(null) getBlockStates(version) .then(b => setBlockStates(b)) getModel(version, gen.id) .then(async m => { Analytics.setGenerator(gen.id) if (currentPreset) { const preset = await loadPreset(currentPreset) m.reset(DataModel.wrapLists(preset), false) } else if (sharedSnippetId) { const snippet = await getSnippet(sharedSnippetId) loadSnippet(m, snippet) } setModel(m) }) .catch(e => { console.error(e); setError(e) }) }, [version, gen.id]) const [dirty, setDirty] = useState(false) useModel(model, () => { setSeachParams({ version: undefined, preset: undefined, [SHARE_KEY]: undefined }) setError(null) setDirty(true) }) const [fileRename, setFileRename] = useState('') const [fileSaved, doSave] = useActiveTimeout() const [fileError, doFileError] = useActiveTimeout() const doFileRename = () => { if (fileRename !== file?.id && fileRename && model && blockStates) { const data = getOutput(model, blockStates) const success = updateFile(gen.id, file?.id, { id: fileRename, data }) if (success) { doSave() } else { doFileError() if (file) { setFileRename(file?.id) } } } else if (file) { setFileRename(file?.id) } } const deleteFile = () => { if (file) { updateFile(gen.id, file.id, {}) } } useEffect(() => { if (file) { setFileRename(file.id) } }, [file]) useEffect(() => { if (model) { setFileRename(file?.id ?? '') if (file && gen.id === file.type) { model.reset(DataModel.wrapLists(file.data)) } setDirty(false) } }, [file, model]) const reset = () => { Analytics.generatorEvent('reset') model?.reset(DataModel.wrapLists(model.schema.default()), true) } const undo = (e: MouseEvent) => { e.stopPropagation() Analytics.generatorEvent('undo', 'Menu') model?.undo() } const redo = (e: MouseEvent) => { e.stopPropagation() Analytics.generatorEvent('redo', 'Menu') model?.redo() } const onKeyUp = (e: KeyboardEvent) => { if (e.ctrlKey && e.key === 'z') { Analytics.generatorEvent('undo', 'Hotkey') model?.undo() } else if (e.ctrlKey && e.key === 'y') { Analytics.generatorEvent('redo', 'Hotkey') model?.redo() } } const onKeyDown = (e: KeyboardEvent) => { if (e.ctrlKey && e.key === 's') { e.preventDefault() if (model && blockStates && file) { Analytics.generatorEvent('save', 'Hotkey') const data = getOutput(model, blockStates) updateFile(gen.id, file?.id, { id: file?.id, data }) setDirty(false) doSave() } } } useEffect(() => { document.addEventListener('keyup', onKeyUp) document.addEventListener('keydown', onKeyDown) return () => { document.removeEventListener('keyup', onKeyUp) document.removeEventListener('keydown', onKeyDown) } }, [model, blockStates, file]) const [presets, setPresets] = useState([]) useEffect(() => { getCollections(version).then(collections => { setPresets(collections.get(gen.id).map(p => p.slice(10))) }) .catch(e => { console.error(e); setError(e) }) }, [version, gen.id]) const selectPreset = (id: string) => { Analytics.generatorEvent('load-preset', id) setSeachParams({ version, preset: id, [SHARE_KEY]: undefined }) } const loadPreset = async (id: string) => { try { const preset = await fetchPreset(version, gen.path ?? gen.id, id) const seed = model?.get(new Path(['generator', 'seed'])) if (preset?.generator?.seed !== undefined && seed !== undefined) { preset.generator.seed = seed if (preset.generator.biome_source?.seed !== undefined) { preset.generator.biome_source.seed = seed } } return preset } catch (e) { setError(e instanceof Error ? e : message(e)) } } const selectVersion = (version: VersionId) => { setSeachParams({ [SHARE_KEY]: undefined }) changeVersion(version) } const [shareUrl, setShareUrl] = useState(undefined) const [shareShown, setShareShown] = useState(false) const [shareCopyActive, shareCopySuccess] = useActiveTimeout({ cooldown: 3000 }) const share = () => { if (shareShown) { setShareShown(false) return } if (currentPreset) { setShareUrl(`${location.protocol}//${location.host}/${gen.url}/?version=${version}&preset=${currentPreset}`) setShareShown(true) copySharedId() } else if (model && blockStates) { const output = getOutput(model, blockStates) if (deepEqual(output, model.schema.default())) { setShareUrl(`${location.protocol}//${location.host}/${gen.url}/?version=${version}`) setShareShown(true) } else { shareSnippet(gen.id, version, output, previewShown) .then(url => { setShareUrl(url) setShareShown(true) }) .catch(e => { if (e instanceof Error) { setError(e) } }) } } } const copySharedId = () => { navigator.clipboard.writeText(shareUrl ?? '') shareCopySuccess() } useEffect(() => { if (!shareCopyActive) { setShareUrl(undefined) setShareShown(false) } }, [shareCopyActive]) const [sourceShown, setSourceShown] = useState(window.innerWidth > 820) const [doCopy, setCopy] = useState(0) const [doDownload, setDownload] = useState(0) const [doImport, setImport] = useState(0) const copySource = () => { Analytics.generatorEvent('copy') setCopy(doCopy + 1) } const downloadSource = () => { Analytics.generatorEvent('download') setDownload(doDownload + 1) } const importSource = () => { Analytics.generatorEvent('import') setSourceShown(true) setImport(doImport + 1) } const toggleSource = () => { Analytics.generatorEvent('toggle-output', !sourceShown ? 'visible' : 'hidden') setSourceShown(!sourceShown) setCopy(0) setDownload(0) setImport(0) } const [copyActive, copySuccess] = useActiveTimeout() const [previewShown, setPreviewShown] = useState(false) const hasPreview = HasPreview.includes(gen.id) && !(gen.id === 'worldgen/configured_feature' && checkVersion(version, '1.18')) if (previewShown && !hasPreview) setPreviewShown(false) let actionsShown = 2 if (hasPreview) actionsShown += 1 if (sourceShown) actionsShown += 2 const togglePreview = () => { Analytics.generatorEvent('toggle-preview', !previewShown ? 'visible' : 'hidden') setPreviewShown(!previewShown) if (!previewShown && sourceShown) { setSourceShown(false) } } return <>
route('/project')} /> {file && } f.type === gen.id).map(f => f.id)} onSelect={(id) => openFile(gen.id, id)} /> {file && }
{dirty ?
{Octicon.dot_fill}
: fileSaved ?
{Octicon.check}
: fileError &&
{Octicon.x}
}
{allowedVersions.reverse().map(v => selectVersion(v)} /> )}
{error && setError(null)} />}
}