Compare commits

..

4 Commits

Author SHA1 Message Date
Dusk 298e34bc04 Update server/src/addons-sources.ts 2026-06-17 17:23:30 +00:00
Dusk a11062a575 Update server/src/addons-sources.ts 2026-06-16 12:56:36 +00:00
Dusk 8d4e447ee7 Update src/common/mods.ts 2026-06-16 12:40:25 +00:00
Dusk a9b8cc5ba5 Update server/src/addons-sources.ts 2026-06-16 12:32:55 +00:00
8 changed files with 617 additions and 1592 deletions
-23
View File
@@ -1,23 +0,0 @@
FROM node:22-alpine AS build
WORKDIR /app
COPY server/package.json server/package-lock.json* ./
RUN npm install --omit=dev --no-audit --no-fund
COPY server/src/ ./src/
FROM node:22-alpine AS runtime
RUN adduser -D -H -s /sbin/nologin manifest
WORKDIR /app
COPY --from=build --chown=manifest:manifest /app /app
RUN npm install --no-save --no-audit --no-fund tsx@^4.21.0
USER manifest
EXPOSE 5000
ENV SOURCE_DIR=/srv/source
ENTRYPOINT ["node", "--import", "tsx/esm", "src/index.ts"]
-545
View File
@@ -14,9 +14,6 @@
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.2.2" "typescript": "^5.2.2"
},
"devDependencies": {
"tsx": "^4.21.0"
} }
}, },
"node_modules/@cspotcode/source-map-support": { "node_modules/@cspotcode/source-map-support": {
@@ -30,448 +27,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@jridgewell/resolve-uri": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
@@ -781,48 +336,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/esbuild": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.7",
"@esbuild/android-arm": "0.27.7",
"@esbuild/android-arm64": "0.27.7",
"@esbuild/android-x64": "0.27.7",
"@esbuild/darwin-arm64": "0.27.7",
"@esbuild/darwin-x64": "0.27.7",
"@esbuild/freebsd-arm64": "0.27.7",
"@esbuild/freebsd-x64": "0.27.7",
"@esbuild/linux-arm": "0.27.7",
"@esbuild/linux-arm64": "0.27.7",
"@esbuild/linux-ia32": "0.27.7",
"@esbuild/linux-loong64": "0.27.7",
"@esbuild/linux-mips64el": "0.27.7",
"@esbuild/linux-ppc64": "0.27.7",
"@esbuild/linux-riscv64": "0.27.7",
"@esbuild/linux-s390x": "0.27.7",
"@esbuild/linux-x64": "0.27.7",
"@esbuild/netbsd-arm64": "0.27.7",
"@esbuild/netbsd-x64": "0.27.7",
"@esbuild/openbsd-arm64": "0.27.7",
"@esbuild/openbsd-x64": "0.27.7",
"@esbuild/openharmony-arm64": "0.27.7",
"@esbuild/sunos-x64": "0.27.7",
"@esbuild/win32-arm64": "0.27.7",
"@esbuild/win32-ia32": "0.27.7",
"@esbuild/win32-x64": "0.27.7"
}
},
"node_modules/escape-html": { "node_modules/escape-html": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -923,21 +436,6 @@
"node": ">=14.14" "node": ">=14.14"
} }
}, },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -960,19 +458,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/get-tsconfig": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
"integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -1232,16 +717,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -1398,26 +873,6 @@
} }
} }
}, },
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/type-is": { "node_modules/type-is": {
"version": "1.6.18", "version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+3 -5
View File
@@ -4,11 +4,12 @@
"main": "index.ts", "main": "index.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "tsx src/index.ts" "dev": "node --loader ts-node/esm src/index.ts"
}, },
"dependencies": { "dependencies": {
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/node": "^20.9.0", "@types/node": "^20.9.0",
"dotenv": "^16.0.0",
"express": "^4.18.2", "express": "^4.18.2",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
@@ -17,8 +18,5 @@
"eslintConfig": { "eslintConfig": {
"extends": "@haaxor1689/eslint-config" "extends": "@haaxor1689/eslint-config"
}, },
"prettier": "@haaxor1689/prettier-config", "prettier": "@haaxor1689/prettier-config"
"devDependencies": {
"tsx": "^4.21.0"
}
} }
+192 -266
View File
@@ -1,266 +1,192 @@
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import { defaultSources, type AddonSource } from './addons-sources.js'; import { defaultSources, type AddonSource } from './addons-sources.js';
const CACHE_TTL_MS = 60 * 60 * 1000; const CACHE_TTL_MS = 60 * 60 * 1000;
const FETCH_CONCURRENCY = 8; const FETCH_CONCURRENCY = 8;
const FETCH_TIMEOUT_MS = 10_000; const FETCH_TIMEOUT_MS = 10_000;
const SOURCES_OVERRIDE_PATH = process.env.ADDONS_SOURCES_PATH ?? ''; const SOURCES_OVERRIDE_PATH = process.env.ADDONS_SOURCES_PATH ?? '';
export type TocData = Record<string, string>; export type TocData = Record<string, string>;
export type ResolvedAddon = { export type ResolvedAddon = {
name: string; name: string;
owner: string; owner: string;
git: string; git: string;
branch?: string; branch?: string;
ref?: string; ref?: string;
toc?: TocData; toc?: TocData;
description?: string; description?: string;
lastUpdated?: string; lastUpdated?: string;
stars?: number; stars?: number;
}; };
type CacheEntry = { at: number; data: ResolvedAddon[] }; type CacheEntry = { at: number; data: ResolvedAddon[] };
let cache: CacheEntry | undefined; let cache: CacheEntry | undefined;
let inFlight: Promise<ResolvedAddon[]> | undefined; let inFlight: Promise<ResolvedAddon[]> | undefined;
const normalizeColorCodes = (s: string): string => const parseToc = (content: string): TocData =>
s.replace(/\|C(?=[0-9a-fA-F]{8})/g, '|c').replace(/\|R/g, '|r'); content
.split('\n')
const parseToc = (content: string): TocData => .filter(l => l.startsWith('## '))
content .map(l => l.slice(3))
.split('\n') .map(l => {
.filter(l => l.startsWith('## ')) const idx = l.indexOf(':');
.map(l => l.slice(3)) if (idx === -1) return null;
.map(l => { return [l.slice(0, idx).trim(), l.slice(idx + 1).trim()] as const;
const idx = l.indexOf(':'); })
if (idx === -1) return null; .filter((e): e is readonly [string, string] => !!e)
return [l.slice(0, idx).trim(), l.slice(idx + 1).trim()] as const; .reduce<TocData>((acc, [k, v]) => {
}) acc[k] = v;
.filter((e): e is readonly [string, string] => !!e) return acc;
.reduce<TocData>((acc, [k, v]) => { }, {});
acc[k] = normalizeColorCodes(v);
return acc; const fetchWithTimeout = async (url: string, init?: RequestInit) => {
}, {}); const controller = new AbortController();
const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
const fetchWithTimeout = async (url: string, init?: RequestInit) => { try {
const controller = new AbortController(); return await fetch(url, { ...init, signal: controller.signal });
const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); } finally {
try { clearTimeout(t);
return await fetch(url, { ...init, signal: controller.signal }); }
} finally { };
clearTimeout(t);
} const parseGitUrl = (git: string) => {
}; // https://github.com/{owner}/{repo}.git
const m = git.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
type RepoMeta = { if (!m || !m[1] || !m[2]) throw Error(`Unsupported git URL: ${git}`);
description?: string; return { owner: m[1], repo: m[2] };
defaultBranch?: string; };
lastUpdated?: string;
stars?: number; const resolveOne = async (src: AddonSource): Promise<ResolvedAddon | null> => {
}; try {
const { owner, repo } = parseGitUrl(src.git);
type RawMeta = { const name = src.name ?? repo;
description?: string | null; const branch = src.branch ?? 'master';
default_branch?: string; const tocRef = src.ref ?? branch;
pushed_at?: string | null;
updated_at?: string | null; const tocUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${tocRef}/${name}.toc`;
stargazers_count?: number | null; const apiUrl = `https://api.github.com/repos/${owner}/${repo}`;
stars_count?: number | null;
}; const [tocRes, apiRes] = await Promise.all([
fetchWithTimeout(tocUrl).catch(() => null),
type Provider = { fetchWithTimeout(apiUrl, {
apiUrl: (owner: string, repo: string) => string; headers: {
apiHeaders: () => Record<string, string>; Accept: 'application/vnd.github+json',
mapMeta: (json: RawMeta) => RepoMeta; ...(process.env.GITHUB_TOKEN && {
tocUrl: (owner: string, repo: string, ref: string, name: string) => string; Authorization: `Bearer ${process.env.GITHUB_TOKEN}`
}; })
}
const githubProvider: Provider = { }).catch(() => null)
apiUrl: (o, r) => `https://api.github.com/repos/${o}/${r}`, ]);
apiHeaders: () => ({
Accept: 'application/vnd.github+json', let toc: TocData | undefined;
...(process.env.GITHUB_TOKEN && { if (tocRes?.ok) {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}` const parsed = parseToc(await tocRes.text());
}) const required = ['Interface', 'Title', 'Author', 'Notes', 'Version'];
}), if (required.every(k => typeof parsed[k] === 'string')) {
mapMeta: j => ({ toc = parsed;
description: j.description ?? undefined, }
defaultBranch: j.default_branch, }
lastUpdated: j.pushed_at ?? undefined,
stars: j.stargazers_count ?? undefined let description: string | undefined;
}), let lastUpdated: string | undefined;
tocUrl: (o, r, ref, name) => let stars: number | undefined;
`https://raw.githubusercontent.com/${o}/${r}/${ref}/${name}.toc` if (apiRes?.ok) {
}; const meta = (await apiRes.json()) as {
description?: string;
const GITEA_API = 'https://octowow.st/git/api/v1'; pushed_at?: string;
const giteaProvider: Provider = { stargazers_count?: number;
apiUrl: (o, r) => `${GITEA_API}/repos/${o}/${r}`, };
apiHeaders: () => ({ Accept: 'application/json' }), description = meta.description ?? undefined;
mapMeta: j => ({ lastUpdated = meta.pushed_at;
description: j.description ?? undefined, stars = meta.stargazers_count;
defaultBranch: j.default_branch, }
lastUpdated: j.updated_at ?? undefined,
stars: j.stars_count ?? undefined if (src.description) {
}), description = src.description;
tocUrl: (o, r, ref, name) => if (toc) toc = { ...toc, Notes: src.description };
`${GITEA_API}/repos/${o}/${r}/raw/${name}.toc?ref=${encodeURIComponent(ref)}` }
};
const result: ResolvedAddon = { name, owner, git: src.git };
const parseGitUrl = ( if (src.branch !== undefined) result.branch = src.branch;
git: string if (src.ref !== undefined) result.ref = src.ref;
): { owner: string; repo: string; provider: Provider } => { if (toc !== undefined) result.toc = toc;
const gh = git.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/); if (description !== undefined) result.description = description;
if (gh && gh[1] && gh[2]) { if (lastUpdated !== undefined) result.lastUpdated = lastUpdated;
return { owner: gh[1], repo: gh[2], provider: githubProvider }; if (stars !== undefined) result.stars = stars;
} return result;
const gitea = git.match(/octowow\.st\/git\/([^/]+)\/([^/]+?)(?:\.git)?$/); } catch (e) {
if (gitea && gitea[1] && gitea[2]) { console.error(`Failed to resolve ${src.git}:`, e);
return { owner: gitea[1], repo: gitea[2], provider: giteaProvider }; return null;
} }
throw Error(`Unsupported git URL: ${git}`); };
};
const poolMap = async <T, R>(
const REQUIRED_TOC_KEYS = ['Interface']; items: T[],
concurrency: number,
const tryFetchToc = async ( fn: (item: T) => Promise<R>
provider: Provider, ): Promise<R[]> => {
owner: string, const results: R[] = new Array(items.length);
repo: string, let idx = 0;
name: string, const worker = async () => {
ref: string while (true) {
): Promise<TocData | undefined> => { const i = idx++;
const res = await fetchWithTimeout( if (i >= items.length) return;
provider.tocUrl(owner, repo, ref, name) const item = items[i];
).catch(() => null); if (item === undefined) return;
if (!res?.ok) return undefined; results[i] = await fn(item);
const parsed = parseToc(await res.text()); }
return REQUIRED_TOC_KEYS.every(k => typeof parsed[k] === 'string') };
? parsed await Promise.all(Array.from({ length: concurrency }, worker));
: undefined; return results;
}; };
const resolveOne = async (src: AddonSource): Promise<ResolvedAddon | null> => { const loadSources = async (): Promise<AddonSource[]> => {
try { if (!SOURCES_OVERRIDE_PATH) return defaultSources;
const { owner, repo, provider } = parseGitUrl(src.git); try {
const name = src.name ?? repo; if (await fs.pathExists(SOURCES_OVERRIDE_PATH)) {
const override = (await fs.readJSON(SOURCES_OVERRIDE_PATH)) as AddonSource[];
const apiRes = await fetchWithTimeout(provider.apiUrl(owner, repo), { if (Array.isArray(override) && override.length > 0) {
headers: provider.apiHeaders() console.log(`Using addon sources override from ${SOURCES_OVERRIDE_PATH}`);
}).catch(() => null); return override;
}
let meta: RepoMeta | undefined; }
if (apiRes?.ok) meta = provider.mapMeta((await apiRes.json()) as RawMeta); } catch (e) {
console.error(`Failed to read override at ${SOURCES_OVERRIDE_PATH}, using defaults:`, e);
const candidates = src.ref }
? [src.ref] return defaultSources;
: src.branch };
? [src.branch]
: [...new Set([meta?.defaultBranch, 'master', 'main'].filter((b): b is string => !!b))]; const buildList = async (): Promise<ResolvedAddon[]> => {
const sources = await loadSources();
let toc: TocData | undefined; console.log(`Resolving metadata for ${sources.length} addons (concurrency=${FETCH_CONCURRENCY})...`);
let resolvedRef: string | undefined; const t0 = Date.now();
for (const ref of candidates) { const results = await poolMap(sources, FETCH_CONCURRENCY, resolveOne);
toc = await tryFetchToc(provider, owner, repo, name, ref); const ok = results.filter((r): r is ResolvedAddon => r !== null);
if (toc) { ok.sort((a, b) => a.name.localeCompare(b.name));
resolvedRef = ref; console.log(`Resolved ${ok.length}/${sources.length} addons in ${Date.now() - t0}ms`);
break; return ok;
} };
}
export const getAddons = async (force = false): Promise<ResolvedAddon[]> => {
const effectiveBranch = src.ref if (!force && cache && Date.now() - cache.at < CACHE_TTL_MS) {
? undefined return cache.data;
: src.branch ?? resolvedRef ?? meta?.defaultBranch; }
// Deduplicate concurrent callers — only one scrape in flight at a time.
let description = meta?.description ?? undefined; if (inFlight) return inFlight;
const lastUpdated = meta?.lastUpdated; inFlight = buildList()
const stars = meta?.stars; .then(data => {
cache = { at: Date.now(), data };
if (src.description) { return data;
description = src.description; })
if (toc) toc = { ...toc, Notes: src.description }; .finally(() => {
} inFlight = undefined;
});
const result: ResolvedAddon = { name, owner, git: src.git }; return inFlight;
if (effectiveBranch !== undefined) result.branch = effectiveBranch; };
if (src.ref !== undefined) result.ref = src.ref;
if (toc !== undefined) result.toc = toc; export const warmUp = () => {
if (description !== undefined) result.description = description; getAddons().catch(e => console.error('Addon resolver warm-up failed:', e));
if (lastUpdated !== undefined) result.lastUpdated = lastUpdated; };
if (stars !== undefined) result.stars = stars;
return result;
} catch (e) {
console.error(`Failed to resolve ${src.git}:`, e);
return null;
}
};
const poolMap = async <T, R>(
items: T[],
concurrency: number,
fn: (item: T) => Promise<R>
): Promise<R[]> => {
const results: R[] = new Array(items.length);
let idx = 0;
const worker = async () => {
while (true) {
const i = idx++;
if (i >= items.length) return;
const item = items[i];
if (item === undefined) return;
results[i] = await fn(item);
}
};
await Promise.all(Array.from({ length: concurrency }, worker));
return results;
};
const loadSources = async (): Promise<AddonSource[]> => {
if (!SOURCES_OVERRIDE_PATH) return defaultSources;
try {
if (await fs.pathExists(SOURCES_OVERRIDE_PATH)) {
const override = (await fs.readJSON(SOURCES_OVERRIDE_PATH)) as AddonSource[];
if (Array.isArray(override) && override.length > 0) {
console.log(`Using addon sources override from ${SOURCES_OVERRIDE_PATH}`);
return override;
}
}
} catch (e) {
console.error(`Failed to read override at ${SOURCES_OVERRIDE_PATH}, using defaults:`, e);
}
return defaultSources;
};
const buildList = async (): Promise<ResolvedAddon[]> => {
const sources = await loadSources();
console.log(`Resolving metadata for ${sources.length} addons (concurrency=${FETCH_CONCURRENCY})...`);
const t0 = Date.now();
const results = await poolMap(sources, FETCH_CONCURRENCY, resolveOne);
const ok = results.filter((r): r is ResolvedAddon => r !== null);
ok.sort((a, b) => a.name.localeCompare(b.name));
console.log(`Resolved ${ok.length}/${sources.length} addons in ${Date.now() - t0}ms`);
return ok;
};
export const getAddons = async (force = false): Promise<ResolvedAddon[]> => {
if (!force && cache && Date.now() - cache.at < CACHE_TTL_MS) {
return cache.data;
}
if (inFlight) return inFlight;
inFlight = buildList()
.then(data => {
cache = { at: Date.now(), data };
return data;
})
.finally(() => {
inFlight = undefined;
});
return inFlight;
};
export const warmUp = () => {
getAddons().catch(e => console.error('Addon resolver warm-up failed:', e));
};
+196 -246
View File
@@ -1,246 +1,196 @@
export type AddonSource = { export type AddonSource = {
git: string; git: string;
branch?: string; branch?: string;
name?: string; name?: string;
description?: string; description?: string;
ref?: string; ref?: string;
}; };
export const defaultSources: AddonSource[] = [ export const defaultSources: AddonSource[] = [
{ { git: 'https://github.com/CosminPOP/AtlasLoot.git', name: 'AtlasLoot' },
git: 'https://github.com/Alukarho/AI_VoiceOver.git', {
description: 'Adds AI-generated voice acting to NPC quest and gossip dialogue' git: 'https://github.com/byCFM2/Atlas-TW.git',
}, branch: 'main',
{ ref: 'pre-rewrite-backup'
git: 'https://github.com/McPewPew/ArcHUD2.git', },
description: 'Combat HUD showing health and power as arcs around your character' {
}, git: 'https://github.com/OldManAlpha/aux-addon.git',
{ git: 'https://github.com/CosminPOP/AtlasLoot.git', name: 'AtlasLoot' }, name: 'aux-addon',
{ description: 'Auction House replacement with advanced filtering and search'
git: 'https://github.com/byCFM2/Atlas-TW.git', },
name: 'Atlas-CFM' { git: 'https://github.com/absir/Bagshui.git', branch: 'main' },
}, { git: 'https://github.com/pepopo978/BetterCharacterStats.git', branch: 'main' },
{ { git: 'https://github.com/pepopo978/BigWigs.git' },
git: 'https://github.com/Road-block/AuldLangSyne.git', {
description: 'Adds personal notes to friends, ignore, and guild lists, remembered while offline' git: 'https://github.com/DBFBlackbull/BitesCookBook.git',
}, description: 'Tracks which items are used in cooking and what they create'
{ },
git: 'https://github.com/FSuhas/AutoLFM.git', { git: 'https://github.com/cutiepoka/CleveRoidMacros.git', branch: 'main' },
description: 'Automated "Looking For More" broadcaster for Turtle WoW dungeons and raids' {
}, git: 'https://github.com/Cinecom/ConsumesManager.git',
{ branch: 'main',
git: 'https://github.com/OldManAlpha/aux-addon.git', description: 'Tracks consumables and food buffs across alts, bank, and mail'
name: 'aux-addon', },
description: 'Auction House replacement with advanced filtering and search' {
}, git: 'https://github.com/Kirchlive/cursive-raid.git',
{ git: 'https://github.com/absir/Bagshui.git' }, name: 'Cursive-Raid',
{ git: 'https://github.com/pepopo978/BetterCharacterStats.git', branch: 'main' }, description: 'Raid debuff tracker with profiles and multi-curse assist (SuperWoW)'
{ git: 'https://github.com/pepopo978/BigWigs.git' }, },
{ { git: 'https://github.com/pepopo978/DoiteAuras.git', branch: 'main' },
git: 'https://github.com/DBFBlackbull/BitesCookBook.git', { git: 'https://github.com/Stormhand-dev/DragonflightUI-Reforged.git', branch: 'main' },
description: 'Tracks which items are used in cooking and what they create' {
}, git: 'https://github.com/Fiurs-Hearth/ExtraResourceBars.git',
{ description: 'Adds extra resource bars (mana, energy, rage) to the UI'
git: 'https://github.com/brotalnia/BlizzPlates.git', },
description: 'Adds castbars, debuffs, and class icons to the default Blizzard nameplates' { git: 'https://github.com/tilare/FlightTracker.git', branch: 'main' },
}, { git: 'https://github.com/lookino/Flyout.git', branch: 'main' },
{ {
git: 'https://github.com/MDGitHubRepo/CallOfElements.git', git: 'https://github.com/trumpetx/GetHead.git',
description: 'All-in-one Shaman totem bar and totem/healing manager' description: 'Recovers Onyxia and Nefarian heads from disenchant grief'
}, },
{ git: 'https://github.com/cutiepoka/CleveRoidMacros.git' }, {
{ git: 'https://github.com/zanthor/GNS.git',
git: 'https://github.com/Cinecom/ConsumesManager.git', branch: 'main',
description: 'Tracks consumables and food buffs across alts, bank, and mail' description: 'Custom naming for Goblin Brainwashing Device specializations'
}, },
{ { git: 'https://github.com/vatichild/guda.git', name: 'Guda', branch: 'main' },
git: 'https://github.com/Kirchlive/cursive-raid.git', { git: 'https://github.com/vatichild/GudaPlates.git', branch: 'main' },
name: 'Cursive-Raid', { git: 'https://github.com/andresuarezschou/HCDeaths.git', branch: 'main' },
description: 'Raid debuff tracker with profiles and multi-curse assist (SuperWoW)' {
}, git: 'https://github.com/Arthur-Helias/InstanceJournal.git',
{ description: "Encounter Journal reimagined for Turtle WoW"
git: 'https://github.com/Zerf/Decursive.git', },
description: 'Raid/party debuff-cleaning helper that dispels whoever needs it' {
}, git: 'https://github.com/Einherjarn/ItemRack.git',
{ description: 'Item set manager with quick-swap menus for inventory'
git: 'https://github.com/DeterminedPanda/DifficultBulletinBoard.git', },
description: 'Organizes LFG, profession, and hardcore chat announcements into a bulletin board' {
}, git: 'https://github.com/Otari98/_LazyPig.git',
{ git: 'https://github.com/pepopo978/DoiteAuras.git' }, name: '_LazyPig',
{ git: 'https://github.com/Stormhand-dev/DragonflightUI-Reforged.git' }, description: 'Auto-dismount, auto-accept, auto-roll, and chat spam filter. /lp to configure'
{ },
git: 'https://github.com/Fiurs-Hearth/ExtraResourceBars.git', { git: 'https://github.com/Dusk-92/LevelRange-Octo.git', branch: 'main' },
description: 'Adds extra resource bars (mana, energy, rage) to the UI' { git: 'https://github.com/tilare/MessageBox.git', branch: 'main' },
}, {
{ git: 'https://github.com/MarcelineVQ/ModifiedPowerAuras.git',
git: 'https://github.com/SeVeN7000/FishingBuddy.git', description: "Advanced version of Sinesther's Power Auras"
description: 'Auto-equips fishing gear and tracks catches, fish, and zone info' },
}, {
{ git: 'https://github.com/tilare/FlightTracker.git' }, git: 'https://github.com/tilare/ModernMapMarkers.git',
{ git: 'https://github.com/lookino/Flyout.git' }, branch: 'main',
{ description: 'Shows dungeons, raids, world bosses, and travel routes on the world map'
git: 'https://github.com/trumpetx/GetHead.git', },
description: 'Recovers Onyxia and Nefarian heads from disenchant grief' {
}, git: 'https://github.com/vegeta1k95/ModernSpellBook.git',
{ description: 'Retail-style spellbook UI for vanilla'
git: 'https://github.com/zanthor/GNS.git', },
description: 'Custom naming for Goblin Brainwashing Device specializations' { git: 'https://github.com/tilare/MovementTracker.git', branch: 'main' },
}, {
{ git: 'https://github.com/vatichild/guda.git', name: 'Guda' }, git: 'https://github.com/Emyrk/NampowerSettings.git',
{ git: 'https://github.com/vatichild/GudaPlates.git' }, description: 'Settings panel for the Nampower spellqueue addon'
{ git: 'https://github.com/andresuarezschou/HCDeaths.git' }, },
{ {
git: 'https://github.com/Arthur-Helias/InstanceJournal.git', git: 'https://github.com/BlackHobbiT/necrosis-twow.git',
description: "Encounter Journal reimagined for Turtle WoW" branch: 'main',
}, description: 'Warlock helper: pets, soul shards, summoning, demon timers'
{ },
git: 'https://github.com/Einherjarn/ItemRack.git', {
description: 'Item set manager with quick-swap menus for inventory' git: 'https://github.com/zanthor/OG-RaidHelper.git',
}, branch: 'main',
{ description: 'Raid management: roles, trade distribution, soft-reserve validation'
git: 'https://github.com/Otari98/_LazyPig.git', },
name: '_LazyPig', {
description: 'Auto-dismount, auto-accept, auto-roll, and chat spam filter. /lp to configure' git: 'https://github.com/ShikawaLePaladin/PallyPowerTW.git',
}, description: 'Paladin buff and assignment manager for raids and parties'
{ git: 'https://github.com/Dusk-92/LevelRange-Octo.git' }, },
{ git: 'https://github.com/tilare/MessageBox.git' }, {
{ git: 'https://github.com/Cliencer/pfExtend.git',
git: 'https://github.com/MarcelineVQ/ModifiedPowerAuras.git', branch: 'main',
description: "Advanced version of Sinesther's Power Auras" description: 'pfQuest extension showing all monster drops and quest chains. /pfex'
}, },
{ { git: 'https://github.com/shagu/pfQuest.git' },
git: 'https://github.com/tilare/ModernMapMarkers.git', { git: 'https://github.com/KameleonUK/pfQuest-turtle.git' },
description: 'Shows dungeons, raids, world bosses, and travel routes on the world map' { git: 'https://github.com/shagu/pfUI.git' },
}, {
{ git: 'https://github.com/jrc13245/pfUI-addonskinner.git',
git: 'https://github.com/vegeta1k95/ModernSpellBook.git', branch: 'main',
description: 'Retail-style spellbook UI for vanilla' description: 'pfUI module that re-skins other addons to match the pfUI theme'
}, },
{ git: 'https://github.com/tilare/MovementTracker.git' }, {
{ git: 'https://github.com/Bombg/pfUI-bettertotems.git',
git: 'https://github.com/Emyrk/NampowerSettings.git', branch: 'main',
description: 'Settings panel for the Nampower spellqueue addon' description: 'pfUI module with improved Shaman totem timers'
}, },
{ {
git: 'https://github.com/BlackHobbiT/necrosis-twow.git', git: 'https://github.com/Arthur-Helias/pfUI-LocationPlus.git',
name: 'Necrosis', name: 'pfUI-locplus',
description: 'Warlock helper: pets, soul shards, summoning, demon timers' description: 'Adds a location panel and zone info to pfUI'
}, },
{ { git: 'https://github.com/acid9000/PizzaWorldBuffs.git', branch: 'main' },
git: 'https://github.com/gnwl/NotGrid.git', {
name: 'notgrid', git: 'https://github.com/npfs666/ProcDoc.git',
description: 'Grid-like compact party/raid frames with buff/debuff, aggro, and proximity tracking' branch: 'main',
}, description: 'Visual proc alerts with pulsing images so you never miss them'
{ },
git: 'https://github.com/zanthor/OG-RaidHelper.git', { git: 'https://github.com/SabineWren/Quiver.git', branch: 'main' },
description: 'Raid management: roles, trade distribution, soft-reserve validation' {
}, git: 'https://github.com/thezephyrsong/Rested.git',
{ description: 'Progress bar showing your rested XP while resting'
git: 'https://github.com/sica42/Outfitter.git', },
description: 'Equipment set manager to save and quickly swap gear outfits, with Turtle mount fixes' { git: 'https://github.com/Otari98/Rinse.git' },
}, {
{ git: 'https://github.com/anzz1/SellValue.git',
git: 'https://github.com/ShikawaLePaladin/PallyPowerTW.git', description: 'Shows item vendor sell value in tooltips when not at a vendor'
description: 'Paladin buff and assignment manager for raids and parties' },
}, { git: 'https://github.com/shagu/ShaguDPS.git' },
{ {
git: 'https://github.com/Cliencer/pfExtend.git', git: 'https://github.com/shagu/ShaguPlates.git',
description: 'pfQuest extension showing all monster drops and quest chains. /pfex' description: 'Nameplates with castbars and class colors. /splates'
}, },
{ git: 'https://github.com/shagu/pfQuest.git' }, { git: 'https://github.com/paokkerkir/ShaguTweaks.git' },
{ git: 'https://github.com/KameleonUK/pfQuest-turtle.git' }, {
{ git: 'https://github.com/shagu/pfUI.git' }, git: 'https://github.com/paokkerkir/ShaguTweaks-extras.git',
{ description: 'Extras module for ShaguTweaks (additional UI tweaks)'
git: 'https://github.com/jrc13245/pfUI-addonskinner.git', },
description: 'pfUI module that re-skins other addons to match the pfUI theme' { git: 'https://github.com/pepopo978/SimpleActionSets.git' },
}, { git: 'https://github.com/Siventt/AttackBar.git' },
{ {
git: 'https://github.com/Bombg/pfUI-bettertotems.git', git: 'https://github.com/Player-Doite/Tactica.git',
description: 'pfUI module with improved Shaman totem timers' branch: 'main',
}, description: 'Auto-build raids: invite/gearcheck, tactics, masterloot, role sync'
{ },
git: 'https://github.com/Arthur-Helias/pfUI-LocationPlus.git', {
name: 'pfUI-locplus', git: 'https://github.com/Otari98/Tmog.git',
description: 'Adds a location panel and zone info to pfUI' description: 'Transmog item browser with collection info in tooltips'
}, },
{ git: 'https://github.com/acid9000/PizzaWorldBuffs.git' }, {
{ git: 'https://github.com/whtmst/T-RestedXP.git',
git: 'https://github.com/npfs666/ProcDoc.git', branch: 'main',
description: 'Visual proc alerts with pulsing images so you never miss them' description: 'Tracks 0% and 100% rested XP thresholds'
}, },
{ git: 'https://github.com/SabineWren/Quiver.git' }, { git: 'https://github.com/sica42/TurtleCalendar.git', branch: 'main' },
{ {
git: 'https://github.com/thezephyrsong/Rested.git', git: 'https://github.com/sica42/TurtleMail.git',
description: 'Progress bar showing your rested XP while resting' description: 'Mailbox UI enhancement: bulk send, search, multi-mail'
}, },
{ git: 'https://github.com/Otari98/Rinse.git' }, { git: 'https://github.com/tempranova/turtlerp.git', name: 'TurtleRP', branch: 'main' },
{ { git: 'https://github.com/CosminPOP/TWThreat.git' },
git: 'https://github.com/anzz1/SellValue.git', {
description: 'Shows item vendor sell value in tooltips when not at a vendor' git: 'https://github.com/whtmst/UnitXP_SP3_Addon.git',
}, branch: 'main',
{ git: 'https://github.com/shagu/ShaguDPS.git' }, description: 'Settings UI for the UnitXP SuperWoW client patch'
{ },
git: 'https://github.com/shagu/ShaguPlates.git', {
description: 'Nameplates with castbars and class colors. /splates' git: 'https://github.com/tdymel/VCB.git',
}, description: 'Smart consolidated buff frames with extensive customization'
{ git: 'https://github.com/paokkerkir/ShaguTweaks.git' }, },
{ {
git: 'https://github.com/paokkerkir/ShaguTweaks-extras.git', git: 'https://github.com/Fiurs-Hearth/WIIIUI.git',
description: 'Extras module for ShaguTweaks (additional UI tweaks)' description: 'Compact custom UI replacement for Turtle WoW'
}, },
{ git: 'https://github.com/pepopo978/SimpleActionSets.git' }, { git: 'https://github.com/refaim/WIM.git' },
{ {
git: 'https://github.com/balakethelock/SuperAPI.git', git: 'https://github.com/Arthur-Helias/ZonesLevel.git',
description: 'Companion compatibility addon bridging the SuperWoW client mod\'s expanded Lua API' description: "Shows zone level range under the title on the world map"
}, }
{ ];
git: 'https://github.com/jrc13245/SuperMacro-turtle-SuperWoW.git',
name: 'SuperMacro',
description: 'Extended macros with long macros, keybind execution, item links, and a code editor'
},
{ git: 'https://github.com/Siventt/AttackBar.git' },
{
git: 'https://github.com/Player-Doite/Tactica.git',
description: 'Auto-build raids: invite/gearcheck, tactics, masterloot, role sync'
},
{
git: 'https://github.com/Otari98/Tmog.git',
description: 'Transmog item browser with collection info in tooltips'
},
{
git: 'https://github.com/whtmst/T-RestedXP.git',
description: 'Tracks 0% and 100% rested XP thresholds'
},
{ git: 'https://github.com/sica42/TurtleCalendar.git' },
{
git: 'https://github.com/sica42/TurtleMail.git',
description: 'Mailbox UI enhancement: bulk send, search, multi-mail'
},
{ git: 'https://github.com/tempranova/turtlerp.git', name: 'TurtleRP' },
{ git: 'https://github.com/CosminPOP/TWThreat.git' },
{
git: 'https://github.com/RetroCro/unitscan-turtle-hc.git',
description: 'Hardcore unitscan fork for Turtle WoW that alerts on rares, elites, and dangerous mobs'
},
{
git: 'https://github.com/whtmst/UnitXP_SP3_Addon.git',
description: 'Settings UI for the UnitXP SuperWoW client patch'
},
{
git: 'https://github.com/tdymel/VCB.git',
description: 'Smart consolidated buff frames with extensive customization'
},
{
git: 'https://github.com/acolis/VitalWatch.git',
description: 'Customizable health, mana, and aggro alert system for solo, group, and raid play'
},
{
git: 'https://github.com/Fiurs-Hearth/WIIIUI.git',
description: 'Compact custom UI replacement for Turtle WoW'
},
{ git: 'https://github.com/refaim/WIM.git' },
{
git: 'https://github.com/Arthur-Helias/ZonesLevel.git',
description: "Shows zone level range under the title on the world map"
}
];
+10 -220
View File
@@ -16,29 +16,7 @@ const allowedExtra = [
const vanillaFixes = ['VfPatcher.dll', 'd3d9.dll', 'dxvk.conf']; const vanillaFixes = ['VfPatcher.dll', 'd3d9.dll', 'dxvk.conf'];
const skipFiles = new Set([ const skipFiles = new Set(['manifest.json', 'wow-client.zip', '.gitkeep']);
'manifest.json',
'manifest.json.tmp',
'wow-client.zip',
'.gitkeep',
'.manifest-overrides.json'
]);
const skipPatterns: RegExp[] = [/\.bak(\.|$)/, /\.crashing(\.|$)/];
const isSkipPattern = (file: string) =>
skipPatterns.some(p => p.test(file));
const skipDirsPosix = new Set([
'Interface/GlueXML',
'Interface/FrameXML',
'Errors',
'Logs',
'Screenshots',
'WDB',
'WTF/Account'
]);
const isSkipDir = (...filePath: string[]) =>
skipDirsPosix.has(filePath.join('/'));
type FolderTags = 'allowExtra'; type FolderTags = 'allowExtra';
type FileTags = 'vanillaFixes'; type FileTags = 'vanillaFixes';
@@ -53,21 +31,8 @@ type FileManifest = { name: string } & (
size: number; size: number;
tags?: FileTags[]; tags?: FileTags[];
} }
| { type: 'del' }
); );
export type BuildProgress = {
state: 'idle' | 'building' | 'ready' | 'failed';
done: number;
total: number;
currentFile: string;
startedAt: number | null;
finishedAt: number | null;
error: string | null;
};
export type ProgressCallback = (p: Pick<BuildProgress, 'done' | 'total' | 'currentFile'>) => void;
const getHash = (...filePath: string[]): Promise<string> => const getHash = (...filePath: string[]): Promise<string> =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const hash = crypto.createHash('sha1'); const hash = crypto.createHash('sha1');
@@ -77,96 +42,9 @@ const getHash = (...filePath: string[]): Promise<string> =>
stream.on('end', () => resolve(hash.digest('hex').toLocaleUpperCase())); stream.on('end', () => resolve(hash.digest('hex').toLocaleUpperCase()));
}); });
const countFiles = async (clientPath: string, ...filePath: string[]): Promise<number> => { export const buildCache = async (clientPath: string) => {
let total = 0;
const dir = path.join(clientPath, ...filePath);
const files = await fs.readdir(dir);
for (const file of files.sort()) {
if (skipFiles.has(file)) continue;
if (isSkipPattern(file)) continue;
const stats = await fs.stat(path.join(dir, file));
if (stats.isDirectory()) {
if (isSkipDir(...filePath, file)) continue;
if (file.match(/patch-./)) {
const mpqPath = path.join(dir, `${file}.mpq`);
if (await fs.pathExists(mpqPath)) total += 1;
}
total += await countFiles(clientPath, ...filePath, file);
} else {
total += 1;
}
}
return total;
};
export const buildCache = async (
clientPath: string,
onProgress?: ProgressCallback
) => {
console.log('Building cache...'); console.log('Building cache...');
const prevManifestPath = path.join(clientPath, 'manifest.json');
let prevManifestMtimeMs = 0;
const prevHashByPath = new Map<string, string>();
const prevVersionByPath = new Map<string, number | undefined>();
const prevSizeByPath = new Map<string, number>();
try {
const prevStat = await fs.stat(prevManifestPath);
prevManifestMtimeMs = prevStat.mtimeMs;
const prev = await fs.readJSON(prevManifestPath);
const walk = (node: FileManifest, prefix: string[]) => {
if (node.type === 'dir' || node.type === 'mpq') {
const newPrefix = node.name ? [...prefix, node.name] : prefix;
if (node.type === 'mpq') {
const mpqKey = [...newPrefix.slice(0, -1), node.name + '.mpq']
.join('/');
prevHashByPath.set(mpqKey, node.hash);
prevSizeByPath.set(mpqKey, node.size);
}
for (const child of node.files) walk(child, newPrefix);
} else {
const key = [...prefix, node.name].join('/');
prevHashByPath.set(key, node.hash);
prevVersionByPath.set(key, node.version);
prevSizeByPath.set(key, node.size);
}
};
walk(prev.root, []);
console.log(
`mtime-skip: loaded ${prevHashByPath.size} cached hashes from ` +
`prior manifest (mtime=${new Date(prevManifestMtimeMs).toISOString()})`
);
} catch (e) {
console.log(
'mtime-skip: no usable prior manifest, full rebuild ' +
`(${(e as Error).message})`
);
}
const total = await countFiles(clientPath);
let done = 0;
let reused = 0;
const tick = (currentFile: string) => {
done += 1;
onProgress?.({ done, total, currentFile });
};
console.log(`Building cache: ${total} files to hash...`);
const getHashCached = async (
relPath: string,
mtimeMs: number,
...filePath: string[]
): Promise<string> => {
if (prevManifestMtimeMs > 0 && mtimeMs <= prevManifestMtimeMs) {
const cached = prevHashByPath.get(relPath);
if (cached) {
reused++;
return cached;
}
}
return getHash(clientPath, ...filePath);
};
const buildTree = async (...filePath: string[]): Promise<FileManifest[]> => { const buildTree = async (...filePath: string[]): Promise<FileManifest[]> => {
const files = await fs.readdir(path.join(clientPath, ...filePath)); const files = await fs.readdir(path.join(clientPath, ...filePath));
@@ -174,34 +52,21 @@ export const buildCache = async (
const tree: FileManifest[] = []; const tree: FileManifest[] = [];
for (const file of files.sort()) { for (const file of files.sort()) {
if (skipFiles.has(file)) continue; if (skipFiles.has(file)) continue;
if (isSkipPattern(file)) continue;
const stats = await fs.stat(path.join(clientPath, ...filePath, file)); const stats = await fs.stat(path.join(clientPath, ...filePath, file));
if (stats.isDirectory()) { if (stats.isDirectory()) {
if (isSkipDir(...filePath, file)) continue;
if (file.match(/patch-./)) { if (file.match(/patch-./)) {
patches.push(file); patches.push(file);
const mpqRelPath = path
.join(...filePath, `${file}.mpq`)
.split(path.sep)
.join('/');
const mpqStat = await fs.stat(
path.join(clientPath, ...filePath, `${file}.mpq`)
);
tree.push({ tree.push({
type: 'mpq', type: 'mpq',
name: file, name: file,
files: await buildTree(...filePath, file), files: await buildTree(...filePath, file),
size: mpqStat.size, size: (
hash: await getHashCached( await fs.stat(path.join(clientPath, ...filePath, `${file}.mpq`))
mpqRelPath, ).size,
mpqStat.mtimeMs, hash: await getHash(clientPath, ...filePath, `${file}.mpq`)
...filePath,
`${file}.mpq`
)
}); });
tick(mpqRelPath);
} else { } else {
const tags: FolderTags[] = []; const tags: FolderTags[] = [];
allowedExtra.includes(path.join(...filePath, file)) && allowedExtra.includes(path.join(...filePath, file)) &&
@@ -216,8 +81,8 @@ export const buildCache = async (
continue; continue;
} }
// Skip if extracted mpq patch
if (patches.find(v => file.match(v))) continue; if (patches.find(v => file.match(v))) continue;
const allowModifiedPaths = new Set([ const allowModifiedPaths = new Set([
'WTF/Config.wtf', 'WTF/Config.wtf',
'Data/fonts.MPQ', 'Data/fonts.MPQ',
@@ -237,97 +102,22 @@ export const buildCache = async (
tree.push({ tree.push({
type: 'file', type: 'file',
name: file, name: file,
hash: await getHashCached( hash: await getHash(clientPath, ...filePath, file),
fullPath,
stats.mtimeMs,
...filePath,
file
),
version: allowModified ? stats.mtimeMs : undefined, version: allowModified ? stats.mtimeMs : undefined,
size: stats.size, size: stats.size,
tags: tags.length ? tags : undefined tags: tags.length ? tags : undefined
}); });
tick(fullPath);
} }
return tree; return tree;
}; };
const rootFiles = await buildTree(); await fs.writeJSON(path.join(clientPath, 'manifest.json'), {
const overridesPath = path.join(clientPath, '.manifest-overrides.json');
try {
if (await fs.pathExists(overridesPath)) {
const ov = await fs.readJSON(overridesPath);
const dels: string[] = Array.isArray(ov.del) ? ov.del : [];
for (const relPath of dels) {
const parts = relPath.split('/').filter(Boolean);
if (parts.length === 0) continue;
const fileName = parts.pop()!;
let dirNode: FileManifest = {
type: 'dir',
name: '',
files: rootFiles
} as FileManifest;
let ok = true;
for (const seg of parts) {
if (dirNode.type !== 'dir' && dirNode.type !== 'mpq') {
ok = false;
break;
}
let child = dirNode.files.find(f => f.name === seg);
if (!child) {
child = {
type: 'dir',
name: seg,
files: [],
tags: ['allowExtra']
};
dirNode.files.push(child);
}
dirNode = child;
}
if (!ok || (dirNode.type !== 'dir' && dirNode.type !== 'mpq')) {
console.warn(
`manifest-overrides: del path "${relPath}" hit a non-dir ` +
`node, skipping`
);
continue;
}
const exists = dirNode.files.some(
f => f.name === fileName && f.type === 'del'
);
if (!exists) {
dirNode.files.push({ type: 'del', name: fileName } as FileManifest);
console.log(
`manifest-overrides: inserted {type:'del', name:'${fileName}'} ` +
`under ${parts.join('/') || '<root>'}`
);
}
}
}
} catch (e) {
console.warn(
`manifest-overrides: failed to apply ${overridesPath} -- continuing ` +
`without overrides (${(e as Error).message})`
);
}
const finalPath = path.join(clientPath, 'manifest.json');
const tmpPath = path.join(clientPath, 'manifest.json.tmp');
await fs.writeJSON(tmpPath, {
build: 3, build: 3,
buildName: '3', buildName: '3',
root: { root: {
type: 'dir', type: 'dir',
name: '', name: '',
files: rootFiles files: await buildTree()
} }
}); });
await fs.rename(tmpPath, finalPath);
if (prevManifestMtimeMs > 0) {
console.log(
`mtime-skip: reused ${reused}/${total} cached hashes ` +
`(re-hashed ${total - reused})`
);
}
}; };
+36 -107
View File
@@ -1,86 +1,61 @@
import path from 'path'; import path from 'path';
import { config as loadEnv } from 'dotenv';
import express from 'express'; import express from 'express';
loadEnv();
import fs from 'fs-extra'; import fs from 'fs-extra';
import { buildCache, type BuildProgress } from './cache.js'; import { buildCache } from './cache.js';
import { getAddons, warmUp as warmUpAddons } from './addons-resolver.js'; import { getAddons, warmUp as warmUpAddons } from './addons-resolver.js';
const SourceDir = process.env.SOURCE_DIR || 'C:\\WoW\\TurtleFresh'; // Set SOURCE_DIR to your local WoW client directory (see server/.env.example).
const SourceDir: string = (() => {
const dir = process.env.SOURCE_DIR;
if (!dir) {
console.error(
'ERROR: SOURCE_DIR is not set.\n' +
'Set it to your local WoW client directory.\n' +
'Example: SOURCE_DIR="C:\\\\WoW\\\\client" npm run dev\n' +
'Or create server/.env — see server/.env.example.'
);
process.exit(1);
}
return dir;
})();
const app = express(); const app = express();
const port = 5000; const port = 5000;
const buildProgress: BuildProgress = {
state: 'idle',
done: 0,
total: 0,
currentFile: '',
startedAt: null,
finishedAt: null,
error: null
};
let buildInFlight: Promise<void> | null = null; let buildInFlight: Promise<void> | null = null;
const ensureManifestBuilt = (): Promise<void> => { const ensureManifestBuilt = (): Promise<void> => {
if (buildInFlight) return buildInFlight; if (buildInFlight) return buildInFlight;
buildProgress.state = 'building'; buildInFlight = buildCache(SourceDir).catch(e => {
buildProgress.done = 0; buildInFlight = null;
buildProgress.total = 0; throw e;
buildProgress.currentFile = ''; });
buildProgress.startedAt = Date.now();
buildProgress.finishedAt = null;
buildProgress.error = null;
buildInFlight = buildCache(SourceDir, p => {
buildProgress.done = p.done;
buildProgress.total = p.total;
buildProgress.currentFile = p.currentFile;
})
.then(() => {
buildProgress.state = 'ready';
buildProgress.finishedAt = Date.now();
})
.catch(e => {
buildProgress.state = 'failed';
buildProgress.error = e instanceof Error ? e.message : String(e);
buildProgress.finishedAt = Date.now();
buildInFlight = null;
throw e;
});
return buildInFlight; return buildInFlight;
}; };
app.get('/api/build-status', (_req, res) => {
res.json(buildProgress);
});
app.get('/api/file/:version/manifest.json', async (_req, res) => { app.get('/api/file/:version/manifest.json', async (_req, res) => {
console.log(`Fetching manifest`); console.log(`Fetching manifest`);
const filePath = path.join(SourceDir, 'manifest.json'); const filePath = path.join(SourceDir, 'manifest.json');
if (!fs.existsSync(filePath)) await ensureManifestBuilt();
if (await fs.pathExists(filePath)) { res.json(await fs.readJSON(filePath));
res.json(await fs.readJSON(filePath));
return;
}
void ensureManifestBuilt().catch(() => {
});
res.setHeader('Retry-After', '5');
res.status(503).json({
error: 'manifest_building',
message:
'Manifest is being built for the first time on this server. ' +
'Poll /api/build-status for progress; retry this endpoint when ready.',
buildProgress
});
}); });
app.get( app.get(
'/api/file/:version/*', '/api/file/:version/*',
async (req: express.Request<{ 0: string }>, res) => { async (req: express.Request<{ 0: string }>, res) => {
const filePath = req.params[0]; const filePath = req.params[0];
const resolved = path.resolve(SourceDir, filePath);
if (!resolved.startsWith(path.resolve(SourceDir) + path.sep)) {
res.status(403).send('Forbidden');
return;
}
console.log(`Fetching file: ${filePath}`); console.log(`Fetching file: ${filePath}`);
res.sendFile(resolved);
res.sendFile(path.join(SourceDir, filePath));
} }
); );
@@ -95,65 +70,19 @@ app.get('/api/addons.json', async (req, res) => {
} }
}); });
const newestSourceMtime = async (dir: string): Promise<number> => {
let newest = 0;
const entries = await fs.readdir(dir);
for (const name of entries) {
if (name === 'manifest.json' || name === 'manifest.json.tmp') continue;
const full = path.join(dir, name);
const stat = await fs.stat(full);
if (stat.isDirectory()) {
const inner = await newestSourceMtime(full);
if (inner > newest) newest = inner;
} else if (stat.mtimeMs > newest) {
newest = stat.mtimeMs;
}
}
return newest;
};
app.listen(port, () => { app.listen(port, () => {
console.log(`Server listening on port ${port}`); console.log(`Server listening on port ${port}`);
warmUpAddons(); warmUpAddons();
void (async () => { void (async () => {
const manifestPath = path.join(SourceDir, 'manifest.json'); const manifestPath = path.join(SourceDir, 'manifest.json');
if (!fs.existsSync(manifestPath)) { if (fs.existsSync(manifestPath)) return;
console.log(`Pre-warming manifest cache for ${SourceDir}...`); console.log(`Pre-warming manifest cache for ${SourceDir}...`);
try {
await ensureManifestBuilt();
console.log(`Manifest cache pre-warm complete.`);
} catch (e) {
console.error(
'Manifest pre-warm failed (will fall back to lazy build on first request):',
e
);
}
return;
}
buildProgress.state = 'ready';
buildProgress.finishedAt = Date.now();
try { try {
const manifestStat = await fs.stat(manifestPath); await ensureManifestBuilt();
const newest = await newestSourceMtime(SourceDir); console.log(`Manifest cache pre-warm complete.`);
if (newest > manifestStat.mtimeMs) {
console.log(
`Manifest is stale (newest source mtime ${new Date(newest).toISOString()} > manifest ${new Date(manifestStat.mtimeMs).toISOString()}); rebuilding in background.`
);
ensureManifestBuilt()
.then(() => console.log('Background manifest rebuild complete.'))
.catch(e =>
console.error('Background manifest rebuild failed:', e)
);
} else {
console.log(
`Manifest cache already on disk at ${manifestPath} and up to date; no rebuild needed.`
);
}
} catch (e) { } catch (e) {
console.error('Manifest staleness check failed:', e); console.error('Manifest pre-warm failed:', e);
} }
})(); })();
}); });
+180 -180
View File
@@ -1,180 +1,180 @@
import { z } from 'zod'; import { z } from 'zod';
export const ModIdSchema = z.enum([ export const ModIdSchema = z.enum([
'dxvk', 'dxvk',
'nampower', 'nampower',
'multiMonitorFix', 'multiMonitorFix',
'transmogFix', 'transmogFix',
'unitXp', 'unitXp',
'vanillaFixes', 'vanillaFixes',
'vanillaHelpers' 'vanillaHelpers'
]); ]);
export type ModId = z.infer<typeof ModIdSchema>; export type ModId = z.infer<typeof ModIdSchema>;
export type ModSource = export type ModSource =
| { | {
kind: 'directFile'; kind: 'directFile';
url: string; url: string;
versionUrl?: string; versionUrl?: string;
latestVersionUrl?: string; latestVersionUrl?: string;
parseLatest?: 'githubRelease' | 'gitlabRelease' | 'codebergRelease'; parseLatest?: 'githubRelease' | 'gitlabRelease' | 'codebergRelease';
apiUrl?: string; apiUrl?: string;
pinnedTag?: string; pinnedTag?: string;
assetName: string; assetName: string;
} }
| { | {
kind: 'archive'; kind: 'archive';
url: string; url: string;
latestVersionUrl?: string; latestVersionUrl?: string;
apiUrl?: string; apiUrl?: string;
parseLatest?: 'githubRelease' | 'gitlabRelease' | 'codebergRelease'; parseLatest?: 'githubRelease' | 'gitlabRelease' | 'codebergRelease';
pinnedTag?: string; pinnedTag?: string;
format: 'zip' | 'tar.gz'; format: 'zip' | 'tar.gz';
extractMap: Record<string, string>; extractMap: Record<string, string>;
} }
| { kind: 'managed' }; | { kind: 'managed' };
export type ModEntry = { export type ModEntry = {
id: ModId; id: ModId;
name: string; name: string;
version: string; version: string;
description: string; description: string;
recommended?: boolean; recommended?: boolean;
requires?: ModId[]; requires?: ModId[];
repoUrl: string; repoUrl: string;
source: ModSource; source: ModSource;
registerInDllsTxt?: string; registerInDllsTxt?: string;
}; };
export const MODS: ModEntry[] = [ export const MODS: ModEntry[] = [
{ {
id: 'dxvk', id: 'dxvk',
name: 'dxvk', name: 'dxvk',
version: 'v2.7.1-1', version: 'v2.7.1-1',
description: 'Enables Vulkan based rendering mode for better performance.', description: 'Enables Vulkan based rendering mode for better performance.',
recommended: true, recommended: true,
repoUrl: 'https://gitlab.com/Ph42oN/dxvk-gplasync', repoUrl: 'https://gitlab.com/Ph42oN/dxvk-gplasync',
source: { source: {
kind: 'archive', kind: 'archive',
url: 'https://gitlab.com/Ph42oN/dxvk-gplasync/-/raw/main/releases/dxvk-gplasync-v2.7.1-1.tar.gz?ref_type=heads', url: 'https://gitlab.com/Ph42oN/dxvk-gplasync/-/raw/main/releases/dxvk-gplasync-v2.7.1-1.tar.gz?ref_type=heads',
pinnedTag: 'v2.7.1-1', pinnedTag: 'v2.7.1-1',
format: 'tar.gz', format: 'tar.gz',
extractMap: { extractMap: {
'dxvk-gplasync-v2.7.1-1/x32/d3d9.dll': 'd3d9.dll' 'dxvk-gplasync-v2.7.1-1/x32/d3d9.dll': 'd3d9.dll'
} }
} }
}, },
{ {
id: 'nampower', id: 'nampower',
name: 'nampower', name: 'nampower',
version: 'v4.6.2', version: 'v4.6.2',
description: description:
'A client modification that minimizes your input lag if you have higher latency.', 'A client modification that minimizes your input lag if you have higher latency.',
repoUrl: 'https://github.com/Emyrk/nampower', repoUrl: 'https://github.com/Emyrk/nampower',
requires: ['vanillaFixes'], requires: ['vanillaFixes'],
source: { source: {
kind: 'directFile', kind: 'directFile',
url: 'https://github.com/Emyrk/nampower/releases/download/v4.6.2/nampower.dll', url: 'https://github.com/Emyrk/nampower/releases/download/v4.6.2/nampower.dll',
pinnedTag: 'v4.6.2', pinnedTag: 'v4.6.2',
assetName: 'nampower.dll' assetName: 'nampower.dll'
}, },
registerInDllsTxt: 'nampower.dll' registerInDllsTxt: 'nampower.dll'
}, },
{ {
id: 'multiMonitorFix', id: 'multiMonitorFix',
name: 'no1600x1200', name: 'no1600x1200',
version: '0.2', version: '0.2',
description: 'Fix for larger resolutions or multi monitor setups.', description: 'Fix for larger resolutions or multi monitor setups.',
repoUrl: 'https://github.com/Mates1500/VanillaMultiMonitorFix', repoUrl: 'https://github.com/Mates1500/VanillaMultiMonitorFix',
requires: ['vanillaFixes'], requires: ['vanillaFixes'],
source: { source: {
kind: 'archive', kind: 'archive',
url: 'https://github.com/Mates1500/VanillaMultiMonitorFix/releases/download/0.2/release.zip', url: 'https://github.com/Mates1500/VanillaMultiMonitorFix/releases/download/0.2/release.zip',
apiUrl: apiUrl:
'https://api.github.com/repos/Mates1500/VanillaMultiMonitorFix/releases/latest', 'https://api.github.com/repos/Mates1500/VanillaMultiMonitorFix/releases/latest',
parseLatest: 'githubRelease', parseLatest: 'githubRelease',
pinnedTag: '0.2', pinnedTag: '0.2',
format: 'zip', format: 'zip',
extractMap: { extractMap: {
'VanillaMultiMonitorFix.dll': 'VanillaMultiMonitorFix.dll' 'VanillaMultiMonitorFix.dll': 'VanillaMultiMonitorFix.dll'
} }
}, },
registerInDllsTxt: 'VanillaMultiMonitorFix.dll' registerInDllsTxt: 'VanillaMultiMonitorFix.dll'
}, },
{ {
id: 'transmogFix', id: 'transmogFix',
name: 'transmogFix', name: 'transmogFix',
version: 'v0.7.0', version: 'v0.7.0',
description: description:
"A client-side fix that eliminates frame drops caused by the server's transmogrification durability workaround.", "A client-side fix that eliminates frame drops caused by the server's transmogrification durability workaround.",
repoUrl: 'https://codeberg.org/MarcelineVQ/WeirdUtils', repoUrl: 'https://codeberg.org/MarcelineVQ/WeirdUtils',
requires: ['vanillaFixes'], requires: ['vanillaFixes'],
source: { source: {
kind: 'directFile', kind: 'directFile',
url: 'https://codeberg.org/MarcelineVQ/WeirdUtils/releases/download/v0.7.0/transmogfix.dll', url: 'https://codeberg.org/MarcelineVQ/WeirdUtils/releases/download/v0.7.0/transmogfix.dll',
pinnedTag: 'v0.7.0', pinnedTag: 'v0.7.0',
assetName: 'transmogfix.dll' assetName: 'transmogfix.dll'
}, },
registerInDllsTxt: 'transmogfix.dll' registerInDllsTxt: 'transmogfix.dll'
}, },
{ {
id: 'unitXp', id: 'unitXp',
name: 'unitXp', name: 'unitXp',
version: 'v89', version: 'v89',
description: 'An attempt to make Vanilla 1.12 modern.', description: 'An attempt to make Vanilla 1.12 modern.',
repoUrl: 'https://codeberg.org/konaka/UnitXP_SP3', repoUrl: 'https://codeberg.org/konaka/UnitXP_SP3',
requires: ['vanillaFixes'], requires: ['vanillaFixes'],
source: { source: {
kind: 'archive', kind: 'archive',
url: 'https://codeberg.org/konaka/UnitXP_SP3/releases/download/v89/UnitXP_SP3%20v89.zip', url: 'https://codeberg.org/konaka/UnitXP_SP3/releases/download/v89/UnitXP_SP3%20v89.zip',
pinnedTag: 'v89', pinnedTag: 'v89',
format: 'zip', format: 'zip',
extractMap: { extractMap: {
'UnitXP_SP3.dll': 'UnitXP_SP3.dll' 'UnitXP_SP3.dll': 'UnitXP_SP3.dll'
} }
}, },
registerInDllsTxt: 'UnitXP_SP3.dll' registerInDllsTxt: 'UnitXP_SP3.dll'
}, },
{ {
id: 'vanillaFixes', id: 'vanillaFixes',
name: 'vanillaFixes', name: 'vanillaFixes',
version: 'v1.5.3', version: 'v1.5.3',
description: 'A client modification that eliminates stutter and animation lag.', description: 'A client modification that eliminates stutter and animation lag.',
recommended: true, recommended: true,
repoUrl: 'https://github.com/hannesmann/vanillafixes', repoUrl: 'https://github.com/hannesmann/vanillafixes',
source: { source: {
kind: 'archive', kind: 'archive',
url: 'https://github.com/hannesmann/vanillafixes/releases/download/v1.5.3/vanillafixes-1.5.3.zip', url: 'https://github.com/hannesmann/vanillafixes/releases/download/v1.5.3/vanillafixes-1.5.3.zip',
apiUrl: apiUrl:
'https://api.github.com/repos/hannesmann/vanillafixes/releases/latest', 'https://api.github.com/repos/hannesmann/vanillafixes/releases/latest',
parseLatest: 'githubRelease', parseLatest: 'githubRelease',
pinnedTag: 'v1.5.3', pinnedTag: 'v1.5.3',
format: 'zip', format: 'zip',
extractMap: { extractMap: {
'VfPatcher.dll': 'VfPatcher.dll', 'VfPatcher.dll': 'VfPatcher.dll',
'VanillaFixes.exe': 'VanillaFixes.exe' 'VanillaFixes.exe': 'VanillaFixes.exe'
} }
} }
}, },
{ {
id: 'vanillaHelpers', id: 'vanillaHelpers',
name: 'vanillaHelpers', name: 'vanillaHelpers',
version: 'v1.1.2', version: 'v1.1.2',
description: 'Utility library that might be required by other patches and addons.', description: 'Utility library that might be required by other patches and addons.',
repoUrl: 'https://github.com/isfir/VanillaHelpers', repoUrl: 'https://github.com/isfir/VanillaHelpers',
requires: ['vanillaFixes'], requires: ['vanillaFixes'],
source: { source: {
kind: 'directFile', kind: 'directFile',
url: 'https://github.com/isfir/VanillaHelpers/releases/download/v1.1.2/VanillaHelpers.dll', url: 'https://github.com/isfir/VanillaHelpers/releases/download/v1.1.2/VanillaHelpers.dll',
apiUrl: apiUrl:
'https://api.github.com/repos/isfir/VanillaHelpers/releases/latest', 'https://api.github.com/repos/isfir/VanillaHelpers/releases/latest',
parseLatest: 'githubRelease', parseLatest: 'githubRelease',
pinnedTag: 'v1.1.2', pinnedTag: 'v1.1.2',
assetName: 'VanillaHelpers.dll' assetName: 'VanillaHelpers.dll'
}, },
registerInDllsTxt: 'VanillaHelpers.dll' registerInDllsTxt: 'VanillaHelpers.dll'
} }
]; ];
export const getMod = (id: ModId): ModEntry | undefined => export const getMod = (id: ModId): ModEntry | undefined =>
MODS.find(m => m.id === id); MODS.find(m => m.id === id);