mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-26 08:26:51 +00:00
Move more stuff to services folder
This commit is contained in:
316
src/app/services/DataFetcher.ts
Normal file
316
src/app/services/DataFetcher.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import type { CollectionRegistry } from '@mcschema/core'
|
||||
import config from '../../config.json'
|
||||
import { message } from '../Utils'
|
||||
import type { VersionAssets, VersionManifest } from './Manifest'
|
||||
import type { BlockStateRegistry, VersionId } from './Schemas'
|
||||
import { checkVersion } from './Schemas'
|
||||
|
||||
['1.15', '1.16', '1.17'].forEach(v => localStorage.removeItem(`cache_${v}`))
|
||||
|
||||
const CACHE_NAME = 'misode-v1'
|
||||
|
||||
type VersionRef = 'mcdata_master' | 'vanilla_datapack_summary' | 'vanilla_datapack_data'
|
||||
|
||||
type Version = {
|
||||
id: string,
|
||||
refs: Partial<{ [key in VersionRef]: string }>,
|
||||
dynamic?: boolean,
|
||||
}
|
||||
|
||||
declare var __MCDATA_MASTER_HASH__: string
|
||||
declare var __VANILLA_DATAPACK_SUMMARY_HASH__: string
|
||||
|
||||
const mcdataUrl = 'https://raw.githubusercontent.com/Arcensoth/mcdata'
|
||||
const vanillaDatapackUrl = 'https://raw.githubusercontent.com/SPGoding/vanilla-datapack'
|
||||
const manifestUrl = 'https://launchermeta.mojang.com/mc/game/version_manifest.json'
|
||||
const resourceUrl = 'https://resources.download.minecraft.net/'
|
||||
const corsUrl = 'https://misode-cors-anywhere.herokuapp.com/'
|
||||
|
||||
const refs: {
|
||||
id: VersionRef,
|
||||
hash: string,
|
||||
url: string,
|
||||
}[] = [
|
||||
{
|
||||
id: 'mcdata_master',
|
||||
hash: __MCDATA_MASTER_HASH__,
|
||||
url: mcdataUrl,
|
||||
},
|
||||
{
|
||||
id: 'vanilla_datapack_summary',
|
||||
hash: __VANILLA_DATAPACK_SUMMARY_HASH__,
|
||||
url: vanillaDatapackUrl,
|
||||
},
|
||||
]
|
||||
|
||||
export async function fetchData(versionId: string, collectionTarget: CollectionRegistry, blockStateTarget: BlockStateRegistry) {
|
||||
const version = config.versions.find(v => v.id === versionId) as Version | undefined
|
||||
if (!version) {
|
||||
console.error(`[fetchData] Unknown version ${version} in ${JSON.stringify(config.versions)}`)
|
||||
return
|
||||
}
|
||||
console.debug(`[fetchData] ${JSON.stringify(version)}`)
|
||||
|
||||
if (version.dynamic) {
|
||||
await Promise.all(refs
|
||||
.filter(r => localStorage.getItem(`cached_${r.id}`) !== r.hash)
|
||||
.map(async r => {
|
||||
console.debug(`[deleteMatching] ${r.id} '${localStorage.getItem(`cached_${r.id}`)}' < '${r.hash}' ${r.url}/${version.refs[r.id]}`)
|
||||
await deleteMatching(url => url.startsWith(`${r.url}/${version.refs[r.id]}`))
|
||||
console.debug(`[deleteMatching] Done! ${r.id} ${r.hash} '${localStorage.getItem(`cached_${r.id}`)}'`)
|
||||
localStorage.setItem(`cached_${r.id}`, r.hash)
|
||||
console.debug(`[deleteMatching] Set! ${r.id} ${r.hash} '${localStorage.getItem(`cached_${r.id}`)}'`)
|
||||
}))
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
fetchRegistries(version, collectionTarget),
|
||||
fetchBlockStateMap(version, blockStateTarget),
|
||||
fetchDynamicRegistries(version, collectionTarget),
|
||||
])
|
||||
}
|
||||
|
||||
async function fetchRegistries(version: Version, target: CollectionRegistry) {
|
||||
console.debug(`[fetchRegistries] ${version.id}`)
|
||||
const registries = config.registries
|
||||
.filter(r => !r.dynamic)
|
||||
.filter(r => checkVersion(version.id, r.minVersion, r.maxVersion))
|
||||
|
||||
if (checkVersion(version.id, undefined, '1.15')) {
|
||||
const url = `${mcdataUrl}/${version.refs.mcdata_master}/generated/reports/registries.json`
|
||||
try {
|
||||
const data = await getData(url, (data) => {
|
||||
const res: {[id: string]: string[]} = {}
|
||||
Object.keys(data).forEach(k => {
|
||||
res[k.slice(10)] = Object.keys(data[k].entries)
|
||||
})
|
||||
return res
|
||||
})
|
||||
registries.forEach(r => {
|
||||
target.register(r.id, data[r.id] ?? [])
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('Error occurred while fetching registries:', message(e))
|
||||
}
|
||||
} else {
|
||||
await Promise.all(registries.map(async r => {
|
||||
try {
|
||||
const url = r.path
|
||||
? `${mcdataUrl}/${version.refs.mcdata_master}/${r.path}/data.min.json`
|
||||
: `${mcdataUrl}/${version.refs.mcdata_master}/processed/reports/registries/${r.id}/data.min.json`
|
||||
target.register(r.id, await getData(url, v => v.values))
|
||||
} catch (e) {
|
||||
console.warn(`Error occurred while fetching registry ${r.id}:`, message(e))
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchBlockStateMap(version: Version, target: BlockStateRegistry) {
|
||||
console.debug(`[fetchBlockStateMap] ${version.id}`)
|
||||
if (checkVersion(version.id, undefined, '1.16')) {
|
||||
const url = (checkVersion(version.id, undefined, '1.15'))
|
||||
? `${mcdataUrl}/${version.refs.mcdata_master}/generated/reports/blocks.json`
|
||||
: `${mcdataUrl}/${version.refs.mcdata_master}/processed/reports/blocks/data.min.json`
|
||||
|
||||
try {
|
||||
const data = await getData(url, (data) => {
|
||||
const res: BlockStateRegistry = {}
|
||||
Object.keys(data).forEach(b => {
|
||||
res[b] = {
|
||||
properties: data[b].properties,
|
||||
default: data[b].states.find((s: any) => s.default).properties,
|
||||
}
|
||||
})
|
||||
return res
|
||||
})
|
||||
Object.assign(target, data)
|
||||
} catch (e) {
|
||||
console.warn('Error occurred while fetching block state map:', message(e))
|
||||
}
|
||||
} else {
|
||||
const url = `${mcdataUrl}/${version.refs.mcdata_master}/processed/reports/blocks/simplified/data.min.json`
|
||||
try {
|
||||
const data = await getData(url)
|
||||
Object.assign(target, data)
|
||||
} catch (e) {
|
||||
console.warn('Error occurred while fetching block state map:', message(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDynamicRegistries(version: Version, target: CollectionRegistry) {
|
||||
console.debug(`[fetchDynamicRegistries] ${version.id}`)
|
||||
const registries = config.registries
|
||||
.filter(r => r.dynamic)
|
||||
.filter(r => checkVersion(version.id, r.minVersion, r.maxVersion))
|
||||
|
||||
if (checkVersion(version.id, '1.16')) {
|
||||
const url = `${vanillaDatapackUrl}/${version.refs.vanilla_datapack_summary}/summary/flattened.min.json`
|
||||
try {
|
||||
const data = await getData(url)
|
||||
registries.forEach(r => {
|
||||
target.register(r.id, data[r.id])
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('Error occurred while fetching dynamic registries:', message(e))
|
||||
}
|
||||
}
|
||||
if (checkVersion(version.id, '1.18')) {
|
||||
target.register('worldgen/noise', Noises)
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchPreset(version: VersionId, registry: string, id: string) {
|
||||
console.debug(`[fetchPreset] ${registry} ${id}`)
|
||||
const versionData = config.versions.find(v => v.id === version)!
|
||||
try {
|
||||
const url = `${vanillaDatapackUrl}/${versionData.refs.vanilla_datapack_data}/data/minecraft/${registry}/${id}.json`
|
||||
const res = await fetch(url)
|
||||
if (registry === 'worldgen/noise_settings' && version === '1.18') {
|
||||
let text = await res.text()
|
||||
text = text.replaceAll('"max_threshold": Infinity', '"max_threshold": 100')
|
||||
const data = JSON.parse(text)
|
||||
if (id !== 'overworld' && id !== 'large_biomes') {
|
||||
data.noise.terrain_shaper = { offset: 0, factor: 0, jaggedness: 0 }
|
||||
}
|
||||
return data
|
||||
}
|
||||
return await res.json()
|
||||
} catch (e) {
|
||||
console.warn(`Error occurred while fetching ${registry} preset ${id}:`, message(e))
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchManifest() {
|
||||
try {
|
||||
const res = await fetch(manifestUrl)
|
||||
return await res.json()
|
||||
} catch (e) {
|
||||
throw new Error(`Error occurred while fetching version manifest: ${message(e)}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAssets(versionId: VersionId, manifest: VersionManifest) {
|
||||
const version = config.versions.find(v => v.id === versionId)
|
||||
const id = version?.latest ?? manifest.latest.snapshot
|
||||
try {
|
||||
const versionMeta = await getData(manifest.versions.find(v => v.id === id)!.url)
|
||||
|
||||
return (await getData(versionMeta.assetIndex.url)).objects
|
||||
} catch (e) {
|
||||
throw new Error(`Error occurred while fetching assets for ${version}: ${message(e)}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchSounds(version: VersionId, assets: VersionAssets) {
|
||||
try {
|
||||
const hash = assets['minecraft/sounds.json'].hash
|
||||
return await getData(getResourceUrl(hash))
|
||||
} catch (e) {
|
||||
throw new Error(`Error occurred while fetching sounds for ${version}: ${message(e)}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function getResourceUrl(hash: string) {
|
||||
return `${corsUrl}${resourceUrl}${hash.slice(0, 2)}/${hash}`
|
||||
}
|
||||
|
||||
async function getData<T = any>(url: string, fn: (v: any) => T = (v: any) => v): Promise<T> {
|
||||
try {
|
||||
const cache = await caches.open(CACHE_NAME)
|
||||
console.debug(`[getData] Opened cache ${CACHE_NAME} ${url}`)
|
||||
const cacheResponse = await cache.match(url)
|
||||
|
||||
if (cacheResponse && cacheResponse.ok) {
|
||||
console.debug(`[getData] Retrieving cached data ${url}`)
|
||||
return await cacheResponse.json()
|
||||
}
|
||||
|
||||
console.debug(`[getData] fetching data ${url}`)
|
||||
const fetchResponse = await fetch(url)
|
||||
const responseData = fn(await fetchResponse.json())
|
||||
await cache.put(url, new Response(JSON.stringify(responseData)))
|
||||
return responseData
|
||||
} catch (e) {
|
||||
console.warn(`[getData] Failed to open cache ${CACHE_NAME}: ${message(e)}`)
|
||||
|
||||
console.debug(`[getData] fetching data ${url}`)
|
||||
const fetchResponse = await fetch(url)
|
||||
const responseData = fn(await fetchResponse.json())
|
||||
return responseData
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMatching(matches: (url: string) => boolean) {
|
||||
try {
|
||||
const cache = await caches.open(CACHE_NAME)
|
||||
console.debug(`[deleteMatching] Opened cache ${CACHE_NAME}`)
|
||||
const promises: Promise<boolean>[] = []
|
||||
|
||||
for (const request of await cache.keys()) {
|
||||
if (matches(request.url)) {
|
||||
promises.push(cache.delete(request))
|
||||
}
|
||||
}
|
||||
console.debug(`[deleteMatching] Removing ${promises.length} cache objects...`)
|
||||
await Promise.all(promises)
|
||||
} catch (e) {
|
||||
console.warn(`[deleteMatching] Failed to open cache ${CACHE_NAME}: ${message(e)}`)
|
||||
}
|
||||
}
|
||||
|
||||
const Noises = [
|
||||
'minecraft:aquifer_barrier',
|
||||
'minecraft:aquifer_fluid_level_floodedness',
|
||||
'minecraft:aquifer_fluid_level_spread',
|
||||
'minecraft:aquifer_lava',
|
||||
'minecraft:calcite',
|
||||
'minecraft:cave_cheese',
|
||||
'minecraft:cave_entrance',
|
||||
'minecraft:cave_layer',
|
||||
'minecraft:clay_bands_offset',
|
||||
'minecraft:continentalness',
|
||||
'minecraft:erosion',
|
||||
'minecraft:gravel',
|
||||
'minecraft:gravel_layer',
|
||||
'minecraft:ice',
|
||||
'minecraft:iceberg_and_badlands_pillar',
|
||||
'minecraft:iceberg_and_badlands_pillar_roof',
|
||||
'minecraft:jagged',
|
||||
'minecraft:nether_state_selector',
|
||||
'minecraft:nether_wart',
|
||||
'minecraft:netherrack',
|
||||
'minecraft:noodle',
|
||||
'minecraft:noodle_ridge_a',
|
||||
'minecraft:noodle_ridge_b',
|
||||
'minecraft:noodle_thickness',
|
||||
'minecraft:offset',
|
||||
'minecraft:ore_gap',
|
||||
'minecraft:ore_vein_a',
|
||||
'minecraft:ore_vein_b',
|
||||
'minecraft:ore_veininess',
|
||||
'minecraft:packed_ice',
|
||||
'minecraft:patch',
|
||||
'minecraft:pillar',
|
||||
'minecraft:pillar_rareness',
|
||||
'minecraft:pillar_thickness',
|
||||
'minecraft:powder_snow_surface',
|
||||
'minecraft:powder_snow_under',
|
||||
'minecraft:ridge',
|
||||
'minecraft:soul_sand_layer',
|
||||
'minecraft:spaghetti_2d',
|
||||
'minecraft:spaghetti_2d_elevation',
|
||||
'minecraft:spaghetti_2d_modulator',
|
||||
'minecraft:spaghetti_2d_thickness',
|
||||
'minecraft:spaghetti_3d_1',
|
||||
'minecraft:spaghetti_3d_2',
|
||||
'minecraft:spaghetti_3d_rarity',
|
||||
'minecraft:spaghetti_3d_thickness',
|
||||
'minecraft:spaghetti_roughness',
|
||||
'minecraft:spaghetti_roughness_modulator',
|
||||
'minecraft:surface',
|
||||
'minecraft:surface_swamp',
|
||||
'minecraft:temperature',
|
||||
'minecraft:vegetation',
|
||||
]
|
||||
56
src/app/services/Manifest.ts
Normal file
56
src/app/services/Manifest.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { fetchAssets, fetchManifest, fetchSounds } from './DataFetcher'
|
||||
import type { VersionId } from './Schemas'
|
||||
|
||||
export type VersionManifest = {
|
||||
latest: {
|
||||
release: string,
|
||||
snapshot: string,
|
||||
},
|
||||
versions: {
|
||||
id: string,
|
||||
type: string,
|
||||
url: string,
|
||||
}[],
|
||||
}
|
||||
let Manifest: VersionManifest | Promise<VersionManifest> | null = null
|
||||
|
||||
export type VersionAssets = {
|
||||
[key: string]: {
|
||||
hash: string,
|
||||
},
|
||||
}
|
||||
const VersionAssets: Record<string, VersionAssets | Promise<VersionAssets>> = {}
|
||||
|
||||
export type SoundEvents = {
|
||||
[key: string]: {
|
||||
sounds: (string | { name: string })[],
|
||||
},
|
||||
}
|
||||
const SoundEvents: Record<string, SoundEvents | Promise<SoundEvents>> = {}
|
||||
|
||||
export async function getManifest() {
|
||||
if (!Manifest) {
|
||||
Manifest = fetchManifest()
|
||||
}
|
||||
return Manifest
|
||||
}
|
||||
|
||||
export async function getAssets(version: VersionId) {
|
||||
if (!VersionAssets[version]) {
|
||||
VersionAssets[version] = (async () => {
|
||||
const manifest = await getManifest()
|
||||
return await fetchAssets(version, manifest)
|
||||
})()
|
||||
}
|
||||
return VersionAssets[version]
|
||||
}
|
||||
|
||||
export async function getSounds(version: VersionId) {
|
||||
if (!SoundEvents[version]) {
|
||||
SoundEvents[version] = (async () => {
|
||||
const assets = await getAssets(version)
|
||||
return await fetchSounds(version, assets)
|
||||
})()
|
||||
}
|
||||
return SoundEvents[version]
|
||||
}
|
||||
126
src/app/services/Schemas.ts
Normal file
126
src/app/services/Schemas.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import type { CollectionRegistry, INode, SchemaRegistry } from '@mcschema/core'
|
||||
import { ChoiceNode, DataModel, Reference, StringNode } from '@mcschema/core'
|
||||
import * as java15 from '@mcschema/java-1.15'
|
||||
import * as java16 from '@mcschema/java-1.16'
|
||||
import * as java17 from '@mcschema/java-1.17'
|
||||
import * as java18 from '@mcschema/java-1.18'
|
||||
import config from '../../config.json'
|
||||
import { message } from '../Utils'
|
||||
import { fetchData } from './DataFetcher'
|
||||
|
||||
export const VersionIds = ['1.15', '1.16', '1.17', '1.18'] as const
|
||||
export type VersionId = typeof VersionIds[number]
|
||||
|
||||
export type BlockStateRegistry = {
|
||||
[block: string]: {
|
||||
properties?: {
|
||||
[key: string]: string[],
|
||||
},
|
||||
default?: {
|
||||
[key: string]: string,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type VersionData = {
|
||||
collections: CollectionRegistry,
|
||||
schemas: SchemaRegistry,
|
||||
blockStates: BlockStateRegistry,
|
||||
}
|
||||
const Versions: Record<string, VersionData | Promise<VersionData>> = {}
|
||||
|
||||
type ModelData = {
|
||||
model: DataModel,
|
||||
version: VersionId,
|
||||
}
|
||||
const Models: Record<string, ModelData> = {}
|
||||
|
||||
const versionGetter: {
|
||||
[versionId in VersionId]: {
|
||||
getCollections: () => CollectionRegistry,
|
||||
getSchemas: (collections: CollectionRegistry) => SchemaRegistry,
|
||||
}
|
||||
} = {
|
||||
1.15: java15,
|
||||
1.16: java16,
|
||||
1.17: java17,
|
||||
1.18: java18,
|
||||
}
|
||||
|
||||
export let CachedDecorator: INode<any>
|
||||
export let CachedFeature: INode<any>
|
||||
|
||||
async function getVersion(id: VersionId): Promise<VersionData> {
|
||||
if (!Versions[id]) {
|
||||
Versions[id] = (async () => {
|
||||
try {
|
||||
const collections = versionGetter[id].getCollections()
|
||||
const blockStates: BlockStateRegistry = {}
|
||||
await fetchData(id, collections, blockStates)
|
||||
const schemas = versionGetter[id].getSchemas(collections)
|
||||
Versions[id] = { collections, schemas, blockStates }
|
||||
return Versions[id]
|
||||
} catch (e) {
|
||||
throw new Error(`Cannot get version "${id}": ${message(e)}`)
|
||||
}
|
||||
})()
|
||||
return Versions[id]
|
||||
}
|
||||
return Versions[id]
|
||||
}
|
||||
|
||||
export async function getModel(version: VersionId, id: string): Promise<DataModel> {
|
||||
if (!Models[id] || Models[id].version !== version) {
|
||||
const versionData = await getVersion(version)
|
||||
|
||||
CachedDecorator = Reference(versionData.schemas, 'configured_decorator')
|
||||
CachedFeature = ChoiceNode([
|
||||
{
|
||||
type: 'string',
|
||||
node: StringNode(versionData.collections, { validator: 'resource', params: { pool: '$worldgen/configured_feature' } }),
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
node: Reference(versionData.schemas, 'configured_feature'),
|
||||
},
|
||||
], { choiceContext: 'feature' })
|
||||
|
||||
const schemaName = config.generators.find(g => g.id === id)?.schema
|
||||
if (!schemaName) {
|
||||
throw new Error(`Cannot find model ${id}`)
|
||||
}
|
||||
try {
|
||||
const schema = versionData.schemas.get(schemaName)
|
||||
const model = new DataModel(schema, { wrapLists: true })
|
||||
if (Models[id]) {
|
||||
model.reset(Models[id].model.data, false)
|
||||
} else {
|
||||
model.validate(true)
|
||||
model.history = [JSON.stringify(model.data)]
|
||||
}
|
||||
Models[id] = { model, version }
|
||||
} catch (e) {
|
||||
const err = new Error(`Cannot get generator "${id}" for version "${version}": ${message(e)}`)
|
||||
if (e instanceof Error) err.stack = e.stack
|
||||
throw err
|
||||
}
|
||||
}
|
||||
return Models[id].model
|
||||
}
|
||||
|
||||
export async function getCollections(version: VersionId): Promise<CollectionRegistry> {
|
||||
const versionData = await getVersion(version)
|
||||
return versionData.collections
|
||||
}
|
||||
|
||||
export async function getBlockStates(version: VersionId): Promise<BlockStateRegistry> {
|
||||
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
|
||||
}
|
||||
4
src/app/services/index.ts
Normal file
4
src/app/services/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './Changelogs'
|
||||
export * from './DataFetcher'
|
||||
export * from './Manifest'
|
||||
export * from './Schemas'
|
||||
Reference in New Issue
Block a user