commit 0d8ef6a7ac82b951a5a58484a6fb5005bc995181 Author: Tobias Berger Date: Sat Dec 28 17:37:22 2024 +0100 Initial Commit diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..81c05ed --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.lockb binary diff=lockb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2ea23d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.direnv +node_modules +data.json +out.json +result diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..1ad6968 Binary files /dev/null and b/bun.lockb differ diff --git a/data.json.sample b/data.json.sample new file mode 100644 index 0000000..5858c59 --- /dev/null +++ b/data.json.sample @@ -0,0 +1,25 @@ +{ + "manager.crowdSourcedData.collectedData": { + "data": { + "version210Beta2": { + "professionNodeLocations": [ + { + "sourceMaterial": { + "name": "Acacia", + "level": 30 + }, + "materialType": "log", + "professionType": "woodcutting", + "label": "§6Acacia\n§a✔§f Ⓒ§7 Woodcutting Lv. Min: §f30\n\n§8Left-Click for Wood\nRight-Click for Paper", + "name": "Acacia Node", + "location": { + "x": 567, + "y": 71, + "z": -1761 + } + } + ] + } + } + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..84888f3 --- /dev/null +++ b/flake.lock @@ -0,0 +1,48 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1735291276, + "narHash": "sha256-NYVcA06+blsLG6wpAbSPTCyLvxD/92Hy4vlY9WxFI1M=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "634fd46801442d760e09493a794c4f15db2d0cbb", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "treefmt-nix": "treefmt-nix" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1735135567, + "narHash": "sha256-8T3K5amndEavxnludPyfj3Z1IkcFdRpR23q+T0BVeZE=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "9e09d30a644c57257715902efbb3adc56c79cf28", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..24a991c --- /dev/null +++ b/flake.nix @@ -0,0 +1,103 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + treefmt-nix = { + url = "github:numtide/treefmt-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + treefmt-nix, + }: + let + # System types to support. + supportedSystems = [ + "x86_64-linux" + "x86_64-darwin" + "aarch64-linux" + "aarch64-darwin" + ]; + + forEachSupportedSystem = + f: + nixpkgs.lib.genAttrs supportedSystems ( + system: + f ({ + pkgs = import nixpkgs { + inherit system; + overlays = [ self.overlays.default ]; + }; + }) + ); + in + { + formatter = forEachSupportedSystem ( + { pkgs, ... }: (treefmt-nix.lib.evalModule pkgs ./treefmt.nix).config.build.wrapper + ); + devShells = forEachSupportedSystem ( + { pkgs, ... }: + { + default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + bun + typescript-language-server + ]; + }; + } + ); + packages = forEachSupportedSystem ( + { pkgs, ... }: + { + default = pkgs.wynntils-waypoints; + } + ); + overlays.default = final: prev: { + wynntils-waypoints = + with final; + stdenv.mkDerivation ( + let + packageJSON = builtins.fromJSON (builtins.readFile ./package.json); + in + { + pname = "wynntils-waypoints"; + inherit (packageJSON) version; + + meta = { + inherit (packageJSON) description; + platforms = supportedSystems; + license = lib.licensesSpdx.${packageJSON.license}; + maintainers = [ + packageJSON.author + ]; + }; + + nativeBuildInputs = [ + bun + ]; + + src = ./.; + + buildPhase = '' + bun build ./main.ts --minify --sourcemap --bytecode --compile --outfile wynntils-waypoints + ''; + dontStrip = true; + installPhase = '' + mkdir -p $out/bin/ + install ./wynntils-waypoints $out/bin/ + ''; + } + ); + }; + + nixosModules.wynntils-waypoints = + { pkgs, ... }: + { + nixpkgs.overlays = [ self.overlay ]; + }; + }; +} diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..665c4fa --- /dev/null +++ b/main.ts @@ -0,0 +1,223 @@ +type MapLocation = { + x: number; + y: number; + z: number; + clusterId?: number; +}; + +type NPCLocation = { + icon: string | null; + description: string | null; + label: string; + name: string; + location: MapLocation; +}; + +type ProfessionNodeLocation = + & { + sourceMaterial: { + name: string; + level: number; + }; + label: string; + name: string; + location: MapLocation; + } + & ({ + materialType: "ore"; + professionType: "mining"; + } | { + materialType: "crop"; + professionType: "farming"; + } | { + materialType: "log"; + professionType: "woodcutting"; + } | { + materialType: "fish"; + professionType: "fishing"; + }); + +type ProfessionCraftingStationLocation = { + name: string; + label: string; + professionType: + | "alchemism" + | "armouring" + | "cooking" + | "jeweling" + | "scribing" + | "tailoring" + | "weaponsmithing" + | "woodworking"; + location: MapLocation; +}; + +function getDistance(locationA: MapLocation, locationB: MapLocation): number { + return Math.sqrt( + (locationB.x - locationA.x) ** 2 + (locationB.y - locationA.y) ** 2 + + (locationB.z - locationA.z) ** 2, + ); +} +function middle(...locations: MapLocation[]): MapLocation { + const sum = locations.reduce((prev, cur) => ({ + x: prev.x + cur.x, + y: prev.y + cur.y, + z: prev.z + cur.z, + })); + return { + x: Math.round(sum.x / locations.length), + y: Math.round(sum.y / locations.length), + z: Math.round(sum.z / locations.length), + }; +} + +function groupLocationItems( + items: T[], + itemsAreSimilar: (valA: T, valB: T) => boolean, + getClusterName: (cluster: T[]) => string, + getClusterIcon: (cluster: T[]) => string, +) { + let clusterId = 0; + for (let rootIdx = 0; rootIdx < items.length; rootIdx++) { + const clusterRoot = items[rootIdx]; + if (clusterRoot.location.clusterId === undefined) { + clusterRoot.location.clusterId = clusterId++; + } + + for (let idx = rootIdx + 1; idx < items.length; idx++) { + if ( + itemsAreSimilar(clusterRoot, items[idx]) + ) { + let originalClusterId = items[idx].location.clusterId; + if ( + originalClusterId !== undefined && + originalClusterId !== clusterRoot.location.clusterId + ) { + for ( + const itemWithOriginalClusterId of items.filter((item) => + item.location.clusterId === originalClusterId + ) + ) { + itemWithOriginalClusterId.location.clusterId = + clusterRoot.location.clusterId; + } + } else { + items[idx].location.clusterId = clusterRoot.location.clusterId; + } + } + } + } + + const clusteredItems: Record = {}; + for (const item of items) { + const groupByClusterid = item.location.clusterId; + if (groupByClusterid === undefined) { + throw new Error("clusterId should not be undefined here"); + } + + if (clusteredItems[groupByClusterid] === undefined) { + clusteredItems[groupByClusterid] = []; + } + + clusteredItems[groupByClusterid].push(item); + } + const clusterPoints: Record< + number, + { + name: string; + icon: string; + color: "#ffffffff"; + visibility: "default"; + location: MapLocation; + } + > = {}; + for (const [clusterId, cluster] of Object.entries(clusteredItems)) { + clusterPoints[clusterId] = { + name: getClusterName(cluster), + color: "#ffffffff", + icon: getClusterIcon(cluster), + visibility: "default", + location: middle( + ...cluster.map((item) => item.location), + ), + }; + } + return clusterPoints; +} + +function professionNodeLocationsAreSimilar( + nodeA: ProfessionNodeLocation, + nodeB: ProfessionNodeLocation, +): boolean { + if ( + nodeA.materialType !== nodeB.materialType || + nodeA.sourceMaterial.name !== nodeB.sourceMaterial.name + ) return false; + + const MAX_DISTANCE = nodeA.professionType === "woodcutting" ? 64 : 16; + return getDistance(nodeA.location, nodeB.location) <= MAX_DISTANCE; +} +function professionNodeLocationsClusterName( + cluster: ProfessionNodeLocation[], +): string { + if ( + cluster.reduce( + (prev, cur) => prev === cur.name ? prev : undefined, + cluster[0].name as string | undefined, + ) === undefined + ) { + console.error( + "Can't find label", + cluster, + ); + } + return cluster[0].name.replace("Node", "") + "x" + cluster.length; +} +function professionNodeLocationsClusterIcon( + cluster: ProfessionNodeLocation[], +): string { + if ( + cluster.reduce( + (prev, cur) => prev === cur.professionType ? prev : undefined, + cluster[0].professionType as string | undefined, + ) === undefined + ) { + console.error( + "Can't find icon", + cluster, + ); + } + return cluster[0].professionType; +} + +async function main() { + const dataPath = Bun.argv[2]; + if (dataPath === undefined) { + console.error("Data file path not specified."); + console.log( + "Usage: " + Bun.file(Bun.argv[1]).name + + " /.../.minecraft/wynntils/storage/{uuid}.data.json", + ); + return; + } + console.debug(Bun.argv[2]); + const data = JSON.parse(await (Bun.file(dataPath)).text()); + const { + professionNodeLocations, + }: { + professionNodeLocations: ProfessionNodeLocation[]; + } = data["manager.crowdSourcedData.collectedData"].data.version210Beta2; + + const clusteredProfessionNodeLocations = groupLocationItems( + professionNodeLocations, + professionNodeLocationsAreSimilar, + professionNodeLocationsClusterName, + professionNodeLocationsClusterIcon, + ); + Bun.write( + "out.json", + JSON.stringify(Object.values(clusteredProfessionNodeLocations), null, 2), + ); +} + +main(); diff --git a/package.json b/package.json new file mode 100644 index 0000000..30cbe12 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "wynntils-waypoints", + "version": "1.0.0", + "description": "Turn locally gathered crowdsource data into waypoints for Wynntils", + "main": "main.ts", + "scripts": {}, + "repository": { + "type": "git", + "url": "forgejo@git.tobot.dev:toby/wynntils-waypoints" + }, + "author": { + "github": "Toby222", + "githubId": 14962962, + "name": "Toby", + "email": "wynntils@tobot.dev", + "url": "https://tobot.dev/" + }, + "license": "CC0-1.0", + "devDependencies": { + "@types/bun": "^1.1.14" + } +} diff --git a/treefmt.nix b/treefmt.nix new file mode 100644 index 0000000..15e8bd2 --- /dev/null +++ b/treefmt.nix @@ -0,0 +1,8 @@ +{ ... }: +{ + projectRootFile = "flake.nix"; + programs = { + nixfmt.enable = true; + deno.enable = true; + }; +}