import { Identifier } from 'deepslate' import type { ComponentChildren } from 'preact' import { createContext } from 'preact' import { useCallback, useContext, useState } from 'preact/hooks' import type { ProjectData } from '../components/previews/Deepslate.js' import config from '../Config.js' import { useAsync } from '../hooks/useAsync.js' import type { VersionId } from '../services/index.js' import { DEFAULT_VERSION } from '../services/index.js' import { DRAFTS_URI, PROJECTS_URI, SpyglassClient } from '../services/Spyglass.js' import { Store } from '../Store.js' import { genPath, hexId, message, safeJsonParse } from '../Utils.js' export type ProjectMeta = { name: string, namespace?: string, version?: VersionId, storage?: ProjectStorage, /** @deprecated */ files?: ProjectFile[], /** @deprecated */ unknownFiles?: UnknownFile[], } export type ProjectStorage = { type: 'indexeddb', rootUri: string, } type ProjectFile = { type: string, id: string, data: any, } type UnknownFile = { path: string, data: string, } export const DRAFT_PROJECT: ProjectMeta = { name: 'Drafts', namespace: 'draft', storage: { type: 'indexeddb', rootUri: DRAFTS_URI, }, } interface ProjectContext { projects: ProjectMeta[], project: ProjectMeta, projectUri: string | undefined, setProjectUri: (uri: string | undefined) => void, createProject: (project: ProjectMeta) => void, deleteProject: (name: string) => void, changeProject: (name: string) => void, updateProject: (project: Partial) => void, } const Project = createContext(undefined) export function useProject() { const context = useContext(Project) if (context === undefined) { throw new Error('Cannot use project outside of provider') } return context } export function ProjectProvider({ children }: { children: ComponentChildren }) { const [projects, setProjects] = useState(Store.getProjects()) const [openProject, setOpenProject] = useState(Store.getOpenProject()) const { value: project} = useAsync(async () => { const project = projects.find(p => p.name === openProject) if (!project) { if (openProject !== undefined && openProject !== DRAFT_PROJECT.name) { console.warn(`Cannot find project ${openProject} to open`) } return DRAFT_PROJECT } if (project.storage === undefined) { try { const projectRoot = `${PROJECTS_URI}${hexId()}/` console.log(`Upgrading project ${openProject} to IndexedDB at ${projectRoot}`) await SpyglassClient.FS.mkdir(projectRoot) if (project.files) { await Promise.all(project.files.map(async file => { const gen = config.generators.find(g => g.id === file.type) if (!gen) { console.warn(`Could not upgrade file ${file.id} of type ${file.type}, no generator found!`) return } const type = genPath(gen, project.version ?? DEFAULT_VERSION) const { namespace, path } = Identifier.parse(file.id) const uri = type === 'pack_mcmeta' ? `${projectRoot}data/pack.mcmeta` : `${projectRoot}data/${namespace}/${type}/${path}.json` return SpyglassClient.FS.writeFile(uri, JSON.stringify(file.data, null, 2)) })) } if (project.unknownFiles) { await Promise.all(project.unknownFiles.map(async file => { const uri = projectRoot + file.path return SpyglassClient.FS.writeFile(uri, file.data) })) } changeProjects(projects.map(p => p === project ? { ...p, storage: { type: 'indexeddb', rootUri: projectRoot } } : p)) } catch (e) { console.error(`Something went wrong upgrading project ${openProject}: ${message(e)}`) return DRAFT_PROJECT } } return project }, [projects, openProject]) const [projectUri, setProjectUri] = useState() const changeProjects = useCallback((projects: ProjectMeta[]) => { Store.setProjects(projects) setProjects(projects) }, []) const createProject = useCallback((project: ProjectMeta) => { changeProjects([...projects, project]) }, [projects]) const deleteProject = useCallback(async (name: string) => { if (name === DRAFT_PROJECT.name) return const project = projects.find(p => p.name === name) if (project) { const projectRoot = getProjectRoot(project) const entries = await SpyglassClient.FS.readdir(projectRoot) await Promise.all(entries.map(async e => SpyglassClient.FS.unlink(e.name))) } changeProjects(projects.filter(p => p.name !== name)) setOpenProject(DRAFT_PROJECT.name) }, [projects]) const changeProject = useCallback((name: string) => { Store.setOpenProject(name) setOpenProject(name) }, []) const updateProject = useCallback((edits: Partial) => { changeProjects(projects.map(p => p.name === openProject ? { ...p, ...edits } : p)) }, [projects, openProject]) const value: ProjectContext = { projects, project: project ?? DRAFT_PROJECT, projectUri, setProjectUri, createProject, changeProject, deleteProject, updateProject, } return {children} } export function getProjectRoot(project: ProjectMeta) { if (project.storage?.type === 'indexeddb') { return project.storage.rootUri } throw new Error(`Unsupported project storage ${project.storage?.type}`) } export async function getWorldgenProjectData(project: ProjectMeta): Promise { const projectRoot = getProjectRoot(project) const categories = ['worldgen/noise_settings', 'worldgen/noise', 'worldgen/density_function'] const result: ProjectData = Object.fromEntries(categories.map(c => [c, {}])) const entries = await SpyglassClient.FS.readdir(projectRoot) for (const entry of entries) { for (const category of categories) { if (entry.name.includes(category)) { const pattern = RegExp(`data/([a-z0-9_.-]+)/${category}/([a-z0-9_./-]+).json$`) const match = entry.name.match(pattern) if (match) { const data = await SpyglassClient.FS.readFile(entry.name) const text = new TextDecoder().decode(data) result[category][`${match[1]}:${match[2]}`] = safeJsonParse(text) } } } } return result }