From daf6d1fab04c9d393c36191a872813d0f09acbf9 Mon Sep 17 00:00:00 2001 From: Allen Kinzalow Date: Tue, 13 Feb 2024 23:27:56 -0500 Subject: [PATCH] Add support for generating cache diff pages (#3) * Add support for detecting file-level changes between two caches * diff items * Compile resulting cache changes into one resulting object * start of index name mapping * Basic support for struct differences * Add support for including added and removed files in resulting diffs * Set up supported PerFileLoadable columns for builder * Add build output for added diffs * Add changed diff building * Fix various styling bugs * Add support for structs, enums, params, and dbrows * changesets * Comments --- .changeset/giant-rings-pull.md | 5 + .changeset/light-dolls-sort.md | 5 + .changeset/many-jars-explode.md | 5 + .github/workflows/workflow-dispatch.yml | 12 +- package.json | 4 +- src/index.ts | 14 +- src/scripts/clues/utils.ts | 4 +- src/scripts/difference/difference.ts | 170 ---------- src/scripts/difference/difference.utils.ts | 39 --- src/scripts/differences/builder/builder.ts | 318 ++++++++++++++++++ .../differences/builder/builder.types.ts | 120 +++++++ .../differences/builder/builder.utils.ts | 87 +++++ src/scripts/differences/builder/index.ts | 3 + src/scripts/differences/differences.ts | 204 +++++++++++ src/scripts/differences/differences.types.ts | 69 ++++ src/scripts/differences/differences.utils.ts | 13 + .../differences/file/content/dbrows.ts | 31 ++ src/scripts/differences/file/content/enums.ts | 31 ++ src/scripts/differences/file/content/items.ts | 31 ++ src/scripts/differences/file/content/npcs.ts | 31 ++ .../differences/file/content/objects.ts | 31 ++ .../differences/file/content/params.ts | 31 ++ .../differences/file/content/struct.ts | 31 ++ src/scripts/differences/file/file.ts | 136 ++++++++ src/utils/cache2/loaders/Obj.ts | 13 +- src/utils/cache2/types.ts | 48 +++ src/utils/colors.ts | 21 ++ src/utils/string.ts | 2 + yarn.lock | 16 +- 29 files changed, 1292 insertions(+), 233 deletions(-) create mode 100644 .changeset/giant-rings-pull.md create mode 100644 .changeset/light-dolls-sort.md create mode 100644 .changeset/many-jars-explode.md delete mode 100644 src/scripts/difference/difference.ts delete mode 100644 src/scripts/difference/difference.utils.ts create mode 100644 src/scripts/differences/builder/builder.ts create mode 100644 src/scripts/differences/builder/builder.types.ts create mode 100644 src/scripts/differences/builder/builder.utils.ts create mode 100644 src/scripts/differences/builder/index.ts create mode 100644 src/scripts/differences/differences.ts create mode 100644 src/scripts/differences/differences.types.ts create mode 100644 src/scripts/differences/differences.utils.ts create mode 100644 src/scripts/differences/file/content/dbrows.ts create mode 100644 src/scripts/differences/file/content/enums.ts create mode 100644 src/scripts/differences/file/content/items.ts create mode 100644 src/scripts/differences/file/content/npcs.ts create mode 100644 src/scripts/differences/file/content/objects.ts create mode 100644 src/scripts/differences/file/content/params.ts create mode 100644 src/scripts/differences/file/content/struct.ts create mode 100644 src/scripts/differences/file/file.ts create mode 100644 src/utils/colors.ts create mode 100644 src/utils/string.ts diff --git a/.changeset/giant-rings-pull.md b/.changeset/giant-rings-pull.md new file mode 100644 index 0000000..e289c48 --- /dev/null +++ b/.changeset/giant-rings-pull.md @@ -0,0 +1,5 @@ +--- +"@osrs-wiki/cache-mediawiki": minor +--- + +Add support for loading different cache versions diff --git a/.changeset/light-dolls-sort.md b/.changeset/light-dolls-sort.md new file mode 100644 index 0000000..fc045e4 --- /dev/null +++ b/.changeset/light-dolls-sort.md @@ -0,0 +1,5 @@ +--- +"@osrs-wiki/cache-mediawiki": minor +--- + +Add support for writing cache differences to a wiki page. diff --git a/.changeset/many-jars-explode.md b/.changeset/many-jars-explode.md new file mode 100644 index 0000000..bd8dc9a --- /dev/null +++ b/.changeset/many-jars-explode.md @@ -0,0 +1,5 @@ +--- +"@osrs-wiki/cache-mediawiki": minor +--- + +Add support for detecting cache differences in indices, archives, and files diff --git a/.github/workflows/workflow-dispatch.yml b/.github/workflows/workflow-dispatch.yml index 6e49120..cf373d5 100644 --- a/.github/workflows/workflow-dispatch.yml +++ b/.github/workflows/workflow-dispatch.yml @@ -1,4 +1,4 @@ -name: Run News Scraper +name: Run Cache Tasks on: workflow_dispatch: @@ -11,6 +11,14 @@ on: description: "Infobox type (optional)" required: false + oldCache: + description: "Old cache date (optional)" + required: false + + newCache: + description: "New cache date (optional)" + required: false + jobs: scrapeNews: runs-on: ubuntu-latest @@ -30,7 +38,7 @@ jobs: run: yarn build - name: Run task - run: yarn start:node -t ${{ github.event.inputs.task }} -infobox ${{ github.event.inputs.infobox }} + run: yarn start:node -t ${{ github.event.inputs.task }} --infobox ${{ github.event.inputs.infobox }} --oldCache ${{ github.event.inputs.oldCache }} --newCache ${{ github.event.inputs.newCache }} - name: Upload Output uses: actions/upload-artifact@v3 diff --git a/package.json b/package.json index 40e79bc..5bb333a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ }, "homepage": "https://github.com/osrs-wiki/cache-mediawiki#readme", "dependencies": { - "@osrs-wiki/mediawiki-builder": "^1.1.2", + "@osrs-wiki/mediawiki-builder": "^1.2.0", "dotenv": "^16.4.1", "fflate": "^0.8.1", "tsconfig-paths": "^4.2.0", @@ -55,7 +55,7 @@ "ts-jest": "^28.0.8", "ts-node": "^10.9.1", "ts-patch": "^2.1.0", - "typescript": "^4.6.3" + "typescript": "^5.3.2" }, "lint-staged": { "src/**/*.{js,ts}": [ diff --git a/src/index.ts b/src/index.ts index 0ce4c9b..4872360 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,15 +2,21 @@ import config from "@config"; import { parseArgs } from "node:util"; import generateCluePages from "./scripts/clues"; -import differences from "./scripts/difference/difference"; +import differencesCache from "./scripts/differences/differences"; import infoboxGenerator from "./scripts/infoboxGenernator"; console.log(`Running ${config.environment}`); const { - values: { task, infobox }, + values: { oldCache, newCache, task, infobox }, } = parseArgs({ options: { + oldCache: { + type: "string", + }, + newCache: { + type: "string", + }, task: { type: "string", short: "t", @@ -21,8 +27,8 @@ const { }, }); -if (task === "differences") { - differences(); +if (task === "differences" || (task === "diffs" && oldCache)) { + differencesCache(oldCache, newCache); } else if (task === "infobox" && infobox) { infoboxGenerator(infobox); } else if (task === "clues") { diff --git a/src/scripts/clues/utils.ts b/src/scripts/clues/utils.ts index 2ad0860..ff1925d 100644 --- a/src/scripts/clues/utils.ts +++ b/src/scripts/clues/utils.ts @@ -21,11 +21,11 @@ export const getCacheProvider = async () => { }); }; -export const getCacheProviderGithub = async () => { +export const getCacheProviderGithub = async (version = "master") => { return new FlatCacheProvider({ async getFile(name) { const req = await fetch( - `https://raw.githubusercontent.com/abextm/osrs-cache/master/${name}` + `https://raw.githubusercontent.com/abextm/osrs-cache/${version}/${name}` ); if (!req.ok) { return; diff --git a/src/scripts/difference/difference.ts b/src/scripts/difference/difference.ts deleted file mode 100644 index 3084e68..0000000 --- a/src/scripts/difference/difference.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { existsSync, writeFileSync } from "node:fs"; -import { mkdir, readFile, readdir } from "node:fs/promises"; -import { dirname } from "node:path"; - -import { getJsonDifference, getStringDifference } from "./difference.utils"; - -const OLD_DUMP = "./data/olddump"; -const NEW_DUMP = "./data/newdump"; - -const EXCLUDED_DIRS = ["object_defs", "binary", "interface_defs"]; - -const differences = async () => { - const directories = await readdir(OLD_DUMP); - - const queue = []; - directories.forEach(async (directory) => { - if (EXCLUDED_DIRS.includes(directory)) { - return; - } - try { - await directoryDifference(directory); - } catch (error) { - queue.push(directory); - } - }); -}; - -const directoryDifference = async (directory: string) => { - console.log(`Checking dir difference: ${directory}`); - const oldDirectoryContent = await readdir(`${OLD_DUMP}/${directory}`); - const newDirectoryContent = await readdir(`${NEW_DUMP}/${directory}`); - - oldDirectoryContent - .filter((content) => !content.includes(".")) - .forEach(async (subdir) => { - const newExists = existsSync(`${NEW_DUMP}/${directory}/${subdir}`); - if (newExists) { - await directoryDifference(`${directory}/${subdir}`); - } else { - const subDirContent = await readdir( - `${OLD_DUMP}/${directory}/${subdir}` - ); - subDirContent.forEach(async (file) => { - const fileData = await readFile( - `${OLD_DUMP}/${directory}/${subdir}/${file}` - ); - await writeFileDiff( - "removed", - `${directory}/${subdir}/${file}`, - fileData - ); - }); - } - }); - - newDirectoryContent - .filter((content) => !content.includes(".")) - .forEach(async (subdir) => { - const oldExists = existsSync(`${OLD_DUMP}/${directory}/${subdir}`); - if (oldExists) { - await directoryDifference(`${directory}/${subdir}`); - } else { - const subDirContent = await readdir( - `${NEW_DUMP}/${directory}/${subdir}` - ); - subDirContent.forEach(async (file) => { - const fileData = await readFile( - `${NEW_DUMP}/${directory}/${subdir}/${file}` - ); - await writeFileDiff( - "added", - `${directory}/${subdir}/${file}`, - fileData - ); - }); - } - }); - - const oldFiles = oldDirectoryContent.filter((content) => - content.includes(".") - ); - const newFiles = newDirectoryContent.filter((content) => - content.includes(".") - ); - - const added = newFiles.filter((file) => !oldFiles.includes(file)); - let same = newFiles.filter((file) => oldFiles.includes(file)); - same = oldFiles.filter((file) => same.includes(file)); - const removed = oldFiles.filter((file) => !newFiles.includes(file)); - - same.forEach(async (file) => { - try { - if (file.endsWith(".json")) { - await saveJsonDifferences(`${directory}/${file}`); - } else if (file.endsWith(".png")) { - await writeImageDifferences( - `${OLD_DUMP}/${directory}/${file}`, - `${NEW_DUMP}/${directory}/${file}` - ); - } else { - } - } catch (error) { - console.log(`Failed to diff ${file}`); - } - }); - - added.forEach(async (file) => { - const fileData = await readFile(`${NEW_DUMP}/${directory}/${file}`); - await writeFileDiff("added", `${directory}/${file}`, fileData); - }); - - removed.forEach(async (file) => { - const fileData = await readFile(`${OLD_DUMP}/${directory}/${file}`); - await writeFileDiff("removed", `${directory}/${file}`, fileData); - }); -}; - -const saveJsonDifferences = async (path: string) => { - try { - const oldFile = await readFile(`${OLD_DUMP}/${path}`, "utf8"); - const newFile = await readFile(`${NEW_DUMP}/${path}`, "utf8"); - try { - const oldJson = JSON.parse(oldFile); - const newJson = JSON.parse(newFile); - const differentJson = getJsonDifference(oldJson, newJson); - if (Object.keys(differentJson).length > 0) { - await writeFileDiff( - "differences", - path, - JSON.stringify(differentJson, null, "\t") - ); - } - } catch (e) { - writeStringDifferences(oldFile, newFile); - } - } catch (e) { - console.log(`Failed to save: ${path}`); - } -}; - -const writeStringDifferences = (oldFile: string, newFile: string) => { - const addedString = getStringDifference(newFile, oldFile); - const removedString = getStringDifference(oldFile, newFile); -}; - -const writeImageDifferences = async ( - oldFilePath: string, - newFilePath: string -) => { - const oldFile = await readFile(oldFilePath); - const newFile = await readFile(newFilePath); - if (oldFile.compare(newFile) !== 0) { - await writeFileDiff("removed", oldFilePath.replace(OLD_DUMP, ""), oldFile); - await writeFileDiff("added", newFilePath.replace(NEW_DUMP, ""), newFile); - } -}; - -const writeFileDiff = async ( - type: string, - path: string, - data: string | Buffer -) => { - const fullPath = `./out/differences/${type}/${path}`; - - await mkdir(dirname(fullPath), { recursive: true }); - - writeFileSync(fullPath, data); -}; - -export default differences; diff --git a/src/scripts/difference/difference.utils.ts b/src/scripts/difference/difference.utils.ts deleted file mode 100644 index a7b7cbb..0000000 --- a/src/scripts/difference/difference.utils.ts +++ /dev/null @@ -1,39 +0,0 @@ -import _ from "underscore"; - -// Helper for the call to _.mapObject/_.mapValues below. -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -const wrapAddedKey = (newValue: any) => ({ oldValue: undefined, newValue }); - -export const getJsonDifference = (oldCollection: any, newCollection: any) => { - const diff = _.reduce( - oldCollection, - function (result: any, oldValue: any, key: any) { - const newValue = newCollection[key]; - if (_.isObject(oldValue) && _.isObject(newValue)) { - const diff = getJsonDifference(oldValue, newValue); - if (!_.isEmpty(diff)) result[key] = diff; - } else if (oldValue !== newValue) { - result[key] = { oldValue, newValue }; - } - return result; - }, - _.isArray(oldCollection) ? [] : {} - ); - const addedKeys = _.difference(_.keys(newCollection), _.keys(oldCollection)); - const additions = _.pick(newCollection, addedKeys); - return _.extend(diff, _.mapObject(additions, wrapAddedKey)); -}; - -export const getStringDifference = (a: string, b: string) => { - let i = 0; - let j = 0; - let result = ""; - - while (j < b.length) { - if (a[i] != b[j] || i == a.length) result += b[j]; - else i++; - j++; - } - return result; -}; diff --git a/src/scripts/differences/builder/builder.ts b/src/scripts/differences/builder/builder.ts new file mode 100644 index 0000000..522cfdf --- /dev/null +++ b/src/scripts/differences/builder/builder.ts @@ -0,0 +1,318 @@ +import { + MediaWikiBreak, + MediaWikiBuilder, + MediaWikiContent, + MediaWikiHeader, + MediaWikiTOC, + MediaWikiTable, + MediaWikiText, +} from "@osrs-wiki/mediawiki-builder"; +import type { + MediaWikiTableCell, + MediaWikiTableRow, +} from "@osrs-wiki/mediawiki-builder"; +import _ from "underscore"; + +import { IndexFeatures, indexNameMap, resultNameMap } from "./builder.types"; +import { formatEntryIdentifier, formatEntryValue } from "./builder.utils"; +import { IndexType } from "../../../utils/cache2"; +import { capitalize } from "../../../utils/string"; +import { + ArchiveDifferences, + CacheDifferences, + ChangedResult, + Difference, + Result, +} from "../differences.types"; + +/** + * Retrieve a MediaWikiBuilder filled with content from two cache differences. + * @param differences The differences between two caches. + * @returns {MediaWikiBuilder} + */ +const differencesBuilder = ( + differences: CacheDifferences +): MediaWikiBuilder => { + const builder = new MediaWikiBuilder(); + + builder.addContents([new MediaWikiTOC()]); + + // TODO: Build from supported index and archives + /*Object.keys(indexNameMap).forEach((index) => { + const indexFeatureMap = indexNameMap[index as unknown as IndexType]; + if (indexFeatureMap) { + if ("name" in indexFeatureMap) { + builder.addContents( + buildArchiveDifferences(differences[index as unknown as IndexType]?.[], indexFeatureMap) + ); + } else { + Object.keys(indexFeatureMap).forEach((archive) => { + const archiveFeatureMap = + indexFeatureMap[archive as unknown as number]; + }); + } + } + });*/ + + Object.keys(differences).forEach((index) => { + const indexFeatureMap = indexNameMap[index as unknown as IndexType]; + if (indexFeatureMap) { + const archives = differences[index as unknown as number]; + Object.keys(archives).forEach((archive) => { + const archiveNumber = archive as unknown as number; + const archiveDifferences = archives[archiveNumber]; + const indexFeature = + "name" in indexFeatureMap + ? indexFeatureMap + : indexFeatureMap[archiveNumber]; + if (indexFeature) { + builder.addContents( + buildArchiveDifferences(archiveDifferences, indexFeature) + ); + } + }); + } + }); + + return builder; +}; + +/** + * Build the media wiki content for the differences in two cache archive files. + * @param archiveDifferences Differences between two cache's archives + * @param indexFeatures Meta deta for indexes + * @returns {MediaWikiContent[]} + */ +const buildArchiveDifferences = ( + archiveDifferences: ArchiveDifferences, + indexFeatures: IndexFeatures +): MediaWikiContent[] => { + const content: MediaWikiContent[] = [ + new MediaWikiHeader(indexFeatures.name, 2), + new MediaWikiBreak(), + ...buildFullResultTable(archiveDifferences, indexFeatures, "added"), + ...buildFullResultTable(archiveDifferences, indexFeatures, "removed"), + ...buildChangedResultTable(archiveDifferences, indexFeatures), + ]; + + return content; +}; + +/** + * Builds media wiki content for archive file differences. + * These tables include all changed fields. + * @param archiveDifferences Differences between two cache's archives + * @param indexFeatures Meta deta for indexes + * @returns {MediaWikiContent[]} + */ +const buildChangedResultTable = ( + archiveDifferences: ArchiveDifferences, + indexFeatures: IndexFeatures +) => { + const differenceName = resultNameMap.changed; + const content: MediaWikiContent[] = []; + const entries: ChangedResult[] = _.pluck( + Object.values(archiveDifferences), + "changed" + ).filter( + (entry) => + entry !== null && entry !== undefined && Object.keys(entry).length > 0 + ); + + const rows: MediaWikiTableRow[] = + entries?.length > 0 + ? entries + .map((entry) => { + const diffKeys = Object.keys(entry).filter((key) => { + const isIdentifier = ( + indexFeatures.identifiers as string[] + ).includes(key); + return ( + !isIdentifier || entry[key].oldValue !== entry[key].newValue + ); + }); + return diffKeys.length > 0 + ? [ + { + cells: indexFeatures.identifiers.map( + (identifier) => ({ + content: formatEntryIdentifier( + identifier, + entry[identifier].newValue, + indexFeatures.urls + ), + options: { + rowspan: diffKeys.length + 1, + }, + }) + ), + }, + ...diffKeys.map((field) => ({ + cells: [ + { + content: [new MediaWikiText(field)], + }, + { + content: [ + new MediaWikiText( + formatEntryValue(field, entry[field].oldValue) + ), + ], + }, + { + content: [ + new MediaWikiText( + formatEntryValue(field, entry[field].newValue) + ), + ], + }, + ], + minimal: true, + })), + ] + : undefined; + }) + .flat() + .filter((value) => value !== undefined) + : []; + + content.push( + new MediaWikiHeader(`${differenceName} ${indexFeatures.name}`, 3), + new MediaWikiBreak(), + new MediaWikiTable({ + rows: [ + { + header: true, + minimal: true, + cells: [ + { + content: [ + new MediaWikiText(`${differenceName} ${indexFeatures.name}`), + ], + options: { + colspan: 5, + }, + }, + ], + }, + { + header: true, + minimal: true, + cells: [ + ...indexFeatures.identifiers.map((identifier) => + capitalize(identifier) + ), + "Key", + "Previous Value", + "New Value", + ].map((column, index) => ({ + content: [new MediaWikiText(column)], + options: index == 0 ? { style: "width: 15em" } : undefined, + })), + }, + ...rows, + ], + options: { + class: "wikitable sortable sticky-header", + }, + }), + new MediaWikiBreak() + ); + return content; +}; + +/** + * Build media wiki content for archive file's additions or removals. + * These tables include specified fields from indexFeatures. + * @param archiveDifferences Differences between two cache's archive files + * @param indexFeatures Meta deta for indexes + * @param type Definition for added or removed content + * @returns + */ +const buildFullResultTable = ( + archiveDifferences: ArchiveDifferences, + indexFeatures: IndexFeatures, + type: Difference +): MediaWikiContent[] => { + const differenceName = resultNameMap[type]; + const tableFields = indexFeatures.fields.map((field) => field.toString()); + const fields = + type === "added" + ? [...indexFeatures.identifiers, ...tableFields] + : indexFeatures.identifiers; + const content: MediaWikiContent[] = []; + const entries: Result[] = _.pluck( + Object.values(archiveDifferences), + type + ).filter((entry) => entry !== null && entry !== undefined); + + const rows: MediaWikiTableRow[] = + entries?.length > 0 + ? entries.map((entry) => { + const identifierCells = indexFeatures.identifiers.map( + (identifier) => ({ + content: formatEntryIdentifier( + identifier, + entry[identifier], + indexFeatures.urls + ), + }) + ); + return { + cells: + type === "removed" + ? identifierCells + : [ + ...identifierCells, + ...tableFields.map((field) => ({ + content: [ + new MediaWikiText( + formatEntryValue(field, entry[field]) + ), + ], + })), + ], + minimal: true, + }; + }) + : []; + + content.push( + new MediaWikiHeader(`${differenceName} ${indexFeatures.name}`, 3), + new MediaWikiBreak(), + new MediaWikiTable({ + rows: [ + { + header: true, + minimal: true, + cells: [ + { + content: [ + new MediaWikiText(`${differenceName} ${indexFeatures.name}`), + ], + options: { + colspan: fields.length, + }, + }, + ], + }, + { + header: true, + minimal: true, + cells: fields.map((field, index) => ({ + content: [new MediaWikiText(field)], + options: index == 0 ? { style: "width: 15em" } : undefined, + })), + }, + ...rows, + ], + options: { + class: "wikitable sortable", + }, + }), + new MediaWikiBreak() + ); + return content; +}; + +export default differencesBuilder; diff --git a/src/scripts/differences/builder/builder.types.ts b/src/scripts/differences/builder/builder.types.ts new file mode 100644 index 0000000..c9d584e --- /dev/null +++ b/src/scripts/differences/builder/builder.types.ts @@ -0,0 +1,120 @@ +import { + ConfigType, + DBRow, + Enum, + IndexType, + Item, + NPC, + Obj, + Param, + Struct, +} from "../../../utils/cache2"; +import { PerFileLoadable } from "../../../utils/cache2/Loadable"; +import { Difference } from "../differences.types"; + +export type IndexFeature = { + name: Name; + identifiers: (keyof T)[]; + fields: (keyof T)[]; + urls?: IndexURLs; +}; + +export type IndexURLType = "abex" | "chisel"; + +export type IndexURLs = { [key in IndexURLType]?: string }; + +export type IndexFeatures = + | IndexFeature + | IndexFeature + | IndexFeature + | IndexFeature + | IndexFeature + | IndexFeature + | IndexFeature; + +export const resultNameMap: { [key in Difference]: string } = { + added: "New", + changed: "Diff", + removed: "Removed", +}; + +export const indexNameMap: { + [key in IndexType]?: { [key: number]: IndexFeatures } | IndexFeatures; +} = { + [IndexType.Configs]: { + [ConfigType.Object]: { + name: "Objects", + identifiers: ["name", "id"], + fields: ["actions"], + urls: { + chisel: "https://chisel.weirdgloop.org/moid/object_id.html#", + abex: "https://abextm.github.io/cache2/#/viewer/obj/", + }, + }, + [ConfigType.Enum]: { + name: "Enums", + identifiers: ["id"], + fields: ["defaultValue", "map"], + urls: { + chisel: + "https://chisel.weirdgloop.org/structs/index.html?type=enums&id=", + abex: "https://abextm.github.io/cache2/#/viewer/enum/", + }, + }, + [ConfigType.Npc]: { + name: "Npcs", + identifiers: ["name", "id"], + fields: ["combatLevel", "actions"], + urls: { + chisel: "https://chisel.weirdgloop.org/moid/npc_id.html#", + abex: "https://abextm.github.io/cache2/#/viewer/npc/", + }, + }, + [ConfigType.Item]: { + name: "Items", + identifiers: ["name", "id"], + fields: [ + "isMembers", + "isGrandExchangable", + "isStackable", + "noteLinkedItem", + "inventoryActions", + "placeholderLinkedItem", + "price", + "weight", + ], + urls: { + chisel: "https://chisel.weirdgloop.org/moid/item_id.html#", + abex: "https://abextm.github.io/cache2/#/viewer/item/", + }, + }, + [ConfigType.Params]: { + name: "Params", + identifiers: ["id"], + fields: ["type", "defaultInt", "defaultString", "isMembers"], + urls: { + chisel: + "https://chisel.weirdgloop.org/structs/index.html?type=enums&id=", + abex: "https://abextm.github.io/cache2/#/viewer/enum/", + }, + }, + [ConfigType.Struct]: { + name: "Structs", + identifiers: ["id"], + fields: ["params"], + urls: { + chisel: + "https://chisel.weirdgloop.org/structs/index.html?type=structs&id=", + abex: "https://abextm.github.io/cache2/#/viewer/struct/", + }, + }, + [ConfigType.DbRow]: { + name: "Database Rows", + identifiers: ["id"], + fields: ["table", "values"], + urls: { + abex: "https://abextm.github.io/cache2/#/viewer/dbrow/", + }, + }, + }, +}; diff --git a/src/scripts/differences/builder/builder.utils.ts b/src/scripts/differences/builder/builder.utils.ts new file mode 100644 index 0000000..56fb0d9 --- /dev/null +++ b/src/scripts/differences/builder/builder.utils.ts @@ -0,0 +1,87 @@ +import { + MediaWikiContent, + MediaWikiExternalLink, + MediaWikiLink, + MediaWikiText, +} from "@osrs-wiki/mediawiki-builder"; + +import { IndexURLType, IndexURLs } from "./builder.types"; +import { jagexHSLtoHex } from "../../../utils/colors"; +import { ResultValue } from "../differences.types"; + +/** + * Format the value a field. + * @param field The field of a {Result} + * @param value The unformatted {ResultValue} + * @returns A formatted string. + */ +export const formatEntryValue = (field: string, value: ResultValue): string => { + if ( + !value || + (typeof value === "object" && Object.keys(value).length === 0) + ) { + return ""; + } + if ( + field.toLocaleLowerCase().includes("color") && + typeof value === "number" + ) { + const hex = jagexHSLtoHex(value); + return `${value}`; + } else if (Array.isArray(value)) { + /*if (field.toLocaleLowerCase().includes("color")) { + return value.reduce( + (result, color) => + result + + " " + + `${color}`, + "" + ); + }*/ + const mappedValues = value.map((value) => + value == null || value == undefined ? "None" : value + ); + const stringValue = + mappedValues.length > 0 ? JSON.stringify(mappedValues) : ""; + return stringValue + .replaceAll('"None"', "None") + .replaceAll('"', "") + .replaceAll(",", ", "); + } else if (typeof value === "string" || typeof value === "number") { + return value.toString(); + } + return JSON.stringify(value) + .replaceAll('"', "'") + .replaceAll(",", ", ") + .replaceAll(":", ": "); +}; + +/** + * Format a field's identifier. + * @param identifier The field identifier key + * @param value The Result value + * @param urls Supported urls for names and identifiers + * @returns + */ +export const formatEntryIdentifier = ( + identifier: string, + value: ResultValue, + urls: IndexURLs +): MediaWikiContent[] => { + switch (identifier) { + case "id": + return Object.keys(urls).map( + (url, index) => + new MediaWikiExternalLink( + index === 0 ? (value as string) : `(${index})`, + `${urls[url as IndexURLType]}${value as string}` + ) + ); + case "name": + return [new MediaWikiLink(value as string)]; + default: + return [new MediaWikiText(formatEntryValue(identifier, value))]; + } +}; diff --git a/src/scripts/differences/builder/index.ts b/src/scripts/differences/builder/index.ts new file mode 100644 index 0000000..49e988d --- /dev/null +++ b/src/scripts/differences/builder/index.ts @@ -0,0 +1,3 @@ +import differencesBuilder from "./builder"; + +export default differencesBuilder; diff --git a/src/scripts/differences/differences.ts b/src/scripts/differences/differences.ts new file mode 100644 index 0000000..107fd7c --- /dev/null +++ b/src/scripts/differences/differences.ts @@ -0,0 +1,204 @@ +import { mkdir, writeFile } from "fs/promises"; + +import differencesBuilder from "./builder"; +import { + ArchiveDifferences, + CacheDifferences, + IndexDifferences, +} from "./differences.types"; +import { isEqualBytes } from "./differences.utils"; +import differencesFile from "./file/file"; +import { FlatIndexData, ArchiveData, IndexType } from "../../utils/cache2"; +import { LazyPromise } from "../../utils/cache2/LazyPromise"; +import { getCacheProviderGithub } from "../clues/utils"; + +/** + * Write cache differences to output files. + * @param oldVersion The old abex cache version (ex: 2024-01-31-rev219) + * @param newVersion The new abex cache version (ex: 2024-02-07-rev219) + */ +const differencesCache = async (oldVersion: string, newVersion = "master") => { + const oldCache = await new LazyPromise(() => + getCacheProviderGithub(oldVersion) + ).asPromise(); + const newCache = await new LazyPromise(() => + getCacheProviderGithub(newVersion) + ).asPromise(); + + const cacheDifferences: CacheDifferences = {}; + // TODO: Support more than index 2 + for (let index = 0; index <= IndexType.Configs; index++) { + const oldIndex = await oldCache.getIndex(index); + const newIndex = await newCache.getIndex(index); + if (oldIndex.crc !== newIndex.crc) { + console.log( + `[Index=${index}] ${oldIndex.revision} -> ${newIndex.revision}` + ); + cacheDifferences[index] = differencesIndex(oldIndex, newIndex); + } else { + console.log(`No changes in index ${index}.`); + } + } + + const builder = differencesBuilder(cacheDifferences); + const dir = `./out/differences`; + await mkdir(dir, { recursive: true }); + await writeFile( + `${dir}/${newVersion} JSON.json`, + JSON.stringify(cacheDifferences) + ); + await writeFile(`${dir}/${newVersion}.txt`, builder.build()); +}; + +/** + * Retrieve the differences between two indices, their archives, and their files. + * @param oldIndex The old index + * @param newIndex The new index + * @returns {IndexDifferences} + */ +const differencesIndex = ( + oldIndex: FlatIndexData, + newIndex: FlatIndexData +): IndexDifferences => { + const newKeys = Array.from(newIndex.archives.keys()); + const oldKeys = Array.from(oldIndex.archives.keys()); + const indexDifferences: IndexDifferences = {}; + + const sharedKeys = newKeys.filter((key) => oldIndex.archives.has(key)); + sharedKeys.forEach((archiveKey) => { + const newArchive = newIndex.archives.get(archiveKey); + const oldArchive = oldIndex.archives.get(archiveKey); + + if (newArchive.crc !== oldArchive.crc) { + console.log( + `[Index=${newIndex.id}] Changed archive: ${newArchive.archive} - (${oldArchive.files.size} -> ${newArchive.files.size})` + ); + indexDifferences[archiveKey] = differencesArchive({ + oldIndex, + oldArchive, + newIndex, + newArchive, + }); + } + }); + + const addedKeys = newKeys.filter((key) => !oldIndex.archives.has(key)); + addedKeys.forEach((archiveKey) => { + const newArchive = newIndex.archives.get(archiveKey); + console.log( + `[Index=${newIndex.id}] Added archive: ${newArchive.archive} (${newArchive.files.size} files)` + ); + const results = differencesArchive({ + newIndex, + newArchive, + }); + indexDifferences[archiveKey] = indexDifferences[archiveKey] + ? { + ...indexDifferences[archiveKey], + ...results, + } + : results; + }); + + const removedKeys = oldKeys.filter((key) => !newIndex.archives.has(key)); + removedKeys.forEach((archiveKey) => { + const oldArchive = oldIndex.archives.get(archiveKey); + console.log( + `[Index=${newIndex.id}] Removed archive: ${oldArchive.archive} (${oldArchive.files.size} files)` + ); + const results = differencesArchive({ + oldIndex, + oldArchive, + }); + indexDifferences[archiveKey] = indexDifferences[archiveKey] + ? { + ...indexDifferences[archiveKey], + ...results, + } + : results; + }); + + return indexDifferences; +}; + +/** + * Retrieve the differences between two archives and their files + * @returns {ArchiveDifferences} + */ +const differencesArchive = ({ + oldIndex, + oldArchive, + newIndex, + newArchive, +}: { + oldIndex?: FlatIndexData; + oldArchive?: ArchiveData; + newIndex?: FlatIndexData; + newArchive?: ArchiveData; +}): ArchiveDifferences => { + const newKeys = newArchive ? Array.from(newArchive.files.keys()) : []; + const oldKeys = oldArchive ? Array.from(oldArchive.files.keys()) : []; + const archiveDifferences: ArchiveDifferences = {}; + + if (newArchive && oldArchive) { + const sharedKeys = newKeys.filter((key) => oldArchive.files.has(key)); + sharedKeys.forEach((fileKey) => { + try { + const newFile = newArchive.getFile(fileKey); + const oldFile = oldArchive.getFile(fileKey); + if (!isEqualBytes(oldFile.data, newFile.data)) { + console.log( + `[Index=${newArchive.index}][Archive=${newArchive.archive}] Changed file: ${newFile.id}` + ); + const results = differencesFile({ + newFile: { index: newIndex, archive: newArchive, file: newFile }, + oldFile: { index: oldIndex, archive: oldArchive, file: oldFile }, + }); + archiveDifferences[fileKey] = results; + } + } catch (error) { + console.error( + `Error checking diffs for ${oldIndex.id}/${oldArchive.archive}/${fileKey}` + ); + } + }); + } + + const addedKeys = oldArchive + ? newKeys.filter((key) => !oldArchive.files.has(key)) + : newKeys; + addedKeys?.forEach((fileKey) => { + const newFile = newArchive.getFile(fileKey); + console.log( + `[Index=${newArchive.index}][Archive=${newArchive.archive}] Added file: ${newFile.id}` + ); + const results = differencesFile({ + newFile: { index: newIndex, archive: newArchive, file: newFile }, + }); + archiveDifferences[fileKey] = archiveDifferences[fileKey] + ? { ...archiveDifferences[fileKey], ...results } + : results; + }); + + const removedKeys = newArchive + ? oldKeys.filter((key) => !newArchive.files.has(key)) + : oldKeys; + removedKeys?.forEach((fileKey) => { + const oldFile = oldArchive.getFile(fileKey); + console.log( + `[Index=${oldArchive.index}][Archive=${ + oldArchive.archive + }] Removed file: ${fileKey.toString()}` + ); + const results = differencesFile({ + oldFile: { index: oldIndex, archive: oldArchive, file: oldFile }, + }); + archiveDifferences[fileKey] = archiveDifferences[fileKey] + ? { ...archiveDifferences[fileKey], ...results } + : results; + }); + + return archiveDifferences; +}; + +export default differencesCache; diff --git a/src/scripts/differences/differences.types.ts b/src/scripts/differences/differences.types.ts new file mode 100644 index 0000000..dcd5302 --- /dev/null +++ b/src/scripts/differences/differences.types.ts @@ -0,0 +1,69 @@ +import { + ArchiveData, + ArchiveFile, + CategoryID, + FlatIndexData, + HSL, + ItemID, + ModelID, + Params, + TextureID, + WearPos, +} from "../../utils/cache2"; + +export type FileContext = { + archive: ArchiveData; + index: FlatIndexData; + file: ArchiveFile; +}; + +export type Result = { [key: string]: ResultValue }; + +export type ResultValue = + | string + | number + | boolean + | string[] + | ItemID + | ModelID + | WearPos + | HSL[] + | TextureID[] + | CategoryID + | ItemID[] + | number[] + | Params + | object + | undefined; + +export type CacheDifferences = { + [key in number]?: IndexDifferences; +}; + +export type IndexDifferences = { + [key: number]: ArchiveDifferences; +}; + +export type ArchiveDifferences = { + [key: number]: FileDifferences; +}; + +export type Difference = "added" | "changed" | "removed"; + +export type ChangedResult = { + [key: string]: { + oldValue: ResultValue; + newValue: ResultValue; + }; +}; + +export type FileDifferences = { + added?: Result; + changed?: ChangedResult; + removed?: Result; +}; + +export type CompareFn = (params: { + oldFile?: FileContext; + newFile?: FileContext; +}) => FileDifferences; diff --git a/src/scripts/differences/differences.utils.ts b/src/scripts/differences/differences.utils.ts new file mode 100644 index 0000000..768ef2a --- /dev/null +++ b/src/scripts/differences/differences.utils.ts @@ -0,0 +1,13 @@ +export function isEqualBytes(bytes1: Uint8Array, bytes2: Uint8Array): boolean { + if (bytes1.length !== bytes2.length) { + return false; + } + + for (let i = 0; i < bytes1.length; i++) { + if (bytes1[i] !== bytes2[i]) { + return false; + } + } + + return true; +} diff --git a/src/scripts/differences/file/content/dbrows.ts b/src/scripts/differences/file/content/dbrows.ts new file mode 100644 index 0000000..e08f3e9 --- /dev/null +++ b/src/scripts/differences/file/content/dbrows.ts @@ -0,0 +1,31 @@ +import _ from "underscore"; + +import { DBRow, DBRowID, Reader } from "../../../../utils/cache2"; +import { CompareFn } from "../../differences.types"; +import { getFileDifferences } from "../file"; + +const compareDBRows: CompareFn = ({ oldFile, newFile }) => { + const oldEntry = oldFile + ? DBRow.decode( + new Reader(oldFile.file.data, { + era: "osrs", + indexRevision: oldFile.index.revision, + }), + oldFile.file.id + ) + : undefined; + + const newEntry = newFile + ? DBRow.decode( + new Reader(newFile.file.data, { + era: "osrs", + indexRevision: newFile.index.revision, + }), + newFile.file.id + ) + : undefined; + + return getFileDifferences(oldEntry, newEntry); +}; + +export default compareDBRows; diff --git a/src/scripts/differences/file/content/enums.ts b/src/scripts/differences/file/content/enums.ts new file mode 100644 index 0000000..6cb8582 --- /dev/null +++ b/src/scripts/differences/file/content/enums.ts @@ -0,0 +1,31 @@ +import _ from "underscore"; + +import { Enum, EnumID, Reader } from "../../../../utils/cache2"; +import { CompareFn } from "../../differences.types"; +import { getFileDifferences } from "../file"; + +const compareEnums: CompareFn = ({ oldFile, newFile }) => { + const oldEntry = oldFile + ? Enum.decode( + new Reader(oldFile.file.data, { + era: "osrs", + indexRevision: oldFile.index.revision, + }), + oldFile.file.id + ) + : undefined; + + const newEntry = newFile + ? Enum.decode( + new Reader(newFile.file.data, { + era: "osrs", + indexRevision: newFile.index.revision, + }), + newFile.file.id + ) + : undefined; + + return getFileDifferences(oldEntry, newEntry); +}; + +export default compareEnums; diff --git a/src/scripts/differences/file/content/items.ts b/src/scripts/differences/file/content/items.ts new file mode 100644 index 0000000..1747753 --- /dev/null +++ b/src/scripts/differences/file/content/items.ts @@ -0,0 +1,31 @@ +import _ from "underscore"; + +import { Item, ItemID, Reader } from "../../../../utils/cache2"; +import { CompareFn } from "../../differences.types"; +import { getFileDifferences } from "../file"; + +const compareItems: CompareFn = ({ oldFile, newFile }) => { + const oldEntry = oldFile + ? Item.decode( + new Reader(oldFile.file.data, { + era: "osrs", + indexRevision: oldFile.index.revision, + }), + oldFile.file.id + ) + : undefined; + + const newEntry = newFile + ? Item.decode( + new Reader(newFile.file.data, { + era: "osrs", + indexRevision: newFile.index.revision, + }), + newFile.file.id + ) + : undefined; + + return getFileDifferences(oldEntry, newEntry); +}; + +export default compareItems; diff --git a/src/scripts/differences/file/content/npcs.ts b/src/scripts/differences/file/content/npcs.ts new file mode 100644 index 0000000..c2c656a --- /dev/null +++ b/src/scripts/differences/file/content/npcs.ts @@ -0,0 +1,31 @@ +import _ from "underscore"; + +import { NPC, NPCID, Reader } from "../../../../utils/cache2"; +import { CompareFn } from "../../differences.types"; +import { getFileDifferences } from "../file"; + +const compareNpcs: CompareFn = ({ oldFile, newFile }) => { + const oldEntry = oldFile + ? NPC.decode( + new Reader(oldFile.file.data, { + era: "osrs", + indexRevision: oldFile.index.revision, + }), + oldFile.file.id + ) + : undefined; + + const newEntry = newFile + ? NPC.decode( + new Reader(newFile.file.data, { + era: "osrs", + indexRevision: newFile.index.revision, + }), + newFile.file.id + ) + : undefined; + + return getFileDifferences(oldEntry, newEntry); +}; + +export default compareNpcs; diff --git a/src/scripts/differences/file/content/objects.ts b/src/scripts/differences/file/content/objects.ts new file mode 100644 index 0000000..3a0fbf9 --- /dev/null +++ b/src/scripts/differences/file/content/objects.ts @@ -0,0 +1,31 @@ +import _ from "underscore"; + +import { Obj, ObjID, Reader } from "../../../../utils/cache2"; +import { CompareFn } from "../../differences.types"; +import { getFileDifferences } from "../file"; + +const compareObjects: CompareFn = ({ oldFile, newFile }) => { + const oldEntry = oldFile + ? Obj.decode( + new Reader(oldFile.file.data, { + era: "osrs", + indexRevision: oldFile.index.revision, + }), + oldFile.file.id + ) + : undefined; + + const newEntry = newFile + ? Obj.decode( + new Reader(newFile.file.data, { + era: "osrs", + indexRevision: newFile.index.revision, + }), + newFile.file.id + ) + : undefined; + + return getFileDifferences(oldEntry, newEntry); +}; + +export default compareObjects; diff --git a/src/scripts/differences/file/content/params.ts b/src/scripts/differences/file/content/params.ts new file mode 100644 index 0000000..1edef60 --- /dev/null +++ b/src/scripts/differences/file/content/params.ts @@ -0,0 +1,31 @@ +import _ from "underscore"; + +import { Param, ParamID, Reader } from "../../../../utils/cache2"; +import { CompareFn } from "../../differences.types"; +import { getFileDifferences } from "../file"; + +const compareParams: CompareFn = ({ oldFile, newFile }) => { + const oldEntry = oldFile + ? Param.decode( + new Reader(oldFile.file.data, { + era: "osrs", + indexRevision: oldFile.index.revision, + }), + oldFile.file.id + ) + : undefined; + + const newEntry = newFile + ? Param.decode( + new Reader(newFile.file.data, { + era: "osrs", + indexRevision: newFile.index.revision, + }), + newFile.file.id + ) + : undefined; + + return getFileDifferences(oldEntry, newEntry); +}; + +export default compareParams; diff --git a/src/scripts/differences/file/content/struct.ts b/src/scripts/differences/file/content/struct.ts new file mode 100644 index 0000000..e681345 --- /dev/null +++ b/src/scripts/differences/file/content/struct.ts @@ -0,0 +1,31 @@ +import _ from "underscore"; + +import { Reader, Struct, StructID } from "../../../../utils/cache2"; +import { CompareFn } from "../../differences.types"; +import { getFileDifferences } from "../file"; + +const compareStructs: CompareFn = ({ oldFile, newFile }) => { + const oldEntry = oldFile + ? Struct.decode( + new Reader(oldFile.file.data, { + era: "osrs", + indexRevision: oldFile.index.revision, + }), + oldFile.file.id + ) + : undefined; + + const newEntry = newFile + ? Struct.decode( + new Reader(newFile.file.data, { + era: "osrs", + indexRevision: newFile.index.revision, + }), + newFile.file.id + ) + : undefined; + + return getFileDifferences(oldEntry, newEntry); +}; + +export default compareStructs; diff --git a/src/scripts/differences/file/file.ts b/src/scripts/differences/file/file.ts new file mode 100644 index 0000000..7b59a9d --- /dev/null +++ b/src/scripts/differences/file/file.ts @@ -0,0 +1,136 @@ +import _ from "underscore"; + +import compareDBRows from "./content/dbrows"; +import compareEnums from "./content/enums"; +import compareItems from "./content/items"; +import compareNpcs from "./content/npcs"; +import compareObjects from "./content/objects"; +import compareParams from "./content/params"; +import compareStructs from "./content/struct"; +import { ConfigType, IndexType } from "../../../utils/cache2"; +import { PerFileLoadable } from "../../../utils/cache2/Loadable"; +import { CompareFn, FileDifferences, ResultValue } from "../differences.types"; + +/** + * A map of index and archive types to decoding functions. + */ +const indexMap: { + [key in IndexType]?: { [key: number]: CompareFn } | CompareFn; +} = { + [IndexType.Configs]: { + [ConfigType.DbRow]: compareDBRows, + [ConfigType.Enum]: compareEnums, + [ConfigType.Item]: compareItems, + [ConfigType.Npc]: compareNpcs, + [ConfigType.Object]: compareObjects, + [ConfigType.Params]: compareParams, + [ConfigType.Struct]: compareStructs, + }, +}; + +/** + * Retrieve the file differences between two given files. + * @params + * @returns The differences between two files. + * If either is undefined the difference will be considered additions or removals. + */ +const differencesFile: CompareFn = ({ oldFile, newFile }): FileDifferences => { + const indexId = oldFile?.index?.id ?? newFile?.index?.id; + let comparisonFn = indexMap[indexId as IndexType]; + let results: FileDifferences = {}; + if (typeof comparisonFn === "function") { + results = comparisonFn({ oldFile, newFile }); + } else { + const archiveId = oldFile?.archive?.archive ?? newFile?.archive?.archive; + comparisonFn = comparisonFn?.[archiveId]; + results = comparisonFn?.({ oldFile, newFile }) ?? {}; + } + return results; +}; + +/** + * Retrieve the differences in a file: added, changed, and removed files. + * @param oldEntry The old file data + * @param newEntry The new file data + * @returns {FileDifferences} + */ +export const getFileDifferences = ( + oldEntry: T, + newEntry: T +): FileDifferences => { + const results: FileDifferences = {}; + if (oldEntry && newEntry) { + results.changed = {}; + Object.keys(oldEntry).forEach((key) => { + const oldEntryValue = oldEntry[key as keyof T] as ResultValue; + const newEntryValue = newEntry[key as keyof T] as ResultValue; + + if ( + ((typeof oldEntryValue === "string" || + typeof oldEntryValue === "number") && + oldEntryValue !== newEntryValue) || + (Array.isArray(oldEntryValue) && + Array.isArray(newEntryValue) && + _.difference(oldEntryValue, newEntryValue).length > 0) + ) { + results.changed[key] = { + oldValue: oldEntryValue, + newValue: newEntryValue, + }; + } else if (oldEntryValue instanceof Map && newEntryValue instanceof Map) { + const oldKeys = Array.from(oldEntryValue.keys()); + const newKeys = Array.from(newEntryValue.keys()); + + const addedKeys = newKeys.filter((key) => !oldKeys.includes(key)); + const removedKeys = oldKeys.filter((key) => !newKeys.includes(key)); + const sharedKeys = newKeys.filter((key) => oldKeys.includes(key)); + const changedKeys = sharedKeys.filter( + (key) => oldEntryValue.get(key) !== newEntryValue.get(key) + ); + if ( + addedKeys.length > 0 || + removedKeys.length > 0 || + changedKeys.length > 0 + ) + results.changed[key] = { + oldValue: Object.fromEntries(oldEntryValue), + newValue: Object.fromEntries(newEntryValue), + }; + } + }); + if ("id" in oldEntry && "id" in newEntry) { + results.changed["id"] = { + oldValue: oldEntry.id as number, + newValue: newEntry.id as number, + }; + } + if ("name" in oldEntry && "name" in newEntry) { + results.changed["name"] = { + oldValue: oldEntry.name as string, + newValue: newEntry.name as string, + }; + } + } else if (oldEntry) { + results.removed = {}; + Object.keys(oldEntry).forEach((key) => { + const oldEntryValue = oldEntry[key as keyof T] as ResultValue; + results.removed[key] = + oldEntryValue instanceof Map + ? Object.fromEntries(oldEntryValue) + : oldEntryValue; + }); + } else if (newEntry) { + results.added = {}; + Object.keys(newEntry).forEach((key) => { + const newEntryValue = newEntry[key as keyof T] as ResultValue; + results.added[key] = + newEntryValue instanceof Map + ? Object.fromEntries(newEntryValue) + : newEntryValue; + }); + } + + return results; +}; + +export default differencesFile; diff --git a/src/utils/cache2/loaders/Obj.ts b/src/utils/cache2/loaders/Obj.ts index d5b5477..69ad2fc 100644 --- a/src/utils/cache2/loaders/Obj.ts +++ b/src/utils/cache2/loaders/Obj.ts @@ -26,7 +26,8 @@ export class Obj extends PerFileLoadable { public static readonly index = 2; public static readonly archive = 6; - public models: null | { type: ObjType; model: ModelID }[] = null; + public models: null | ModelID[] = null; + public modelTypes: null | ObjType[] = null; public name = "null"; public width = 1; public length = 1; @@ -80,11 +81,11 @@ export class Obj extends PerFileLoadable { case 5: { const len = r.u8(); v.models = new Array(len); + v.modelTypes = new Array(len); for (let i = 0; i < len; i++) { - v.models[i] = { - model: r.u16(), - type: opcode == 5 ? ObjType.CentrepieceStraight : r.u8(), - }; + v.models[i] = r.u16(); + v.modelTypes[i] = + opcode == 5 ? ObjType.CentrepieceStraight : r.u8(); } break; } @@ -256,7 +257,7 @@ export class Obj extends PerFileLoadable { if (v.isDoor === -1) { v.isDoor = 0; if ( - v.models?.[0]?.type === ObjType.CentrepieceStraight || + v.modelTypes?.[0] === ObjType.CentrepieceStraight || v.actions.some((a) => a !== null) ) { v.isDoor = 1; diff --git a/src/utils/cache2/types.ts b/src/utils/cache2/types.ts index 601c23e..6a03494 100644 --- a/src/utils/cache2/types.ts +++ b/src/utils/cache2/types.ts @@ -146,3 +146,51 @@ export namespace DBColumnID { return [(c >>> 12) as DBTableID, (c >>> 4) & 0xff, c & 0xf]; } } + +export enum IndexType { + Animations = 0, + Skeletons = 1, + Configs = 2, + Interfaces = 3, + SoundEffects = 4, + Maps = 5, + MusicTracks = 6, + Models = 7, + Sprites = 8, + Textures = 9, + Binary = 10, + MusicJingles = 11, + ClientScripts = 12, + Fonts = 13, + MusicSamples = 14, + MusicPatches = 15, + Unused = 16, + WorldMapGeography = 17, + WorldMap = 18, + WorldMapGrounds = 19, + DbTableIndex = 21, +} + +export enum ConfigType { + Underlay = 1, + IndentityKit = 3, + Overlay = 4, + Inventory = 5, + Object = 6, + Enum = 8, + Npc = 9, + Item = 10, + Params = 11, + Sequence = 12, + SpotAnim = 13, + VarBit = 14, + VarClientString = 15, + VarPlayer = 16, + VarClient = 19, + Hitsplat = 32, + Healthbar = 33, + Struct = 34, + Area = 35, + DbRow = 38, + DbTable = 39, +} diff --git a/src/utils/colors.ts b/src/utils/colors.ts new file mode 100644 index 0000000..c8e0141 --- /dev/null +++ b/src/utils/colors.ts @@ -0,0 +1,21 @@ +export const jagexHSLtoHex = (value: number) => { + const hue = (value >> 10) & 0x3f; + const sat = (value >> 7) & 0x07; + const lum = value & 0x7f; + + console.log(`H: ${hue}, S: ${sat}, L: ${lum}`); + return hslToHex(hue, sat, lum); +}; + +export const hslToHex = (h: number, s: number, l: number) => { + l /= 100; + const a = (s * Math.min(l, 1 - l)) / 100; + const f = (n: number) => { + const k = (n + h / 30) % 12; + const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); + return Math.round(255 * color) + .toString(16) + .padStart(2, "0"); // convert to Hex and prefix "0" if needed + }; + return `#${f(0)}${f(8)}${f(4)}`; +}; diff --git a/src/utils/string.ts b/src/utils/string.ts new file mode 100644 index 0000000..770eeea --- /dev/null +++ b/src/utils/string.ts @@ -0,0 +1,2 @@ +export const capitalize = (word: string) => + word.charAt(0).toUpperCase() + word.slice(1); diff --git a/yarn.lock b/yarn.lock index b493e24..b8e0619 100644 --- a/yarn.lock +++ b/yarn.lock @@ -921,10 +921,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@osrs-wiki/mediawiki-builder@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@osrs-wiki/mediawiki-builder/-/mediawiki-builder-1.1.2.tgz#3e5e0db94b3a4d3b9fcf3e6f1ee6fbec4a110a79" - integrity sha512-L1zEApxhcYUwVPOpil8QsputnNDaa0YSuO+MFLdUtvi1XA9++elLFqtuHN5sOjI49uAJr58hvYCxurBrw0JeIw== +"@osrs-wiki/mediawiki-builder@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@osrs-wiki/mediawiki-builder/-/mediawiki-builder-1.2.0.tgz#962ccff334668e402c1711ea6e8762dce1716bb3" + integrity sha512-X5+8wBiMBzsbp9j8a3oSa7SzriCBbwGFp12YF14VOEJuIXoL4GVoSDa8Sra8kYQJJ9JIhZUMLOcZRhuPh5DyfA== dependencies: tslib "^2.6.2" @@ -4974,10 +4974,10 @@ typescript-transform-paths@^3.4.6: dependencies: minimatch "^3.0.4" -typescript@^4.6.3: - version "4.6.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c" - integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw== +typescript@^5.3.2: + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== unbox-primitive@^1.0.1: version "1.0.1"