diff --git a/package-lock.json b/package-lock.json index b3ddb865..ffdc16b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5033,9 +5033,9 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "typescript": { - "version": "3.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.3.tgz", - "integrity": "sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ==" + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", + "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==" }, "union-value": { "version": "1.0.1", diff --git a/package.json b/package.json index aaac94a2..e4d74f3c 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "selfsigned": "^1.10.8", "split.js": "^1.5.11", "ts-loader": "^7.0.4", - "typescript": "^3.9.3", + "typescript": "^4.1.3", "webpack": "^4.44.2", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.11.0" diff --git a/src/app/App.ts b/src/app/App.ts index a89a50c2..9a8c2299 100644 --- a/src/app/App.ts +++ b/src/app/App.ts @@ -5,7 +5,7 @@ import * as java17 from '@mcschema/java-1.17' import { LocalStorageProperty } from './state/LocalStorageProperty'; import { Property } from './state/Property'; import { Preview } from './preview/Preview'; -import { RegistryFetcher } from './RegistryFetcher'; +import { fetchData } from './DataFetcher'; import { BiomeNoisePreview } from './preview/BiomeNoisePreview'; import { NoiseSettingsPreview } from './preview/NoiseSettingsPreview'; import { DecoratorPreview } from './preview/DecoratorPreview'; @@ -14,7 +14,7 @@ import { locale, Locales } from './Locales'; import { Tracker } from './Tracker'; import { Settings } from './Settings'; -const Versions: { +export const Versions: { [versionId: string]: { getCollections: () => CollectionRegistry, getSchemas: (collections: CollectionRegistry) => SchemaRegistry, @@ -40,6 +40,17 @@ export const Models: { config.models.filter(m => m.schema) .forEach(m => Models[m.id] = new DataModel(ObjectNode({}))) +export const BlockStateRegistry: { + [block: string]: { + properties: { + [key: string]: string[] + }, + default: { + [key: string]: string + } + } +} = {} + export const App = { version: new LocalStorageProperty('schema_version', config.versions[config.versions.length - 1].id) .watch(Tracker.dimVersion), @@ -96,7 +107,7 @@ App.mobilePanel.watchRun((value) => { async function updateSchemas(version: string) { const collections = Versions[version].getCollections() - await RegistryFetcher(collections, version) + await fetchData(collections, version) const schemas = Versions[version].getSchemas(collections) config.models .filter(m => m.schema) diff --git a/src/app/RegistryFetcher.ts b/src/app/DataFetcher.ts similarity index 58% rename from src/app/RegistryFetcher.ts rename to src/app/DataFetcher.ts index 1197f097..9c1d4b08 100644 --- a/src/app/RegistryFetcher.ts +++ b/src/app/DataFetcher.ts @@ -1,7 +1,12 @@ import { CollectionRegistry } from '@mcschema/core' -import { checkVersion } from './App' +import { BlockStateRegistry, checkVersion } from './App' import config from '../config.json' +type VersionConfig = { + id: string, + mcdata_ref: string +} + type RegistryConfig = { id: string minVersion?: string @@ -13,24 +18,39 @@ const localStorageCache = (version: string) => `cache_${version}` declare var __MCDATA_MASTER_HASH__: string; const baseUrl = 'https://raw.githubusercontent.com/Arcensoth/mcdata' -export const mcdata = (ref: string, registry: string) => { +const mcdata = (ref: string, registry: string) => { return `${baseUrl}/${ref}/processed/reports/registries/${registry}/data.min.json` } -export const RegistryFetcher = async (target: CollectionRegistry, versionId: string) => { +export const fetchData = async (target: CollectionRegistry, versionId: string) => { const version = config.versions.find(v => v.id === versionId) if (!version) return const cache = JSON.parse(localStorage.getItem(localStorageCache(versionId)) ?? '{}') const cacheValid = version.mcdata_ref !== 'master' || cache.mcdata_hash === __MCDATA_MASTER_HASH__ - let cacheDirty = false - if (checkVersion('1.15', versionId)) { + const cacheDirty = (await Promise.all([ + fetchRegistries(target, version, cache, cacheValid), + fetchBlockStateMap(version, cache, cacheValid) + ])).some(v => v) + + if (cacheDirty) { + if (version.mcdata_ref === 'master') { + cache.mcdata_hash = __MCDATA_MASTER_HASH__ + } + localStorage.setItem(localStorageCache(versionId), JSON.stringify(cache)) + } +} + +const fetchRegistries = async (target: CollectionRegistry, version: VersionConfig, cache: any, cacheValid: boolean) => { + let cacheDirty = false + if (!cache.registries) cache.registries = {} + if (checkVersion('1.15', version.id)) { const url = `${baseUrl}/${version.mcdata_ref}/generated/reports/registries.json` if (cacheValid && cache.registries) { config.registries.forEach((r: string | RegistryConfig) => { if (typeof r === 'string') r = { id: r } - if (!checkVersion(versionId, r.minVersion, r.maxVersion)) return + if (!checkVersion(version.id, r.minVersion, r.maxVersion)) return target.register(r.id, cache.registries[r.id]) }) @@ -40,26 +60,22 @@ export const RegistryFetcher = async (target: CollectionRegistry, versionId: str const data = await res.json() config.registries.forEach(async (r: string | RegistryConfig) => { if (typeof r === 'string') r = { id: r } - if (!checkVersion(versionId, r.minVersion, r.maxVersion)) return + if (!checkVersion(version.id, r.minVersion, r.maxVersion)) return - if (!cache.registries) cache.registries = {} const values = Object.keys(data[`minecraft:${r.id}`].entries) target.register(r.id, values) cache.registries[r.id] = values cacheDirty = true }) } catch (e) { - console.warn(`Error occurred while fetching registries for version ${versionId}`) + console.warn(`Error occurred while fetching registries for version ${version.id}`) } } } else { await Promise.all(config.registries.map(async (r: string | RegistryConfig) => { if (typeof r === 'string') r = { id: r } - - if (r.minVersion && !checkVersion(versionId, r.minVersion)) return - if (r.maxVersion && !checkVersion(r.maxVersion, versionId)) return - - if (!cache.registries) cache.registries = {} + if (!checkVersion(version.id, r.minVersion, r.maxVersion)) return + if (cacheValid && cache.registries?.[r.id]) { target.register(r.id, cache.registries[r.id]) return @@ -81,11 +97,33 @@ export const RegistryFetcher = async (target: CollectionRegistry, versionId: str } })) } - - if (cacheDirty) { - if (version.mcdata_ref === 'master') { - cache.mcdata_hash = __MCDATA_MASTER_HASH__ - } - localStorage.setItem(localStorageCache(versionId), JSON.stringify(cache)) - } + return cacheDirty +} + +const fetchBlockStateMap = async (version: VersionConfig, cache: any, cacheValid: boolean) => { + if (cacheValid && cache.block_state_map) { + Object.keys(cache.block_state_map).forEach(block => { + BlockStateRegistry[block] = cache.block_state_map[block] + }) + return false + } + + const url = (checkVersion(version.id, undefined, '1.15')) + ? `${baseUrl}/${version.mcdata_ref}/generated/reports/blocks.json` + : `${baseUrl}/${version.mcdata_ref}/processed/reports/blocks/data.min.json` + + const res = await fetch(url) + const data = await res.json() + + cache.block_state_map = {} + Object.keys(data).forEach(block => { + const res = { + properties: data[block].properties, + default: data[block].states.find((s: any) => s.default).properties + } + BlockStateRegistry[block] = res + cache.block_state_map[block] = res + }) + + return true } diff --git a/src/app/hooks/customValidation.ts b/src/app/hooks/customValidation.ts new file mode 100644 index 00000000..3bff1cce --- /dev/null +++ b/src/app/hooks/customValidation.ts @@ -0,0 +1,36 @@ +import { Errors, Hook, relativePath } from '@mcschema/core' +import { BlockStateRegistry } from '../App' +import { walk } from './walk' + +export const customValidation: Hook<[any, Errors], void> = walk<[Errors]>({ + base() {}, + + boolean() {}, + + choice() {}, + + list() {}, + + map({ config }, path, value) { + if (config.validation?.validator === 'block_state_map') { + const block = relativePath(path, config.validation.params.id).get() + const errors = path.getModel().errors + + const requiredProps = BlockStateRegistry[block].properties ?? {} + const existingKeys = Object.keys(value ?? {}) + Object.keys(requiredProps).forEach(p => { + if (!existingKeys.includes(p)) { + errors.add(path, 'error.block_state.missing_property', p) + } else if (!requiredProps[p].includes(value[p])) { + errors.add(path.push(p), 'error.invalid_enum_option', value[p]) + } + }) + } + }, + + number() {}, + + object() {}, + + string() {} +}) diff --git a/src/app/hooks/walk.ts b/src/app/hooks/walk.ts new file mode 100644 index 00000000..300bebb3 --- /dev/null +++ b/src/app/hooks/walk.ts @@ -0,0 +1,39 @@ +import { Hook } from '@mcschema/core' + +type Args = any[] + +export const walk = (hook: Hook<[any, ...U], void>): Hook<[any, ...U], void> => ({ + ...hook, + + choice(params, path, value, ...args) { + hook.choice(params, path, value, ...args) + params.switchNode.hook(this, path, value, ...args) + }, + + list(params, path, value, ...args) { + hook.list(params, path, value, ...args) + if (!Array.isArray(value)) return + value.forEach((e, i) => + params.children.hook(this, path.push(i), value, ...args) + ) + }, + + map(params, path, value, ...args) { + hook.map(params, path, value, ...args) + if (typeof value !== 'object') return + Object.keys(value).forEach(f => + params.children.hook(this, path.push(f), value[f], ...args) + ) + }, + + object(params, path, value, ...args) { + hook.object(params, path, value, ...args) + if (value === null || typeof value !== 'object') return + const activeFields = params.getActiveFields(path) + Object.keys(activeFields) + .filter(f => activeFields[f].enabled(path)) + .forEach(f => { + activeFields[f].hook(this, path.push(f), value[f], ...args) + }) + } +}) diff --git a/src/app/views/Generator.ts b/src/app/views/Generator.ts index f3d5e5da..c049e03e 100644 --- a/src/app/views/Generator.ts +++ b/src/app/views/Generator.ts @@ -6,6 +6,8 @@ import { Errors } from '../components/Errors' import { TreePanel } from '../components/panels/TreePanel' import { SourcePanel } from '../components/panels/SourcePanel' import { PreviewPanel } from '../components/panels/PreviewPanel' +import { customValidation } from '../hooks/customValidation' +import { ModelPath, Path } from '@mcschema/core' export const Generator = (view: View): string => { const model = Models[App.model.get()!.id] @@ -26,7 +28,10 @@ export const Generator = (view: View): string => { } } model.addListener({ - invalidated: validatePreview + invalidated: () => { + validatePreview() + model.schema.hook(customValidation, new ModelPath(model, new Path()), model.data, model.errors) + } }) App.schemasLoaded.watch((value) => { if (value) { diff --git a/src/locales/en.json b/src/locales/en.json index fa3f03bb..10373d8e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -4,6 +4,7 @@ "dimension-type": "Dimension Type", "dimension": "Dimension", "download": "Download", + "error.block_state.missing_property": "Missing block property \"%0%\"", "fields": "Fields", "item-modifier": "Item Modifier", "language": "Language",