Initial Commit

This commit is contained in:
Tobias Berger 2024-12-28 17:37:22 +01:00
commit 0d8ef6a7ac
Signed by: toby
GPG key ID: 2D05EFAB764D6A88
10 changed files with 436 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
*.lockb binary diff=lockb

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.direnv
node_modules
data.json
out.json
result

BIN
bun.lockb Executable file

Binary file not shown.

25
data.json.sample Normal file
View file

@ -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
}
}
]
}
}
}
}

48
flake.lock Normal file
View file

@ -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
}

103
flake.nix Normal file
View file

@ -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 ];
};
};
}

223
main.ts Normal file
View file

@ -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<T extends { location: MapLocation }>(
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<number, T[]> = {};
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();

22
package.json Normal file
View file

@ -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"
}
}

8
treefmt.nix Normal file
View file

@ -0,0 +1,8 @@
{ ... }:
{
projectRootFile = "flake.nix";
programs = {
nixfmt.enable = true;
deno.enable = true;
};
}