diff --git a/src/commands/implementations/entity/execute.ts b/src/commands/implementations/entity/execute.ts index 2fcc8bf..ed36780 100644 --- a/src/commands/implementations/entity/execute.ts +++ b/src/commands/implementations/entity/execute.ts @@ -97,7 +97,7 @@ export class ExecuteCommandNode extends ContainerCommandNode { } // Create a new MCFunctionNode with the body of the ExecuteNode. - const mcFunction = new MCFunctionClass(this.sandstoneCore, `${currentMCFunction.resource.path.slice(1).join('/')}/${this.callbackName}`, { + const mcFunction = new MCFunctionClass(this.sandstoneCore, `${currentMCFunction.resource.path.slice(2).join('/')}/${this.callbackName}`, { addToSandstoneCore: false, creator: 'sandstone', onConflict: 'rename', @@ -401,7 +401,7 @@ export class ExecuteCommand extends ExecuteCommandPart { positioned(pos?: Coordinates) { if (pos) { - return this.nestedExecute(['as', coordinatesParser(pos)]) + return this.nestedExecute(['positioned', coordinatesParser(pos)]) } return this.subCommand([['as']], ExecutePositionedAsCommand, false) } @@ -420,7 +420,7 @@ export class ExecuteCommand extends ExecuteCommandPart { rotated(rotation?: Rotation) { if (rotation) { - return this.nestedExecute(['as', rotationParser(rotation)]) + return this.nestedExecute(['rotated', rotationParser(rotation)]) } return this.subCommand([['as']], ExecuteRotatedAsCommand, false) } diff --git a/src/core/resources/mcfunction.ts b/src/core/resources/mcfunction.ts index b819fc4..774f3be 100644 --- a/src/core/resources/mcfunction.ts +++ b/src/core/resources/mcfunction.ts @@ -115,7 +115,7 @@ export class MCFunctionNode extends ContainerNode implements ResourceNode { return this.contextStack.pop() } - getValue = () => this.body.map((node) => node.getValue()).join('\n') + getValue = () => this.body.filter((node) => node.getValue() !== null).map((node) => node.getValue()).join('\n') } export type MCFunctionClassArguments = ({ @@ -185,8 +185,6 @@ export class _RawMCFunctionClass extends CallableResourceClass { protected lazy: boolean - protected addToSandstoneCore: boolean - constructor(core: SandstoneCore, name: string, args: MCFunctionClassArguments) { super(core, { packType: core.pack.dataPack(), extension: 'mcfunction' }, MCFunctionNode, core.pack.resourceToPath(name, ['functions']), { ...args, @@ -305,6 +303,29 @@ export class _RawMCFunctionClass extends CallableResourceClass { } }, true) } + + splice(start: number, removeItems: number | 'auto', ...contents: _RawMCFunctionClass[] | [() => void]) { + const fake = new MCFunctionClass(this.core, 'fake', { + addToSandstoneCore: false, + creator: 'sandstone', + onConflict: 'ignore', + }) + + const fullBody: Node[] = [] + + if (contents[0] instanceof _RawMCFunctionClass) { + for (const mcfunction of contents as _RawMCFunctionClass[]) { + fullBody.push(...mcfunction.node.body) + } + } else { + this.core.enterMCFunction(fake) + this.core.insideContext(fake.node, contents[0], false) + this.core.exitMCFunction() + fullBody.push(...fake.node.body) + } + + this.node.body.splice(start, removeItems === 'auto' ? fullBody.length : removeItems, ...fullBody) + } } export const MCFunctionClass = makeClassCallable(_RawMCFunctionClass) diff --git a/src/core/resources/predicate.ts b/src/core/resources/predicate.ts index 5e3a346..48a45fc 100644 --- a/src/core/resources/predicate.ts +++ b/src/core/resources/predicate.ts @@ -49,7 +49,6 @@ export class PredicateClass extends ResourceClass implements List } else { predicateJSON = predicate } - console.log(predicateJSON) if (Array.isArray(predicateJSON)) { this.predicateJSON.push(...predicateJSON) } else { diff --git a/src/core/resources/resource.ts b/src/core/resources/resource.ts index c909d7c..e4c0419 100644 --- a/src/core/resources/resource.ts +++ b/src/core/resources/resource.ts @@ -54,6 +54,8 @@ export abstract class ResourceClass> path + addToSandstoneCore: boolean + onConflict: LiteralUnion renameIndex = 2 @@ -72,62 +74,66 @@ export abstract class ResourceClass> this.path = path + this.addToSandstoneCore = args.addToSandstoneCore + this.creator = args.creator ?? 'sandstone' this.onConflict = args.onConflict || process.env[`${this.node.resource.path[1].toUpperCase()}_CONFLICT_STRATEGY`] || process.env.DEFAULT_CONFLICT_STRATEGY || 'throw' } protected handleConflicts() { - const resourceType = this.node.resource.path[1] - - const conflict = [...this.core.resourceNodes].find((node) => node.resource.path.join('') === this.node.resource.path.join('')) - - if (conflict) { - const oldResource = conflict.resource - const newResource = this.node.resource - - switch (this.onConflict) { - case 'throw': { - // eslint-disable-next-line max-len - throw new Error(`Created a ${resourceType.substring(0, resourceType.length - 1)} with the duplicate name ${newResource.name}, and onConflict was set to "throw".`) + if (this.addToSandstoneCore) { + const resourceType = this.node.resource.path[1] + + const conflict = [...this.core.resourceNodes].find((node) => node.resource.path.join('') === this.node.resource.path.join('')) + + if (conflict) { + const oldResource = conflict.resource + const newResource = this.node.resource + + switch (this.onConflict) { + case 'throw': { + // eslint-disable-next-line max-len + throw new Error(`Created a ${resourceType.substring(0, resourceType.length - 1)} with the duplicate name ${newResource.name}, and onConflict was set to "throw".`) + } + case 'replace': { + this.core.resourceNodes.forEach((node) => { + if (node.resource.path.join('') === oldResource.path.join('')) { + this.core.resourceNodes.delete(node) + } + }) + this.core.resourceNodes.add(this.node) + } break + case 'warn': { + console.warn([ + 'Warning:', + `Tried to create a ${resourceType.substring(0, resourceType.length - 1)} named "${newResource.name}", but found an already existing one.`, + "The new one has replaced the old one. To remove this warning, please change the options of the resource to { onConflict: '/* other option */' }.", + ].join('\n')) + this.core.resourceNodes.forEach((node) => { + if (node.resource.path.join('') === oldResource.path.join('')) { + this.core.resourceNodes.delete(node) + } + }) + this.core.resourceNodes.add(this.node) + } break + case 'rename': { + // eslint-disable-next-line no-plusplus + this.path[this.path.length - 1] += `${oldResource.renameIndex++}` + + this.core.resourceNodes.add(this.node) + } break + case 'prepend': { + (oldResource as unknown as ListResource).unshift(newResource) + } break + case 'append': { + (oldResource as unknown as ListResource).push(newResource) + } break + default: break } - case 'replace': { - this.core.resourceNodes.forEach((node) => { - if (node.resource.path.join('') === oldResource.path.join('')) { - this.core.resourceNodes.delete(node) - } - }) - this.core.resourceNodes.add(this.node) - } break - case 'warn': { - console.warn([ - 'Warning:', - `Tried to create a ${resourceType.substring(0, resourceType.length - 1)} named "${newResource.name}", but found an already existing one.`, - "The new one has replaced the old one. To remove this warning, please change the options of the resource to { onConflict: '/* other option */' }.", - ].join('\n')) - this.core.resourceNodes.forEach((node) => { - if (node.resource.path.join('') === oldResource.path.join('')) { - this.core.resourceNodes.delete(node) - } - }) - this.core.resourceNodes.add(this.node) - } break - case 'rename': { - // eslint-disable-next-line no-plusplus - this.path[this.path.length - 1] += `${oldResource.renameIndex++}` - - this.core.resourceNodes.add(this.node) - } break - case 'prepend': { - (oldResource as unknown as ListResource).unshift(newResource) - } break - case 'append': { - (oldResource as unknown as ListResource).push(newResource) - } break - default: break + } else { + this.core.resourceNodes.add(this.node) } - } else { - this.core.resourceNodes.add(this.node) } } diff --git a/src/core/sandstoneCore.ts b/src/core/sandstoneCore.ts index 50494f1..29ea682 100644 --- a/src/core/sandstoneCore.ts +++ b/src/core/sandstoneCore.ts @@ -1,3 +1,4 @@ +/* eslint-disable operator-linebreak */ import fs from 'fs-extra' import path from 'path' @@ -123,17 +124,31 @@ export class SandstoneCore { return finalResources } - // TODO: Support dry & verbose runs - save = async (cliOptions: { fileHandler: (relativePath: string, content: any) => Promise }, opts: { visitors: GenericCoreVisitor[] }) => { + save = async (cliOptions: { fileHandler: (relativePath: string, content: any) => Promise, dry: boolean, verbose: boolean }, opts: { visitors: GenericCoreVisitor[] }) => { const resources = this.generateResources(opts) for await (const node of resources) { - const resourcePath = path.join(node.resource.packType.type, ...node.resource.path) + const { packType, fileExtension } = node.resource + const _path = [packType.type, ...node.resource.path] + + if (packType.resourceSubFolder) { + _path.splice(1, 0, packType.resourceSubFolder) + } + const resourcePath = path.join(..._path) const value = node.getValue() + if (cliOptions.verbose) { + console.log( + `Path: ${resourcePath}.${fileExtension}\n\n` + + `${typeof value === 'string' ? value : ''}`, + ) + } + /* @ts-ignore */ - await cliOptions.fileHandler(`${resourcePath}.${node.resource.fileExtension}`, value) + if (!cliOptions.dry) { + await cliOptions.fileHandler(`${resourcePath}.${fileExtension}`, value) + } } } } diff --git a/src/flow/Flow.ts b/src/flow/Flow.ts index 613cacc..71eb508 100644 --- a/src/flow/Flow.ts +++ b/src/flow/Flow.ts @@ -1,16 +1,17 @@ -import { ConditionClass } from '../variables/index' -import { AndNode, NotNode, OrNode } from './conditions' +import { + AndNode, ConditionNode, NotNode, OrNode, +} from './conditions' import { IfStatement } from './if_else' import type { SandstoneCore } from '../core' -import type { ConditionNode } from './conditions' +import type { ConditionClass } from '../variables/index' type Condition = ConditionNode | ConditionClass export class Flow { constructor(public sandstoneCore: SandstoneCore) { } conditionToNode(condition: Condition) { - if (condition instanceof ConditionClass) { + if (!(condition instanceof ConditionNode)) { return condition._toMinecraftCondition() } return condition diff --git a/src/flow/conditions/and.ts b/src/flow/conditions/and.ts index c5e7c63..685a43f 100644 --- a/src/flow/conditions/and.ts +++ b/src/flow/conditions/and.ts @@ -7,5 +7,5 @@ export class AndNode extends ConditionNode { super(sandstoneCore) } - getValue = (negated = false) => this.conditions.join(' ') + getValue = (negated = false) => this.conditions.map((condition) => condition.getValue(negated)).join(' ') } diff --git a/src/flow/conditions/condition.ts b/src/flow/conditions/condition.ts index 0c36fa4..9445e1b 100644 --- a/src/flow/conditions/condition.ts +++ b/src/flow/conditions/condition.ts @@ -28,3 +28,9 @@ export abstract class SingleConditionNode extends ConditionNode { return [keyword, ...this.getCondition()].join(' ') } } + +export abstract class SingleExecuteNode extends ConditionNode { + abstract getCondition(): unknown[] + + getValue = (negated = false) => this.getCondition().join(' ') +} diff --git a/src/flow/conditions/or.ts b/src/flow/conditions/or.ts index 0342cf1..e22a65d 100644 --- a/src/flow/conditions/or.ts +++ b/src/flow/conditions/or.ts @@ -1,13 +1,29 @@ +import { IfNode } from '../if_else' import { ConditionNode } from './condition' import type { SandstoneCore } from '#core' export class OrNode extends ConditionNode { + variable + constructor(sandstoneCore: SandstoneCore, public conditions: ConditionNode[]) { super(sandstoneCore) - } - getValue = (negated?: boolean | undefined) => { - throw new Error('OR conditions must be postprocessed.') + const { Variable, _ } = sandstoneCore.pack + + this.variable = Variable(undefined, 'condition') + + const currentNode = this.sandstoneCore.getCurrentMCFunctionOrThrow() + + currentNode.resource.push(() => { + this.variable.reset() + + for (const condition of conditions) { + // eslint-disable-next-line no-new + new IfNode(sandstoneCore, condition, () => this.variable.add(1), false) + } + }) } + + getValue = (negated?: boolean | undefined) => this.variable.matches('1..')._toMinecraftCondition().getValue(negated) } diff --git a/src/flow/conditions/success.ts b/src/flow/conditions/success.ts new file mode 100644 index 0000000..b4de04e --- /dev/null +++ b/src/flow/conditions/success.ts @@ -0,0 +1,14 @@ +import { SingleExecuteNode } from './condition' + +import type { Score } from 'sandstone/variables/Score' +import type { SandstoneCore } from '#core' + +export class SuccessConditionNode extends SingleExecuteNode { + constructor(sandstoneCore: SandstoneCore, readonly score: Score) { + super(sandstoneCore) + } + + getCondition(): unknown[] { + return ['store', 'success', 'score', this.score] + } +} diff --git a/src/flow/if_else.ts b/src/flow/if_else.ts index 218f57f..91154bc 100644 --- a/src/flow/if_else.ts +++ b/src/flow/if_else.ts @@ -6,13 +6,21 @@ import type { ConditionNode } from './conditions' export class IfNode extends ContainerNode { nextFlowNode?: IfNode | ElseNode - constructor(sandstoneCore: SandstoneCore, public condition: ConditionNode, public callback: () => void) { + constructor(sandstoneCore: SandstoneCore, public condition: ConditionNode, public callback: () => void, reset = true) { super(sandstoneCore) - // Generate the body of the If node. - this.sandstoneCore.getCurrentMCFunctionOrThrow().enterContext(this) - this.callback() - this.sandstoneCore.currentMCFunction?.exitContext() + const currentNode = this.sandstoneCore.getCurrentMCFunctionOrThrow() + + if (reset) { + currentNode.resource.push(() => sandstoneCore.pack.flowVariable.reset()) + } + + if (callback.toString() !== '() => {}') { + // Generate the body of the If node. + currentNode.enterContext(this) + this.callback() + currentNode.exitContext() + } } getValue = () => { @@ -53,9 +61,7 @@ export class ElseNode extends ContainerNode { this.sandstoneCore.currentMCFunction?.exitContext() } - getValue = () => { - throw new Error('Minecraft does not support else statements. This must be postprocessed.') - } + getValue = () => null } export class ElseStatement { diff --git a/src/index.ts b/src/index.ts index b6ab183..b8c72c0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -110,15 +110,15 @@ export type DatapackConfig = { * * @see [https://minecraft.gamepedia.com/Data_Pack#pack.mcmeta](https://minecraft.gamepedia.com/Data_Pack#pack.mcmeta) */ - formatVersion: number + packFormat: number /** List of experimental game features to enable. */ - features: string[], + features?: string[], /** * Section for filtering out files from data packs applied below this one. Any file that matches one of the patterns inside `block` will be treated as if it was not present in the pack at all. */ - filter: { + filter?: { /** List of patterns */ block: { /** A regular expression for the namespace of files to be filtered out. If unspecified, it applies to every namespace. */ @@ -202,6 +202,24 @@ export interface SandstoneConfig { afterAll?: () => (void | Promise) } + resources?: { + /** Path regex of files to exclude from the output. */ + exclude?: { + /** From `SandstonePack` (your code in `./src`). */ + generated?: RegExp[] + + /** From `./resources`. */ + existing?: RegExp[] + } | RegExp[] + + /** Handle files before they are written to the output. */ + handle?: { + path: RegExp + + callback: (contents: string | Buffer | Promise) => Promise + }[] + } + /** * The strategy to use when 2 resources of the same type (Advancement, MCFunctions...) have the same name. */ diff --git a/src/pack/pack.ts b/src/pack/pack.ts index c646340..9237cf3 100644 --- a/src/pack/pack.ts +++ b/src/pack/pack.ts @@ -105,13 +105,13 @@ class DataPack extends PackType { // TODO: typing. low priority readonly packMcmeta: any - constructor(archiveOutput: boolean, options: { packFormat: number, packDescription: JSONTextComponent, features?: string[], filter?: { namespace?: string, path?: string }[] }) { + constructor(archiveOutput: boolean, options: { packFormat: number, description: JSONTextComponent, features?: string[], filter?: { namespace?: string, path?: string }[] }) { super('datapack', 'saves/$worldName$/datapacks/$packName$', 'world/datapacks/$packName$', 'datapacks/$packName$', 'server', archiveOutput, 'data', true) this.packMcmeta = { pack: { pack_format: options.packFormat, - description: options.packDescription, + description: options.description, }, } @@ -143,6 +143,8 @@ export class SandstonePack { packTypes: Map + packOptions = JSON.parse(process.env.PACK_OPTIONS as string) + dataPack = () => this.packTypes.get('datapack') as DataPack // Smithed Pack IDs @@ -172,7 +174,7 @@ export class SandstonePack { this.core = new SandstoneCore(this) this.packTypes = new Map() - this.packTypes.set('datapack', new DataPack(false, JSON.parse(process.env.PACK_OPTIONS as string))) + this.packTypes.set('datapack', new DataPack(false, this.packOptions.datapack)) this.commands = new SandstoneCommands(this) @@ -253,7 +255,7 @@ export class SandstonePack { create: (name: string, criteria: LiteralUnion = 'dummy', display?: JSONTextComponent, alreadyExists?: true): ObjectiveClass => { let namespace: boolean = false - if (name.includes('.')) { + if (name.includes('.') || name.includes('__')) { namespace = true } @@ -269,8 +271,14 @@ export class SandstonePack { get: (name: string): ObjectiveClass => new ObjectiveClass(this, name, undefined, undefined, { creator: 'user' }), } + __rootObjective?: ObjectiveClass + get rootObjective() { - return this.Objective.create('sandstone', 'dummy', [{ text: 'Sandstone', color: 'gold' }, ' internals']) + if (this.__rootObjective) { + return this.__rootObjective + } + this.__rootObjective = this.Objective.create('__sandstone', 'dummy', [{ text: 'Sandstone', color: 'gold' }, ' internals']) + return this.__rootObjective } Variable: ( @@ -327,6 +335,10 @@ export class SandstonePack { return anonymousScore } + get flowVariable() { + return this.rootObjective('if_result') + } + /** * Creates a new label * @param label Label/tag name @@ -694,7 +706,7 @@ export class SandstonePack { ...options, }) - save = async (cliOptions: { fileHandler: (relativePath: string, content: any) => Promise }) => { + save = async (cliOptions: { fileHandler: (relativePath: string, content: any) => Promise, dry: boolean, verbose: boolean }) => { await this.core.save(cliOptions, { visitors: [ // Initialization visitors diff --git a/src/pack/visitors/ifElseTransformationVisitor.ts b/src/pack/visitors/ifElseTransformationVisitor.ts index c47ff2d..0d75a72 100644 --- a/src/pack/visitors/ifElseTransformationVisitor.ts +++ b/src/pack/visitors/ifElseTransformationVisitor.ts @@ -1,6 +1,8 @@ +/* eslint-disable no-spaced-func */ +/* eslint-disable func-call-spacing */ +import { SuccessConditionNode } from 'sandstone/flow/conditions/success' import { ExecuteCommandNode } from '#commands' import { IfNode, NotNode, ScoreConditionNode } from '#flow' -import { ObjectiveClass } from '#variables' import { GenericSandstoneVisitor } from './visitor' @@ -18,17 +20,12 @@ function* flattenIfNode(node: IfNode): IterableIterator { } } -const IF_OBJECTIVE = { - objective: 'sandstone', - name: 'if_result', -} - export class IfElseTransformationVisitor extends GenericSandstoneVisitor { visitIfNode = (node_: IfNode) => { // Start by flattening all nodes const nodes = Array.from(flattenIfNode(node_)) - const ifScore = new ObjectiveClass(this.pack, IF_OBJECTIVE.objective, undefined, undefined, { creator: 'sandstone' }).ScoreHolder(IF_OBJECTIVE.name) + const ifScore = this.core.pack.flowVariable // 1. If we have a single if node. No need to store its result then. if (nodes.length === 1) { @@ -45,11 +42,7 @@ export class IfElseTransformationVisitor extends GenericSandstoneVisitor { const conditions: ConditionNode[] = [] if (i > 0) { - conditions.push(new NotNode(this.core, new ScoreConditionNode(this.core, [ifScore.target.toString(), ifScore.objective.name, 'matches', '0..']))) - /* - * TODO: replace with a real, existing objective - * conditions.push(flow.not(createCondition(['unless', 'score', IF_OBJECTIVE.name, IF_OBJECTIVE.objective, 'matches', '0..']))) - */ + conditions.push(new NotNode(this.core, new ScoreConditionNode(this.core, [`${ifScore}`, 'matches', '0..']))) } let callbackName: string @@ -58,17 +51,18 @@ export class IfElseTransformationVisitor extends GenericSandstoneVisitor { // If we have a "If" node, add the condition if (node instanceof IfNode) { conditions.push(node.condition) + conditions.push(new SuccessConditionNode(this.core, ifScore)) callbackName = i === 0 ? 'if' : 'elseif' } else { callbackName = 'else' } - return new ExecuteCommandNode(this.pack, [], { + return new ExecuteCommandNode(this.pack, [[flow.and(...conditions).getValue()]], { isSingleExecute: false, givenCallbackName: callbackName, body, }) }) - return node_ + return transformedNodes } } diff --git a/src/pack/visitors/initConstantsVisitor.ts b/src/pack/visitors/initConstantsVisitor.ts index 237c865..4e5f7ce 100644 --- a/src/pack/visitors/initConstantsVisitor.ts +++ b/src/pack/visitors/initConstantsVisitor.ts @@ -6,7 +6,7 @@ import { GenericSandstoneVisitor } from './visitor' export class InitConstantsVisitor extends GenericSandstoneVisitor { onStart = () => { const { pack } = this - const { commands, core } = pack + const { scoreboard } = pack.commands // Remove duplicates let constants = [...pack.constants.values()] @@ -15,7 +15,7 @@ export class InitConstantsVisitor extends GenericSandstoneVisitor { if (constants.length !== 0) { pack.initMCFunction.unshift(() => { for (const constant of constants) { - commands.scoreboard.players.set(constant, pack.rootObjective, constant) + scoreboard.players.set(constant, pack.rootObjective, constant) } }) } diff --git a/src/utils.ts b/src/utils.ts index 003f7ba..590f4f4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -160,7 +160,7 @@ export type PartialFunction< export function toMinecraftResourceName(path: readonly string[], typeNested: number = 1): string { const [namespace, ...folders] = path - folders.splice(1, typeNested) + folders.splice(0, typeNested) return `${namespace}:${folders.join('/')}` } diff --git a/src/variables/Score.ts b/src/variables/Score.ts index ee7e493..3e4c461 100644 --- a/src/variables/Score.ts +++ b/src/variables/Score.ts @@ -555,6 +555,6 @@ export class Score extends ComponentClass implements ConditionClass { * @param range The range to compare the current score against. */ matches = (range: Range) => ({ - _toMinecraftCondition: () => new this.sandstonePack.conditions.Score(this.sandstonePack.core, ['if', 'score', `${this.target}`, `${this.objective}`, 'matches', rangeParser(range)]), + _toMinecraftCondition: () => new this.sandstonePack.conditions.Score(this.sandstonePack.core, [`${this.target}`, `${this.objective}`, 'matches', rangeParser(range)]), }) }