Skip to content
Open
16 changes: 14 additions & 2 deletions packages/java-edition/src/dependency/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,26 @@ export interface VersionInfo {

export namespace PackMcmeta {
export function readPackFormat(data: any): number {
const supported = data?.pack?.supported_formats
const pack = data?.pack
if (!pack) {
throw new Error('“pack” is not set')
}

const max_format = pack.max_format
if (typeof max_format === 'number') {
return max_format
}
if (Array.isArray(max_format) && max_format.length > 0 && typeof max_format[0] === 'number') {
return pack.max_format[0] // TODO: Do we need the minor version as well?
}
const supported = pack.supported_formats
if (Array.isArray(supported) && supported.length === 2 && typeof supported[1] === 'number') {
return supported[1]
}
if (typeof supported === 'object' && typeof supported?.max_inclusive === 'number') {
return supported.max_inclusive
}
const format = data?.pack?.pack_format
const format = pack.pack_format
if (typeof format === 'number') {
return format
}
Expand Down
2 changes: 1 addition & 1 deletion packages/java-edition/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export const initialize: core.ProjectInitializer = async (ctx) => {
meta.registerLanguage('vsh', { extensions: ['.vsh'] })

json.getInitializer(jsonUriPredicate)(ctx)
jeJson.initialize(ctx)
jeJson.initialize(ctx, packs)
jeMcf.initialize(ctx, summary.commands, release)
nbt.initialize(ctx)

Expand Down
164 changes: 144 additions & 20 deletions packages/java-edition/src/json/checker/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type * as core from '@spyglassmc/core'
import * as core from '@spyglassmc/core'
import * as json from '@spyglassmc/json'
import type * as mcdoc from '@spyglassmc/mcdoc'
import { dissectUri, reportDissectError } from '../../binder/index.js'
import type { PackInfo } from '../../dependency/common.js'

const NEW_RESOURCEPACK_PACK_FORMAT = 65
const NEW_DATAPACK_PACK_FORMAT = 82

function createTagDefinition(registry: string): mcdoc.McdocType {
const id: mcdoc.AttributeValue = {
Expand All @@ -18,29 +22,149 @@ function createTagDefinition(registry: string): mcdoc.McdocType {
}
}

export const file: core.Checker<json.JsonFileNode> = (node, ctx) => {
const child = node.children[0]
if (ctx.doc.uri.endsWith('/pack.mcmeta')) {
const type: mcdoc.McdocType = { kind: 'reference', path: '::java::pack::Pack' }
return json.checker.index(type)(child, ctx)
}
const parts = dissectUri(ctx.doc.uri, ctx)
if (parts?.ok) {
if (parts.category.startsWith('tag/')) {
const type = createTagDefinition(parts.category.slice(4))
export function file(packs: PackInfo[]): core.Checker<json.JsonFileNode> {
return (node, ctx) => {
const child = node.children[0]
if (ctx.doc.uri.endsWith('/pack.mcmeta')) {
const thisPack = packs.find(p => core.fileUtil.isSubUriOf(ctx.doc.uri, p.packRoot))

const newPackFormat = thisPack?.type === 'assets'
? NEW_RESOURCEPACK_PACK_FORMAT
: NEW_DATAPACK_PACK_FORMAT

const { min_format, max_format } = getPackFormatRangeFromPackMcMeta(child, newPackFormat)
let type: mcdoc.McdocType

const oldRange = { kind: 0b01, max: newPackFormat } satisfies mcdoc.NumericRange
const newRange = { kind: 0, min: newPackFormat } satisfies mcdoc.NumericRange

// This is not done in mcdoc to give better error messages by essentially muting all
// irrelevant fields. Maybe there is a way mcdoc runtime could be improved in the future
// to show better error messages for complex types.
if (min_format && max_format) {
if (max_format < newPackFormat) {
type = createConcreteType('OldPack', oldRange)
} else if (min_format < newPackFormat) {
type = createConcreteType('IntermediatePack', oldRange, newRange)
} else {
type = createConcreteType('NewPack', oldRange, newRange)
}
} else {
type = createConcreteType('Pack', oldRange, newRange)
}

return json.checker.index(type)(child, ctx)
}
const type: mcdoc.McdocType = {
kind: 'dispatcher',
registry: 'minecraft:resource',
parallelIndices: [{ kind: 'static', value: parts.category }],
const parts = dissectUri(ctx.doc.uri, ctx)
if (parts?.ok) {
if (parts.category.startsWith('tag/')) {
const type = createTagDefinition(parts.category.slice(4))
return json.checker.index(type)(child, ctx)
}
const type: mcdoc.McdocType = {
kind: 'dispatcher',
registry: 'minecraft:resource',
parallelIndices: [{ kind: 'static', value: parts.category }],
}
return json.checker.index(type, { discardDuplicateKeyErrors: true })(child, ctx)
} else if (parts?.ok === false) {
reportDissectError(parts.path, parts.expected, ctx)
}
}
}

export function register(meta: core.MetaRegistry, packs: PackInfo[]) {
meta.registerChecker<json.JsonFileNode>('json:file', file(packs))
}

function getPackFormatRangeFromPackMcMeta(packFormat: json.JsonNode, newPackFormat: number) {
const pack = getJsonField(packFormat, 'pack')

let min_format: number | bigint | undefined = undefined
let max_format: number | bigint | undefined = undefined

if (pack) {
max_format = getMinorVersionSupportedFormat(pack, 'max_format')
min_format = getMinorVersionSupportedFormat(pack, 'min_format')

const supported_formats = getJsonField(pack, 'supported_formats')
if (supported_formats) {
if (json.JsonArrayNode.is(supported_formats)) {
const min_supported = supported_formats.children.length >= 1
? supported_formats.children[0].value
: undefined
const max_supported = supported_formats.children.length === 2
? supported_formats.children[1].value
: undefined

if (!min_format && min_supported && json.JsonNumberNode.is(min_supported)) {
min_format = min_supported.value.value
}

if (!max_format && max_supported && json.JsonNumberNode.is(max_supported)) {
max_format = max_supported.value.value
}
} else if (json.JsonObjectNode.is(supported_formats)) {
min_format ??= getJsonNumber(supported_formats, 'min_inclusive')
max_format ??= getJsonNumber(supported_formats, 'max_inclusive')
}
}

const pack_format = getJsonNumber(pack, 'pack_format')
min_format ??= pack_format
max_format ??= pack_format

min_format ??= max_format
max_format ??= (!min_format || min_format > newPackFormat) ? min_format : newPackFormat
}

return { min_format, max_format }

function getJsonField(jsonNode: json.JsonNode | undefined, key: string) {
return (jsonNode?.children?.find((c): c is json.JsonPairNode =>
json.JsonPairNode.is(c)
&& c.key?.value === key
) as json.JsonPairNode | undefined)
?.value
}

function getJsonNumber(jsonNode: json.JsonNode | undefined, key: string) {
const field = getJsonField(jsonNode, key)
if (field !== undefined && json.JsonNumberNode.is(field)) {
return field.value.value
}
return undefined
}
function getMinorVersionSupportedFormat(jsonNode: json.JsonNode | undefined, key: string) {
const field = getJsonField(jsonNode, key)
if (field !== undefined) {
if (json.JsonNumberNode.is(field)) {
return field.value.value
} else if (json.JsonArrayNode.is(field) && field.children.length >= 1) {
const value = field.children[0].value
if (value && json.JsonNumberNode.is(value)) {
return value.value.value
}
}
}
return json.checker.index(type, { discardDuplicateKeyErrors: true })(child, ctx)
} else if (parts?.ok === false) {
reportDissectError(parts.path, parts.expected, ctx)
return undefined
}
}

export function register(meta: core.MetaRegistry) {
meta.registerChecker<json.JsonFileNode>('json:file', file)
function createConcreteType(
structName: string,
firstRange: mcdoc.NumericRange,
secondRange?: mcdoc.NumericRange,
) {
const type = {
kind: 'concrete',
child: { kind: 'reference', path: `::java::pack::${structName}` },
typeArgs: [{ kind: 'int', valueRange: firstRange }],
} satisfies mcdoc.ConcreteType

if (secondRange) {
type.typeArgs.push({ kind: 'int', valueRange: secondRange })
}

return type
}
5 changes: 3 additions & 2 deletions packages/java-edition/src/json/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
/* istanbul ignore file */

import type * as core from '@spyglassmc/core'
import type { PackInfo } from '../dependency/common.js'
import * as binder from './binder/index.js'
import * as checker from './checker/index.js'
import * as completer from './completer/index.js'
import { registerMcdocAttributes } from './mcdocAttributes.js'

export const initialize = (ctx: core.ProjectInitializerContext) => {
export const initialize = (ctx: core.ProjectInitializerContext, packs: PackInfo[]) => {
registerMcdocAttributes(ctx.meta)

binder.register(ctx.meta)
checker.register(ctx.meta)
checker.register(ctx.meta, packs)
completer.register(ctx.meta)
}