Initial Commit
This commit is contained in:
commit
0d8ef6a7ac
10 changed files with 436 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
use flake
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
*.lockb binary diff=lockb
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.direnv
|
||||||
|
node_modules
|
||||||
|
data.json
|
||||||
|
out.json
|
||||||
|
result
|
BIN
bun.lockb
Executable file
BIN
bun.lockb
Executable file
Binary file not shown.
25
data.json.sample
Normal file
25
data.json.sample
Normal 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
48
flake.lock
Normal 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
103
flake.nix
Normal 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
223
main.ts
Normal 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
22
package.json
Normal 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
8
treefmt.nix
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
projectRootFile = "flake.nix";
|
||||||
|
programs = {
|
||||||
|
nixfmt.enable = true;
|
||||||
|
deno.enable = true;
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue