From b263d888e61a26196de59f317828f2a40592ca64 Mon Sep 17 00:00:00 2001 From: Mike Ammerlaan Date: Fri, 15 Dec 2023 08:52:17 -0800 Subject: [PATCH] Manifest validation fixes Also, use a specific name for browser storage. --- app/src/app/Project.ts | 10 +- .../info/AddOnItemRequirementsGenerator.ts | 59 +++++++- app/src/info/AddOnRequirementsGenerator.ts | 139 ++++++++++++++++++ app/src/info/StrictPlatformInfoGenerator.ts | 4 +- app/src/local/IAuthenticationToken.ts | 9 ++ app/src/minecraft/BehaviorManifestJson.ts | 24 +++ app/src/minecraft/IAddonManifest.ts | 24 +++ app/src/minecraft/ResourceManifestJson.ts | 20 ++- app/src/storage/BrowserStorage.ts | 12 ++ app/src/storage/FileSystemFile.ts | 12 -- app/src/storage/FileSystemFolder.ts | 2 +- 11 files changed, 294 insertions(+), 21 deletions(-) diff --git a/app/src/app/Project.ts b/app/src/app/Project.ts index c6f981ad..5b764ea7 100644 --- a/app/src/app/Project.ts +++ b/app/src/app/Project.ts @@ -1603,7 +1603,7 @@ export default class Project { if ( folderContext === FolderContext.unknown && - folder.files["manifest.json"] && + (folder.files["manifest.json"] || folder.files["pack_manifest.json"]) && !folder.files["level.dat"] && !folder.files["levelname.txt"] ) { @@ -1667,7 +1667,7 @@ export default class Project { const baseName = StorageUtilities.getBaseFromName(candidateFile.name); const folderPath = StorageUtilities.canonicalizePath(StorageUtilities.getPath(projectPath)); - if (canonFileName === "manifest.json") { + if (canonFileName === "manifest.json" || canonFileName === "pack_manifest.json") { if (folderContext === FolderContext.resourcePack) { this.#defaultResourcePackFolder = folder; this.#resourcePacksContainer = parentFolder; @@ -2467,7 +2467,11 @@ export default class Project { public async processProjectFolder(folder: IFolder) { await folder.load(false); - const manifest = folder.files["manifest.json"]; + let manifest = folder.files["manifest.json"]; + + if (manifest === undefined) { + manifest = folder.files["pack_manifest.json"]; + } if (!this.#projectFolder) { throw new Error("Unexpectedly could not find a project folder."); diff --git a/app/src/info/AddOnItemRequirementsGenerator.ts b/app/src/info/AddOnItemRequirementsGenerator.ts index 9ce01f00..d8fb3084 100644 --- a/app/src/info/AddOnItemRequirementsGenerator.ts +++ b/app/src/info/AddOnItemRequirementsGenerator.ts @@ -11,6 +11,8 @@ import BehaviorAnimation from "../minecraft/BehaviorAnimation"; import ResourceAnimationController from "../minecraft/ResourceAnimationController"; import ResourceAnimation from "../minecraft/ResourceAnimation"; import ResourceRenderController from "../minecraft/ResourceRenderController"; +import ResourceManifestJson from "../minecraft/ResourceManifestJson"; +import BehaviorManifestJson from "../minecraft/BehaviorManifestJson"; export default class AddOnItemRequirementsGenerator implements IProjectInfoItemGenerator { id = "ADDONIREQ"; @@ -27,7 +29,62 @@ export default class AddOnItemRequirementsGenerator implements IProjectInfoItemG async generate(projectItem: ProjectItem): Promise { const items: ProjectInfoItem[] = []; - if (projectItem.itemType === ProjectItemType.animationControllerBehaviorJson) { + if (projectItem.itemType === ProjectItemType.resourcePackManifestJson) { + await projectItem.ensureFileStorage(); + + if (projectItem.file) { + const rpManifest = await ResourceManifestJson.ensureOnFile(projectItem.file); + + if (rpManifest) { + await rpManifest.load(); + + if (!rpManifest.packScope) { + items.push( + new ProjectInfoItem( + InfoItemType.testCompleteFail, + this.id, + 140, + `Resource pack manifest does not specify that header/pack_scope should be 'world'`, + projectItem + ) + ); + } + if (!rpManifest.productType) { + items.push( + new ProjectInfoItem( + InfoItemType.testCompleteFail, + this.id, + 141, + `Resource pack manifest does not specify a metadata/product_type that should be 'addon'`, + projectItem + ) + ); + } + } + } + } else if (projectItem.itemType === ProjectItemType.behaviorPackManifestJson) { + await projectItem.ensureFileStorage(); + + if (projectItem.file) { + const bpManifest = await BehaviorManifestJson.ensureOnFile(projectItem.file); + + if (bpManifest) { + await bpManifest.load(); + + if (!bpManifest.productType) { + items.push( + new ProjectInfoItem( + InfoItemType.testCompleteFail, + this.id, + 142, + `Behavior pack manifest does not specify a metadata/product_type that should be 'addon'`, + projectItem + ) + ); + } + } + } + } else if (projectItem.itemType === ProjectItemType.animationControllerBehaviorJson) { await projectItem.ensureFileStorage(); if (projectItem.file) { diff --git a/app/src/info/AddOnRequirementsGenerator.ts b/app/src/info/AddOnRequirementsGenerator.ts index e757fa5c..0dc2f817 100644 --- a/app/src/info/AddOnRequirementsGenerator.ts +++ b/app/src/info/AddOnRequirementsGenerator.ts @@ -5,6 +5,11 @@ import { InfoItemType } from "./IInfoItemData"; import IFolder from "../storage/IFolder"; import StorageUtilities from "../storage/StorageUtilities"; import ProjectInfoSet from "./ProjectInfoSet"; +import BehaviorManifestJson from "../minecraft/BehaviorManifestJson"; +import { ProjectItemType } from "../app/IProjectItemData"; +import ProjectItem from "../app/ProjectItem"; +import Utilities from "../core/Utilities"; +import ResourceManifestJson from "../minecraft/ResourceManifestJson"; const UniqueRegEx = new RegExp(/[a-zA-Z0-9]{2,}_[a-zA-Z0-9]{2,}:[\w]+/); @@ -153,6 +158,139 @@ export default class AddOnRequirementsGenerator implements IProjectInfoGenerator async generate(project: Project): Promise { const items: ProjectInfoItem[] = []; + let behaviorPackManifest: undefined | BehaviorManifestJson = undefined; + let behaviorPackItem: undefined | ProjectItem = undefined; + let resourcePackManifest: undefined | ResourceManifestJson = undefined; + let resourcePackItem: undefined | ProjectItem = undefined; + + for (const projectItem of project.items) { + if (projectItem.file) { + if (projectItem.itemType === ProjectItemType.behaviorPackManifestJson) { + if (behaviorPackManifest) { + items.push( + new ProjectInfoItem( + InfoItemType.testCompleteFail, + this.id, + 160, + `Found more than one behavior pack manifest, which is not supported`, + projectItem + ) + ); + } + + behaviorPackManifest = await BehaviorManifestJson.ensureOnFile(projectItem.file); + behaviorPackItem = projectItem; + + await behaviorPackManifest?.load(); + } else if (projectItem.itemType === ProjectItemType.resourcePackManifestJson) { + if (resourcePackManifest) { + items.push( + new ProjectInfoItem( + InfoItemType.testCompleteFail, + this.id, + 161, + `Found more than one resource pack manifest, which is not supported`, + projectItem + ) + ); + } + + resourcePackManifest = await ResourceManifestJson.ensureOnFile(projectItem.file); + resourcePackItem = projectItem; + + await resourcePackManifest?.load(); + } + } + } + + if (!behaviorPackManifest || !behaviorPackManifest.definition) { + items.push( + new ProjectInfoItem( + InfoItemType.testCompleteFail, + this.id, + 163, + `Did not find a valid behavior pack manifest.`, + undefined + ) + ); + } + + if (!resourcePackManifest || !resourcePackManifest.definition) { + items.push( + new ProjectInfoItem( + InfoItemType.testCompleteFail, + this.id, + 164, + `Did not find a valid resource pack manifest.`, + undefined + ) + ); + } + + if ( + behaviorPackManifest && + resourcePackManifest && + behaviorPackManifest.definition && + resourcePackManifest.definition + ) { + if (!behaviorPackManifest.definition.dependencies || behaviorPackManifest.getNonInternalDependencyCount() !== 1) { + items.push( + new ProjectInfoItem( + InfoItemType.testCompleteFail, + this.id, + 165, + `Did not find exactly one dependency on the corresponding resource pack in the behavior pack manifest.`, + behaviorPackItem, + behaviorPackManifest.getNonInternalDependencyCount() + ) + ); + } else if ( + !behaviorPackManifest.definition.dependencies[0].uuid || + !Utilities.uuidEqual( + behaviorPackManifest.definition.dependencies[0].uuid, + resourcePackManifest.definition.header.uuid + ) + ) { + items.push( + new ProjectInfoItem( + InfoItemType.testCompleteFail, + this.id, + 167, + `Behavior pack manifest does not have a proper dependency on the resource pack identifier.`, + behaviorPackItem + ) + ); + } + + if (!resourcePackManifest.definition.dependencies || resourcePackManifest.definition.dependencies.length !== 1) { + items.push( + new ProjectInfoItem( + InfoItemType.testCompleteFail, + this.id, + 168, + `Did not find exactly one dependency on the corresponding behavior pack in the resource pack manifest.`, + resourcePackItem + ) + ); + } else if ( + !resourcePackManifest.definition.dependencies[0].uuid || + !Utilities.uuidEqual( + resourcePackManifest.definition.dependencies[0].uuid, + behaviorPackManifest.definition.header.uuid + ) + ) { + items.push( + new ProjectInfoItem( + InfoItemType.testCompleteFail, + this.id, + 169, + `Resource pack manifest does not have a proper dependency on the behavior pack identifier.`, + behaviorPackItem + ) + ); + } + } + const bpFolder = await project.getDefaultBehaviorPackFolder(); if (bpFolder) { @@ -266,6 +404,7 @@ export default class AddOnRequirementsGenerator implements IProjectInfoGenerator folderNameCanon !== "materials" && folderNameCanon !== "blocks" && folderNameCanon !== "models" && + folderNameCanon !== "attachables" && folderNameCanon !== "render_controllers" && folderNameCanon !== "animation_controllers" && folderNameCanon !== "animations" diff --git a/app/src/info/StrictPlatformInfoGenerator.ts b/app/src/info/StrictPlatformInfoGenerator.ts index 925583ef..161ca7c9 100644 --- a/app/src/info/StrictPlatformInfoGenerator.ts +++ b/app/src/info/StrictPlatformInfoGenerator.ts @@ -59,7 +59,7 @@ export default class StrictPlatformInfoGenerator implements IProjectInfoGenerato this.id, 100, `Uses a minecraft: identifier override`, - undefined, + pi, desc.identifier ) ); @@ -76,7 +76,7 @@ export default class StrictPlatformInfoGenerator implements IProjectInfoGenerato this.id, 100, `Uses a runtime_identifier override`, - undefined, + pi, desc.runtime_identifier ) ); diff --git a/app/src/local/IAuthenticationToken.ts b/app/src/local/IAuthenticationToken.ts index 1a57370a..5d0c74a0 100644 --- a/app/src/local/IAuthenticationToken.ts +++ b/app/src/local/IAuthenticationToken.ts @@ -1,4 +1,13 @@ +export enum ServerPermissionLevel { + none = 0, + displayReadOnly = 1, + fullReadOnly = 2, + updateState = 3, + admin = 4, +} + export interface IAuthenticationToken { time: number; code: string; + permissionLevel: ServerPermissionLevel; } diff --git a/app/src/minecraft/BehaviorManifestJson.ts b/app/src/minecraft/BehaviorManifestJson.ts index 9c3c20c1..e67b7e47 100644 --- a/app/src/minecraft/BehaviorManifestJson.ts +++ b/app/src/minecraft/BehaviorManifestJson.ts @@ -30,6 +30,14 @@ export default class BehaviorManifestJson { return this._onLoaded.asEvent(); } + public get productType() { + if (!this.definition || !this.definition.metadata) { + return undefined; + } + + return this.definition.metadata.product_type; + } + public get description() { if (!this.definition || !this.definition.header) { return undefined; @@ -74,6 +82,22 @@ export default class BehaviorManifestJson { this._id = newId; } + public getNonInternalDependencyCount() { + if (!this.definition || !this.definition.dependencies) { + return 0; + } + + let count = 0; + + for (let dependency of this.definition.dependencies) { + if (dependency.uuid) { + count++; + } + } + + return count; + } + static async ensureOnFile(file: IFile, loadHandler?: IEventHandler) { let bmj: BehaviorManifestJson | undefined = undefined; diff --git a/app/src/minecraft/IAddonManifest.ts b/app/src/minecraft/IAddonManifest.ts index 97aa0b97..da177384 100644 --- a/app/src/minecraft/IAddonManifest.ts +++ b/app/src/minecraft/IAddonManifest.ts @@ -4,6 +4,18 @@ export default interface IAddonManifest { header: IAddonManifestHeader; modules: IAddonModule[]; dependencies: IAddonDependency[]; + metadata?: IAddonMetadata; + capabilities?: string[]; +} + +export interface IResourcePackManifest { + format_version: number; + __comment__?: string; + header: IResourceAddonManifestHeader; + modules: IAddonModule[]; + dependencies: IAddonDependency[]; + metadata?: IAddonMetadata; + capabilities?: string[]; } export interface IAddonManifestHeader { @@ -14,6 +26,10 @@ export interface IAddonManifestHeader { min_engine_version: number[]; } +export interface IResourceAddonManifestHeader extends IAddonManifestHeader { + pack_scope?: "world" | "global" | "any"; +} + export interface IAddonModule { description: string; type: string; @@ -28,3 +44,11 @@ export interface IAddonDependency { module_name?: string; version: number[] | string; } + +export interface IAddonMetadata { + license?: string; + authors?: string[]; + url?: string; + product_type?: "" | "addon"; + generated_with?: { [toolName: string]: string[] }; +} diff --git a/app/src/minecraft/ResourceManifestJson.ts b/app/src/minecraft/ResourceManifestJson.ts index 6caf3d90..1b4a22be 100644 --- a/app/src/minecraft/ResourceManifestJson.ts +++ b/app/src/minecraft/ResourceManifestJson.ts @@ -1,7 +1,7 @@ import IFile from "../storage/IFile"; import Log from "../core/Log"; import { EventDispatcher, IEventHandler } from "ste-events"; -import IAddonManifest, { IAddonManifestHeader } from "./IAddonManifest"; +import { IAddonManifestHeader, IResourcePackManifest } from "./IAddonManifest"; import Utilities from "../core/Utilities"; import Project from "../app/Project"; @@ -11,7 +11,7 @@ export default class ResourceManifestJson { private _version?: string; private _isLoaded: boolean = false; - public definition?: IAddonManifest; + public definition?: IResourcePackManifest; private _onLoaded = new EventDispatcher(); @@ -45,6 +45,22 @@ export default class ResourceManifestJson { } } + public get packScope() { + if (!this.definition || !this.definition.header) { + return undefined; + } + + return this.definition.header.pack_scope; + } + + public get productType() { + if (!this.definition || !this.definition.metadata) { + return undefined; + } + + return this.definition.metadata.product_type; + } + public get name() { if (this.definition && this.definition.header) { return this.definition.header.name; diff --git a/app/src/storage/BrowserStorage.ts b/app/src/storage/BrowserStorage.ts index 58c0c55d..6b4d98fa 100644 --- a/app/src/storage/BrowserStorage.ts +++ b/app/src/storage/BrowserStorage.ts @@ -1,9 +1,11 @@ import BrowserFolder from "./BrowserFolder"; import StorageBase from "./StorageBase"; import IStorage from "./IStorage"; +import localforage from "localforage"; export default class BrowserStorage extends StorageBase implements IStorage { rootFolder: BrowserFolder; + static isConfigured: boolean = false; static readonly folderDelimiter = "/"; @@ -16,6 +18,16 @@ export default class BrowserStorage extends StorageBase implements IStorage { name = "." + name; } + if (!BrowserStorage.isConfigured) { + localforage.config({ + name: "Minecraft Creator Tools", + storeName: "minecraft_creator_tools", + version: 1.0, + }); + + BrowserStorage.isConfigured = true; + } + this.rootFolder = new BrowserFolder(this, null, "fs" + name, "root"); } diff --git a/app/src/storage/FileSystemFile.ts b/app/src/storage/FileSystemFile.ts index b144ea67..5e7bc706 100644 --- a/app/src/storage/FileSystemFile.ts +++ b/app/src/storage/FileSystemFile.ts @@ -237,20 +237,8 @@ export default class FileSystemFile extends FileBase implements IFile { async saveContent(force?: boolean): Promise { if (this.needsSave || force) { - /*const contentDescript = "null"; - - if (this.content instanceof Uint8Array) { - contentDescript = this.content.length + " bytes"; - } else if (typeof this.content === "string") { - contentDescript = this.content.length + " text"; - }*/ - Log.assert(this.content !== null, "Null content found."); - // Log.debug("Saving file " + contentDescript + " to '" + this.fullPath + "'"); - - //await localforage.setItem(this.fullPath, this.content); - if (this.content !== null) { const handle = await this.ensureWriteHandle(); diff --git a/app/src/storage/FileSystemFolder.ts b/app/src/storage/FileSystemFolder.ts index 5a8737d2..fca9421d 100644 --- a/app/src/storage/FileSystemFolder.ts +++ b/app/src/storage/FileSystemFolder.ts @@ -195,7 +195,7 @@ export default class FileSystemFolder extends FolderBase implements IFolder { await this._parentFolder.save(true); } - throw new Error("Not implemented."); // need to add considerable localforage remapping, including remapping and removing source keys + throw new Error("Not implemented."); //return true; }