mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-08 10:20:37 +01:00
Compare commits
22
Commits
production
...
scripts
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2d3c59e97 | ||
|
|
1cb760e227 | ||
|
|
ce27388c00 | ||
|
|
e8b2651595 | ||
|
|
3695e2cd5e | ||
|
|
01445cf845 | ||
|
|
b121147b43 | ||
|
|
b3b56462d1 | ||
|
|
d42cccc3c2 | ||
|
|
c592501f48 | ||
|
|
5f4cf15500 | ||
|
|
232b3e7770 | ||
|
|
99373872ed | ||
|
|
fddf407cc0 | ||
|
|
88346308cb | ||
|
|
ebedac0642 | ||
|
|
2150fb3758 | ||
|
|
682f039c6c | ||
|
|
0632899ecf | ||
|
|
8566b2cc2f | ||
|
|
ffb5a1f9c8 | ||
|
|
85e03d3b42 |
No files matched your search
@@ -30,8 +30,8 @@ jobs:
|
|||||||
- name: Setup bun
|
- name: Setup bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Set up script
|
||||||
run: bun install
|
run: scripts/setup.sh
|
||||||
|
|
||||||
- name: Setup Git
|
- name: Setup Git
|
||||||
run: |
|
run: |
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
- name: Discover IDs
|
- name: Discover IDs
|
||||||
run: bun run discover-ids
|
run: scripts/discover-ids.sh
|
||||||
env:
|
env:
|
||||||
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
|
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
|
||||||
GH_TOKEN: ${{ secrets.IDS_TOKEN }}
|
GH_TOKEN: ${{ secrets.IDS_TOKEN }}
|
||||||
@@ -89,6 +89,9 @@ env:
|
|||||||
# wargaming
|
# wargaming
|
||||||
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
|
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
|
||||||
|
|
||||||
|
# workflow
|
||||||
|
SHOULD_USE_BLITZKIT_CLOSED: ${{ github.event_name != 'pull_request' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build website
|
name: Build website
|
||||||
@@ -107,11 +110,20 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
# the PR's ref, or the manual input, or production (selected when cron)
|
|
||||||
ref: ${{ github.event.pull_request.head.sha || inputs.branch || 'production' }}
|
ref: ${{ github.event.pull_request.head.sha || inputs.branch || 'production' }}
|
||||||
submodules: recursive
|
submodules: false
|
||||||
token: ${{ secrets.GH_TOKEN || github.token }}
|
token: ${{ secrets.GH_TOKEN || github.token }}
|
||||||
|
|
||||||
|
# Future proofing for Reforged: we'll have both closed and open repos, so
|
||||||
|
# we need to handle them here instead of disabling submodules outright
|
||||||
|
- name: Checkout submodules
|
||||||
|
run: |
|
||||||
|
if [ "$SHOULD_USE_BLITZKIT_CLOSED" = "true" ]; then
|
||||||
|
git submodule update --init --recursive --depth=1
|
||||||
|
else
|
||||||
|
git submodule update --init --recursive --depth=1 -- . ':!packages/closed'
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Setup bun
|
- name: Setup bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
@@ -125,19 +137,21 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y aria2 libarchive-tools
|
sudo apt-get install -y aria2 libarchive-tools
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Set up script
|
||||||
run: bun install
|
run: scripts/setup.sh
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: scripts/lint.sh
|
||||||
|
|
||||||
- name: Download client
|
- name: Download client
|
||||||
run: bun run download-client
|
run: scripts/download-client.sh
|
||||||
|
|
||||||
- name: Download SmartDLC
|
- name: Download SmartDLC
|
||||||
if: ${{ env.CLIENT_PROVIDER == 'steam' }}
|
if: ${{ env.CLIENT_PROVIDER == 'steam' && env.SHOULD_USE_BLITZKIT_CLOSED == 'true' }}
|
||||||
|
run: scripts/download-smart-dlc.sh
|
||||||
run: bun run download-smart-dlc
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: bun run --bun build
|
run: scripts/build-website.sh
|
||||||
env:
|
env:
|
||||||
CHUNK: ${{ matrix.chunk }}
|
CHUNK: ${{ matrix.chunk }}
|
||||||
|
|
||||||
|
|||||||
@@ -44,16 +44,16 @@ cd blitzkit
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> There is a closed source submodule under `packages/closed` that you can safely ignore. This sole purpose of this submodule is to provide `dvp.ts`, a proprietary Wargaming game asset tree compression tool. You will not need this at any point.
|
> There is a closed source submodule under `packages/closed` that you can safely ignore. This sole purpose of this submodule is to provide `dvp.ts`, a proprietary Wargaming game asset tree compression tool. You will not need this at any point.
|
||||||
|
|
||||||
Then, download the dependencies with Bun.
|
Then, grand execution permission to the setup script.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun install
|
chmod +x scripts/setup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, compile the proto files into TypeScript.
|
Finally, run the setup script.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
build build-protos
|
scripts/setup.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Developing: Environment
|
## Developing: Environment
|
||||||
@@ -65,11 +65,11 @@ Go through the different packages in the `packages` directory and clone their re
|
|||||||
Launching the website is as easy as running the `dev` script.
|
Launching the website is as easy as running the `dev` script.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun dev
|
scripts/dev-website.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
If you ever make a change to any proto file, make sure to rebuild them.
|
If you ever make a change to any proto file, make sure to rebuild them.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
build build-protos
|
scripts/build-protos.sh
|
||||||
```
|
```
|
||||||
@@ -55,7 +55,6 @@
|
|||||||
"packages/scripts": {
|
"packages/scripts": {
|
||||||
"name": "@blitzkit/scripts",
|
"name": "@blitzkit/scripts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blitzkit/closed": "workspace:*",
|
|
||||||
"@blitzkit/core": "workspace:*",
|
"@blitzkit/core": "workspace:*",
|
||||||
"@blitzkit/i18n": "workspace:*",
|
"@blitzkit/i18n": "workspace:*",
|
||||||
"@gltf-transform/core": "^4.2.1",
|
"@gltf-transform/core": "^4.2.1",
|
||||||
@@ -1643,8 +1642,6 @@
|
|||||||
|
|
||||||
"jimp": ["[email protected]", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="],
|
"jimp": ["[email protected]", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="],
|
||||||
|
|
||||||
"jiti": ["[email protected]", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ=="],
|
|
||||||
|
|
||||||
"jpeg-js": ["[email protected]", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
|
"jpeg-js": ["[email protected]", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
|
||||||
|
|
||||||
"js-tokens": ["[email protected]", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
"js-tokens": ["[email protected]", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
@@ -1951,7 +1948,7 @@
|
|||||||
|
|
||||||
"potpack": ["[email protected]", "", {}, "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ=="],
|
"potpack": ["[email protected]", "", {}, "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ=="],
|
||||||
|
|
||||||
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
"prettier": ["prettier@2.8.7", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw=="],
|
||||||
|
|
||||||
"pretty-bytes": ["[email protected]", "", {}, "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="],
|
"pretty-bytes": ["[email protected]", "", {}, "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="],
|
||||||
|
|
||||||
@@ -2733,8 +2730,6 @@
|
|||||||
|
|
||||||
"wrap-ansi/strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
"wrap-ansi/strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||||
|
|
||||||
"yaml-language-server/prettier": ["[email protected]", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw=="],
|
|
||||||
|
|
||||||
"yaml-language-server/request-light": ["[email protected]", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="],
|
"yaml-language-server/request-light": ["[email protected]", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="],
|
||||||
|
|
||||||
"yaml-language-server/vscode-languageserver": ["[email protected]", "", { "dependencies": { "vscode-languageserver-protocol": "3.16.0" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw=="],
|
"yaml-language-server/vscode-languageserver": ["[email protected]", "", { "dependencies": { "vscode-languageserver-protocol": "3.16.0" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw=="],
|
||||||
|
|||||||
@@ -3,17 +3,6 @@
|
|||||||
"version": "1.33.0",
|
"version": "1.33.0",
|
||||||
"license": "BlitzKit License",
|
"license": "BlitzKit License",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
|
||||||
"dev": "cd packages/website && bun --bun run dev",
|
|
||||||
"build": "cd packages/website && bun --bun run build",
|
|
||||||
"discover-ids": "cd packages/scripts && bun run discover-ids",
|
|
||||||
"build-protos": "cd packages/scripts && bun run build-protos",
|
|
||||||
"record-performance": "cd packages/scripts && bun run record-performance",
|
|
||||||
"download-client": "cd packages/scripts && bun run download-client",
|
|
||||||
"download-smart-dlc": "cd packages/scripts && bun run download-smart-dlc",
|
|
||||||
"clean-astro": "rm -rf packages/website/.astro packages/website/node_modules/.astro packages/website/node_modules/.vite",
|
|
||||||
"vfs": "cd packages/scripts && bun run vfs"
|
|
||||||
},
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|||||||
+1
-1
Submodule packages/closed updated: f89f0e0772...182fcdb273.
@@ -79,14 +79,8 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
|
|||||||
I_HAT,
|
I_HAT,
|
||||||
-MathUtils.degToRad(turretRotation.pitch),
|
-MathUtils.degToRad(turretRotation.pitch),
|
||||||
)
|
)
|
||||||
.applyAxisAngle(
|
.applyAxisAngle(J_HAT, -MathUtils.degToRad(turretRotation.roll))
|
||||||
J_HAT,
|
.applyAxisAngle(K_HAT, -MathUtils.degToRad(turretRotation.yaw));
|
||||||
-MathUtils.degToRad(turretRotation.roll),
|
|
||||||
)
|
|
||||||
.applyAxisAngle(
|
|
||||||
K_HAT,
|
|
||||||
-MathUtils.degToRad(turretRotation.yaw),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const coordinates = position.toArray();
|
const coordinates = position.toArray();
|
||||||
@@ -298,23 +292,29 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
|
|||||||
|
|
||||||
turret.userString;
|
turret.userString;
|
||||||
|
|
||||||
const turretCollisionName = parsePath(
|
let turretCollisionName = parsePath(
|
||||||
turret.hitTester.collisionModel,
|
turret.hitTester.collisionModel,
|
||||||
).name.toLowerCase();
|
).name.toLowerCase();
|
||||||
const isFullYaw = -turretYaw[0] + turretYaw[1] === 360;
|
const isFullYaw = -turretYaw[0] + turretYaw[1] === 360;
|
||||||
|
|
||||||
if (!collisionBoundingBoxes[turretCollisionName] && isFullYaw) {
|
if (!collisionBoundingBoxes[turretCollisionName] && isFullYaw) {
|
||||||
throw new Error(
|
turretCollisionName = "turret_01";
|
||||||
`Missing turret collision mesh for ${nation}-${tankKey} ${turretKey}`,
|
|
||||||
|
console.warn(
|
||||||
|
`HACK: Missing turret collision mesh for ${nation}/${tankKey}; turret key: ${
|
||||||
|
turretKey
|
||||||
|
}; collision model: ${turretCollisionName}; available options: ${Object.keys(
|
||||||
|
collisionBoundingBoxes,
|
||||||
|
).join(", ")}; using ${turretCollisionName} instead`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const turretBoundingBox = collisionBoundingBoxes[
|
let turretBoundingBox = collisionBoundingBoxes[turretCollisionName];
|
||||||
turretCollisionName
|
|
||||||
] ?? {
|
if (turretBoundingBox === undefined) {
|
||||||
min: [0, 0, 0] as Vector3Tuple,
|
console.warn("HACK: missing collision; using 0's");
|
||||||
max: [0, 0, 0] as Vector3Tuple,
|
turretBoundingBox = { min: [0, 0, 0], max: [0, 0, 0] };
|
||||||
};
|
}
|
||||||
|
|
||||||
modelDefinitions.models[tankId].turrets[turretId] = {
|
modelDefinitions.models[tankId].turrets[turretId] = {
|
||||||
armor: turretArmor,
|
armor: turretArmor,
|
||||||
@@ -342,9 +342,18 @@ export abstract class ServerBlitzKitAPI3 extends ServerBlitzKitAPI2 {
|
|||||||
)
|
)
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.map(Number) as [number, number];
|
.map(Number) as [number, number];
|
||||||
const gunModel = Number(
|
let gunModel = Number(
|
||||||
parsePath(gun.models.undamaged).name.split("_")[1],
|
parsePath(gun.models.undamaged).name.split("_")[1],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (tankId === 30497) {
|
||||||
|
console.warn(
|
||||||
|
"HACK: using gun_02 for hacker tank instead of gun_01... hehe, that's quite appropriate for this tank haha",
|
||||||
|
);
|
||||||
|
|
||||||
|
gunModel = 2;
|
||||||
|
}
|
||||||
|
|
||||||
const front = gun.extraPitchLimits?.front
|
const front = gun.extraPitchLimits?.front
|
||||||
? gun.extraPitchLimits.front.split(" ").map(Number)
|
? gun.extraPitchLimits.front.split(" ").map(Number)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@@ -158,7 +158,11 @@ export type Component =
|
|||||||
"ro.notShadowOnly": boolean;
|
"ro.notShadowOnly": boolean;
|
||||||
"ro.sOclCull": boolean;
|
"ro.sOclCull": boolean;
|
||||||
"ro.sOclIndex": number;
|
"ro.sOclIndex": number;
|
||||||
} & Record<`rb${number}.${"lodIndex" | "switchIndex"}`, number>;
|
} & Record<`rb${number}.${"lodIndex" | "switchIndex"}`, number> &
|
||||||
|
Record<
|
||||||
|
`skinnedObject.batch${number}.targetsData`,
|
||||||
|
ArrayBuffer | undefined
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
"comp.typename": "SlotComponent";
|
"comp.typename": "SlotComponent";
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@blitzkit/scripts",
|
"name": "@blitzkit/scripts",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
|
||||||
"discover-ids": "bun src/discoverIds/index.ts",
|
|
||||||
"build-protos": "bun src/protos.ts",
|
|
||||||
"record-performance": "bun src/recordPerformance/index.ts",
|
|
||||||
"download-client": "bun src/downloadClient/index.ts",
|
|
||||||
"download-smart-dlc": "bun src/downloadSmartDLC/index.ts",
|
|
||||||
"vfs": "bun src/vfs/index.ts"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blitzkit/closed": "workspace:*",
|
|
||||||
"@blitzkit/core": "workspace:*",
|
"@blitzkit/core": "workspace:*",
|
||||||
"@blitzkit/i18n": "workspace:*",
|
"@blitzkit/i18n": "workspace:*",
|
||||||
"@gltf-transform/core": "^4.2.1",
|
"@gltf-transform/core": "^4.2.1",
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
import { dvp } from "@blitzkit/closed";
|
|
||||||
import { assertSecret } from "@blitzkit/core";
|
|
||||||
import { mkdir, writeFile } from "fs/promises";
|
|
||||||
import { parse as parsePath } from "path";
|
|
||||||
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
||||||
import { RegionsPublish } from "../types/regionsPublish";
|
|
||||||
import { vfs } from "../vfs/vfs";
|
|
||||||
|
|
||||||
const CLIENT_DIR = assertSecret(import.meta.env.CLIENT_DIR);
|
|
||||||
|
|
||||||
const regionsPublish = await vfs.yaml<RegionsPublish>(
|
|
||||||
"Data/regions_publish.yaml",
|
|
||||||
);
|
|
||||||
const [smartDlcUrl] = regionsPublish.Common.smartDlcUrls;
|
|
||||||
|
|
||||||
const versionTextFile = await vfs.text(`Data/version.txt`);
|
|
||||||
const currentVersion = versionTextFile
|
|
||||||
.split(" ")[0]
|
|
||||||
.split(".")
|
|
||||||
.slice(0, 3)
|
|
||||||
.join(".");
|
|
||||||
|
|
||||||
console.log(`Installing patches for ${currentVersion}...`);
|
|
||||||
|
|
||||||
let patchIndex = 1;
|
|
||||||
while (true) {
|
|
||||||
const response = await fetch(
|
|
||||||
`${smartDlcUrl}/s${currentVersion}_${patchIndex}.yaml`,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(patchIndex, response.status);
|
|
||||||
|
|
||||||
if (!response.ok) break;
|
|
||||||
|
|
||||||
console.log(`Applying patch ${patchIndex}...`);
|
|
||||||
|
|
||||||
const data = parseYaml(await response.text());
|
|
||||||
const dvpm = await fetch(`${smartDlcUrl}/${data.dx11}`).then((response) =>
|
|
||||||
response.arrayBuffer(),
|
|
||||||
);
|
|
||||||
const dvpd = await fetch(
|
|
||||||
`${smartDlcUrl}/${data.dx11.replace(".dvpm", ".dvpd")}`,
|
|
||||||
).then((response) => response.arrayBuffer());
|
|
||||||
const files = await dvp(dvpm, dvpd);
|
|
||||||
|
|
||||||
for (const { path, data } of files) {
|
|
||||||
console.log(`Patching "${path}"...`);
|
|
||||||
|
|
||||||
const { dir } = parsePath(path);
|
|
||||||
|
|
||||||
await mkdir(`${CLIENT_DIR}/Data/${dir}`, { recursive: true });
|
|
||||||
|
|
||||||
const buffer = Buffer.from(data);
|
|
||||||
|
|
||||||
await writeFile(`${CLIENT_DIR}/Data/${path}`, new Uint8Array(buffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("patches done for patch");
|
|
||||||
|
|
||||||
if ("dynamicContentLocalizationsDir" in data) {
|
|
||||||
console.log(
|
|
||||||
`${smartDlcUrl}/${data.dynamicContentLocalizationsDir}/en.yaml`,
|
|
||||||
);
|
|
||||||
const localizationsResponse = await fetch(
|
|
||||||
`${smartDlcUrl}/${data.dynamicContentLocalizationsDir}/en.yaml`,
|
|
||||||
);
|
|
||||||
console.log("fetched");
|
|
||||||
const newStrings = parseYaml(await localizationsResponse.text());
|
|
||||||
console.log("got new strings");
|
|
||||||
const oldStrings =
|
|
||||||
await vfs.yaml<Record<string, string>>(`Data/Strings/en.yaml`);
|
|
||||||
console.log("got old strings");
|
|
||||||
const patchedStrings = { ...oldStrings, ...newStrings };
|
|
||||||
const patchedContent = stringifyYaml(patchedStrings);
|
|
||||||
console.log("stringified");
|
|
||||||
|
|
||||||
await mkdir(`${CLIENT_DIR}/Data/Strings`, { recursive: true });
|
|
||||||
console.log("mkdir");
|
|
||||||
await writeFile(
|
|
||||||
`${CLIENT_DIR}/Data/Strings/en.yaml`,
|
|
||||||
new Uint8Array(Buffer.from(patchedContent)),
|
|
||||||
);
|
|
||||||
console.log("file");
|
|
||||||
}
|
|
||||||
|
|
||||||
patchIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
vfs.dispose();
|
|
||||||
process.exit(0);
|
|
||||||
@@ -1,13 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@blitzkit/website",
|
"name": "@blitzkit/website",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
|
||||||
"dev": "astro dev",
|
|
||||||
"build": "astro build",
|
|
||||||
"preview": "astro preview",
|
|
||||||
"lint": "astro check",
|
|
||||||
"astro": "astro"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.9.4",
|
"@astrojs/check": "^0.9.4",
|
||||||
"@astrojs/node": "^9.4.4",
|
"@astrojs/node": "^9.4.4",
|
||||||
|
|||||||
+35
-2
@@ -9,6 +9,7 @@ import { skinTree } from "../../../../../../core/blitzkit/skinTree";
|
|||||||
import { controlsEnabledEvent } from "../../../../../../core/controlsEnabled";
|
import { controlsEnabledEvent } from "../../../../../../core/controlsEnabled";
|
||||||
import { useModel } from "../../../../../../hooks/useModel";
|
import { useModel } from "../../../../../../hooks/useModel";
|
||||||
import { useSkin } from "../../../../../../hooks/useSkin";
|
import { useSkin } from "../../../../../../hooks/useSkin";
|
||||||
|
import { useSkinAnimation } from "../../../../../../hooks/useSkinAnimation";
|
||||||
import { useTankModelDefinition } from "../../../../../../hooks/useTankModelDefinition";
|
import { useTankModelDefinition } from "../../../../../../hooks/useTankModelDefinition";
|
||||||
import { useTankTransform } from "../../../../../../hooks/useTankTransform";
|
import { useTankTransform } from "../../../../../../hooks/useTankTransform";
|
||||||
import { Duel } from "../../../../../../stores/duel";
|
import { Duel } from "../../../../../../stores/duel";
|
||||||
@@ -36,12 +37,17 @@ export function TankModel() {
|
|||||||
const skins = [skinGltf, attachmentGltf].filter((skin) => skin !== undefined);
|
const skins = [skinGltf, attachmentGltf].filter((skin) => skin !== undefined);
|
||||||
|
|
||||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||||
|
useSkinAnimation(skins);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const emissiveMaterials = new Set<MeshStandardMaterial>();
|
const emissiveMaterials = new Set<MeshStandardMaterial>();
|
||||||
|
const roots = [
|
||||||
|
gltf.scene,
|
||||||
|
...skins.flatMap((skin) => [...skin.parts.values()].flat()),
|
||||||
|
];
|
||||||
|
|
||||||
for (const scene of [gltf.scene, ...skins.map((skin) => skin.scene)]) {
|
for (const root of roots) {
|
||||||
scene.traverse((object) => {
|
root.traverse((object) => {
|
||||||
if (
|
if (
|
||||||
object instanceof Mesh &&
|
object instanceof Mesh &&
|
||||||
object.material instanceof MeshStandardMaterial &&
|
object.material instanceof MeshStandardMaterial &&
|
||||||
@@ -144,6 +150,15 @@ export function TankModel() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
primitive(object, key) {
|
||||||
|
return (
|
||||||
|
<primitive
|
||||||
|
key={key}
|
||||||
|
object={object}
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
|
|
||||||
@@ -221,6 +236,15 @@ export function TankModel() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
primitive(object, key) {
|
||||||
|
return (
|
||||||
|
<primitive
|
||||||
|
key={key}
|
||||||
|
object={object}
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
|
|
||||||
@@ -302,6 +326,15 @@ export function TankModel() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
primitive(object, key) {
|
||||||
|
return (
|
||||||
|
<primitive
|
||||||
|
key={key}
|
||||||
|
object={object}
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export function isHalloween() {
|
export function isHalloween() {
|
||||||
|
return false;
|
||||||
|
|
||||||
const data = new Date();
|
const data = new Date();
|
||||||
return data.getMonth() === 9 && data.getDate() === 31;
|
return data.getMonth() === 9 && data.getDate() === 31;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
import type { ComponentProps, ReactNode } from "react";
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
import { AdditiveBlending, Mesh, MeshStandardMaterial, Object3D } from "three";
|
import {
|
||||||
|
AdditiveBlending,
|
||||||
|
Mesh,
|
||||||
|
MeshStandardMaterial,
|
||||||
|
Object3D,
|
||||||
|
type Material,
|
||||||
|
} from "three";
|
||||||
|
|
||||||
|
export function prepareMaterial(material: Material | Material[]) {
|
||||||
|
if (!(material instanceof MeshStandardMaterial)) return;
|
||||||
|
|
||||||
|
material.depthWrite = true;
|
||||||
|
|
||||||
|
if (material.userData.blending === "additive") {
|
||||||
|
material.blending = AdditiveBlending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function jsxTree(
|
export function jsxTree(
|
||||||
node: Object3D,
|
node: Object3D,
|
||||||
@@ -14,6 +30,7 @@ export function jsxTree(
|
|||||||
props: ComponentProps<"group">,
|
props: ComponentProps<"group">,
|
||||||
key: string,
|
key: string,
|
||||||
) => ReactNode;
|
) => ReactNode;
|
||||||
|
primitive?: (object3d: Object3D, key: string) => ReactNode;
|
||||||
},
|
},
|
||||||
): ReactNode {
|
): ReactNode {
|
||||||
if (node instanceof Mesh) {
|
if (node instanceof Mesh) {
|
||||||
@@ -25,13 +42,7 @@ export function jsxTree(
|
|||||||
scale: node.scale,
|
scale: node.scale,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (props.material instanceof MeshStandardMaterial) {
|
prepareMaterial(props.material);
|
||||||
props.material.depthWrite = true;
|
|
||||||
|
|
||||||
if (props.material.userData.blending === "additive") {
|
|
||||||
props.material.blending = AdditiveBlending;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergers?.mesh ? (
|
return mergers?.mesh ? (
|
||||||
mergers.mesh(node, props, node.uuid)
|
mergers.mesh(node, props, node.uuid)
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { ReadStream } from "@blitzkit/core";
|
||||||
|
import { times } from "lodash-es";
|
||||||
|
|
||||||
|
const HEADER_SIZE = 16;
|
||||||
|
|
||||||
|
export enum MotionChannel {
|
||||||
|
Translation,
|
||||||
|
Rotation,
|
||||||
|
Scale,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MotionKeys {
|
||||||
|
times: number[];
|
||||||
|
values: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Motion {
|
||||||
|
duration: number;
|
||||||
|
tracks: Map<string, Map<MotionChannel, MotionKeys>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MotionReadStream extends ReadStream {
|
||||||
|
motion(): Motion {
|
||||||
|
this.seek(HEADER_SIZE);
|
||||||
|
|
||||||
|
const duration = this.float32();
|
||||||
|
const tracks: Motion["tracks"] = new Map();
|
||||||
|
|
||||||
|
times(this.uint32(), () => {
|
||||||
|
const uid = this.paddedString();
|
||||||
|
const channels = new Map<MotionChannel, MotionKeys>();
|
||||||
|
|
||||||
|
this.paddedString();
|
||||||
|
this.seek(4);
|
||||||
|
|
||||||
|
times(this.uint32(), () => {
|
||||||
|
const type = this.uint32() & 0xff;
|
||||||
|
|
||||||
|
this.seek(4);
|
||||||
|
|
||||||
|
const dimension = this.uint32() & 0xff;
|
||||||
|
const keys: MotionKeys = { times: [], values: [] };
|
||||||
|
|
||||||
|
times(this.uint32(), () => {
|
||||||
|
keys.times.push(this.float32());
|
||||||
|
times(dimension, () => keys.values.push(this.float32()));
|
||||||
|
});
|
||||||
|
channels.set(type, keys);
|
||||||
|
});
|
||||||
|
tracks.set(uid, channels);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { duration, tracks };
|
||||||
|
}
|
||||||
|
|
||||||
|
paddedString() {
|
||||||
|
const end = new Uint8Array(this.buffer).indexOf(0, this.index);
|
||||||
|
const value = this.utf8(end - this.index);
|
||||||
|
|
||||||
|
this.index = (end + 4) & ~3;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
import { VertexAttribute, type Component } from "@blitzkit/core";
|
||||||
|
import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
|
||||||
|
import {
|
||||||
|
MathUtils,
|
||||||
|
type Buffer,
|
||||||
|
type Document,
|
||||||
|
type mat4,
|
||||||
|
type Node,
|
||||||
|
type Primitive,
|
||||||
|
type Skin,
|
||||||
|
type TypedArray,
|
||||||
|
type vec3,
|
||||||
|
type vec4,
|
||||||
|
} from "@gltf-transform/core";
|
||||||
|
import { isEqual, times } from "lodash-es";
|
||||||
|
import { dirname, normalize } from "path/posix";
|
||||||
|
import { Matrix4 } from "three";
|
||||||
|
import { MotionChannel, MotionReadStream, type Motion } from "./motion";
|
||||||
|
|
||||||
|
type SkeletonComponent = Extract<
|
||||||
|
Component,
|
||||||
|
{ "comp.typename": "SkeletonComponent" }
|
||||||
|
>;
|
||||||
|
type MotionComponent = Extract<
|
||||||
|
Component,
|
||||||
|
{ "comp.typename": "MotionComponent" }
|
||||||
|
>;
|
||||||
|
type AccessorType = "SCALAR" | "VEC3" | "VEC4" | "MAT4";
|
||||||
|
|
||||||
|
const JOINT_ROOT = 0xffffff;
|
||||||
|
const MOTION_PATHS = {
|
||||||
|
[MotionChannel.Translation]: "translation",
|
||||||
|
[MotionChannel.Rotation]: "rotation",
|
||||||
|
[MotionChannel.Scale]: "scale",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
function createAccessor(
|
||||||
|
document: Document,
|
||||||
|
buffer: Buffer,
|
||||||
|
type: AccessorType,
|
||||||
|
array: TypedArray,
|
||||||
|
) {
|
||||||
|
return document
|
||||||
|
.createAccessor()
|
||||||
|
.setType(type)
|
||||||
|
.setArray(array)
|
||||||
|
.setBuffer(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadMotion(
|
||||||
|
vfs: AbstractVFS,
|
||||||
|
path: string,
|
||||||
|
component: MotionComponent,
|
||||||
|
) {
|
||||||
|
const file = await vfs.file(
|
||||||
|
normalize(`Data/3d/${dirname(path)}/${component["motion.filepath"]}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
return new MotionReadStream(
|
||||||
|
file.buffer.slice(
|
||||||
|
file.byteOffset,
|
||||||
|
file.byteOffset + file.byteLength,
|
||||||
|
) as ArrayBuffer,
|
||||||
|
).motion();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSkeleton(
|
||||||
|
document: Document,
|
||||||
|
buffer: Buffer,
|
||||||
|
node: Node,
|
||||||
|
skeleton: SkeletonComponent,
|
||||||
|
motion: Motion | undefined,
|
||||||
|
) {
|
||||||
|
const joints = times(
|
||||||
|
skeleton.jointsCount,
|
||||||
|
(index) => skeleton.joints[index.toString().padStart(4, "0")],
|
||||||
|
);
|
||||||
|
const jointNodes = joints.map((joint) => {
|
||||||
|
const translation: vec3 = [0, 0, 0];
|
||||||
|
const rotation: vec4 = [0, 0, 0, 1];
|
||||||
|
const scale: vec3 = [1, 1, 1];
|
||||||
|
const track = motion?.tracks.get(joint["joint.uid"]);
|
||||||
|
const trackScale = track?.get(MotionChannel.Scale)?.values[0];
|
||||||
|
|
||||||
|
MathUtils.decompose(
|
||||||
|
joint["joint.bindPose"].flat() as mat4,
|
||||||
|
translation,
|
||||||
|
rotation,
|
||||||
|
scale,
|
||||||
|
);
|
||||||
|
|
||||||
|
// rest on the first frame so the static pose matches the animation
|
||||||
|
return document
|
||||||
|
.createNode(joint["joint.name"])
|
||||||
|
.setTranslation(
|
||||||
|
(track?.get(MotionChannel.Translation)?.values.slice(0, 3) as vec3) ??
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
.setRotation(
|
||||||
|
(track?.get(MotionChannel.Rotation)?.values.slice(0, 4) as vec4) ??
|
||||||
|
rotation,
|
||||||
|
)
|
||||||
|
.setScale(
|
||||||
|
trackScale === undefined ? scale : [trackScale, trackScale, trackScale],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
joints.forEach((joint, index) => {
|
||||||
|
const parent = joint["joint.parentIndex"];
|
||||||
|
|
||||||
|
(parent === JOINT_ROOT ? node : jointNodes[parent]).addChild(
|
||||||
|
jointNodes[index],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const skin = document
|
||||||
|
.createSkin()
|
||||||
|
.setInverseBindMatrices(
|
||||||
|
createAccessor(
|
||||||
|
document,
|
||||||
|
buffer,
|
||||||
|
"MAT4",
|
||||||
|
new Float32Array(
|
||||||
|
joints.flatMap((joint) => joint["joint.invBindPose"].flat()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
jointNodes.forEach((jointNode) => skin.addJoint(jointNode));
|
||||||
|
|
||||||
|
return {
|
||||||
|
skin,
|
||||||
|
joints: new Map(
|
||||||
|
joints.map((joint, index) => [joint["joint.uid"], jointNodes[index]]),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMotionClip(
|
||||||
|
document: Document,
|
||||||
|
buffer: Buffer,
|
||||||
|
clip: string,
|
||||||
|
joints: Map<string, Node>,
|
||||||
|
motion: Motion,
|
||||||
|
component: MotionComponent,
|
||||||
|
) {
|
||||||
|
const animation = document.createAnimation(clip);
|
||||||
|
const playbackRate = component["motion.playbackRate"];
|
||||||
|
const duration = motion.duration / playbackRate;
|
||||||
|
|
||||||
|
for (const [uid, track] of motion.tracks) {
|
||||||
|
const joint = joints.get(uid);
|
||||||
|
|
||||||
|
if (!joint) continue;
|
||||||
|
|
||||||
|
for (const [type, { times, values }] of track) {
|
||||||
|
const keyTimes = times.map((time) => time / playbackRate);
|
||||||
|
const keyValues =
|
||||||
|
type === MotionChannel.Scale
|
||||||
|
? values.flatMap((value) => [value, value, value])
|
||||||
|
: [...values];
|
||||||
|
|
||||||
|
// the game loops over the clip's duration, which can outlast its keys
|
||||||
|
if (keyTimes[keyTimes.length - 1] < duration) {
|
||||||
|
const size = keyValues.length / keyTimes.length;
|
||||||
|
|
||||||
|
keyTimes.push(duration);
|
||||||
|
keyValues.push(...keyValues.slice(-size));
|
||||||
|
}
|
||||||
|
|
||||||
|
const sampler = document
|
||||||
|
.createAnimationSampler()
|
||||||
|
.setInput(
|
||||||
|
createAccessor(
|
||||||
|
document,
|
||||||
|
buffer,
|
||||||
|
"SCALAR",
|
||||||
|
new Float32Array(keyTimes),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setOutput(
|
||||||
|
createAccessor(
|
||||||
|
document,
|
||||||
|
buffer,
|
||||||
|
type === MotionChannel.Rotation ? "VEC4" : "VEC3",
|
||||||
|
new Float32Array(keyValues),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setInterpolation("LINEAR");
|
||||||
|
|
||||||
|
animation
|
||||||
|
.addSampler(sampler)
|
||||||
|
.addChannel(
|
||||||
|
document
|
||||||
|
.createAnimationChannel()
|
||||||
|
.setTargetNode(joint)
|
||||||
|
.setTargetPath(MOTION_PATHS[type])
|
||||||
|
.setSampler(sampler),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a socket follow a joint. The game replaces the socket's own transform
|
||||||
|
* with the joint's, so it sits right on the joint.
|
||||||
|
*/
|
||||||
|
export function bindSocket(socket: Node, joint: Node, skin: Skin) {
|
||||||
|
joint.addChild(socket);
|
||||||
|
socket.setMatrix(new Matrix4().toArray() as mat4);
|
||||||
|
socket.traverse((child) => {
|
||||||
|
const childSkin = child.getSkin();
|
||||||
|
|
||||||
|
// skinned meshes carrying a copy of the owner's skeleton deform with it
|
||||||
|
if (
|
||||||
|
childSkin &&
|
||||||
|
isEqual(
|
||||||
|
childSkin.listJoints().map((joint) => joint.getName()),
|
||||||
|
skin.listJoints().map((joint) => joint.getName()),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
child.setSkin(skin);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinPrimitive(
|
||||||
|
document: Document,
|
||||||
|
buffer: Buffer,
|
||||||
|
primitive: Primitive,
|
||||||
|
attributes: Map<VertexAttribute, number[][]>,
|
||||||
|
targetsData: ArrayBuffer | undefined,
|
||||||
|
) {
|
||||||
|
const jointIndices =
|
||||||
|
attributes.get(VertexAttribute.JOINTINDEX) ??
|
||||||
|
attributes.get(VertexAttribute.HARD_JOINTINDEX);
|
||||||
|
|
||||||
|
if (!jointIndices || !targetsData) return false;
|
||||||
|
|
||||||
|
const targets = new Uint32Array(new Uint8Array(targetsData).slice().buffer);
|
||||||
|
const weights =
|
||||||
|
attributes.get(VertexAttribute.JOINTWEIGHT) ?? jointIndices.map(() => [1]);
|
||||||
|
|
||||||
|
primitive
|
||||||
|
.setAttribute(
|
||||||
|
"JOINTS_0",
|
||||||
|
createAccessor(
|
||||||
|
document,
|
||||||
|
buffer,
|
||||||
|
"VEC4",
|
||||||
|
new Uint16Array(
|
||||||
|
jointIndices.flatMap((indices) =>
|
||||||
|
times(4, (index) => {
|
||||||
|
if (index >= indices.length) return 0;
|
||||||
|
|
||||||
|
const target = targets[indices[index]];
|
||||||
|
|
||||||
|
if (target === undefined) {
|
||||||
|
throw new Error(`Missing joint target ${indices[index]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setAttribute(
|
||||||
|
"WEIGHTS_0",
|
||||||
|
createAccessor(
|
||||||
|
document,
|
||||||
|
buffer,
|
||||||
|
"VEC4",
|
||||||
|
new Float32Array(
|
||||||
|
weights.flatMap((vertexWeights) => {
|
||||||
|
const padded = times(4, (index) => vertexWeights[index] ?? 0);
|
||||||
|
// some game weights sum below 1; glTF requires normalized weights
|
||||||
|
const sum = padded.reduce((total, weight) => total + weight, 0);
|
||||||
|
|
||||||
|
return padded.map((weight) => (sum > 0 ? weight / sum : weight));
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -5,24 +5,24 @@ import { jsxTree } from "./jsxTree";
|
|||||||
type Mergers = Parameters<typeof jsxTree>[1];
|
type Mergers = Parameters<typeof jsxTree>[1];
|
||||||
|
|
||||||
// names get sanitized and made unique on load
|
// names get sanitized and made unique on load
|
||||||
function nameOf(object3d: Object3D): string {
|
export function nameOf(object3d: Object3D): string {
|
||||||
return object3d.userData.name ?? object3d.name;
|
return object3d.userData.name ?? object3d.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* skins are layered in order, so an attachment's states win over the
|
* skins are layered in order, so an attachment's states win over the
|
||||||
* camouflage's
|
* camouflage's; `primitive` renders customization parts as they are so their
|
||||||
|
* bones can animate
|
||||||
*/
|
*/
|
||||||
export function skinTree(node: Object3D, skins: Skin[], mergers: Mergers) {
|
export function skinTree(
|
||||||
const skinNodes = new Map<string, Object3D[]>();
|
node: Object3D,
|
||||||
|
skins: Skin[],
|
||||||
|
mergers: NonNullable<Mergers>,
|
||||||
|
) {
|
||||||
const materials = new Map<string, Material>();
|
const materials = new Map<string, Material>();
|
||||||
const states: Record<string, string | null> = {};
|
const states: Record<string, string | null> = {};
|
||||||
|
|
||||||
for (const skin of skins) {
|
for (const skin of skins) {
|
||||||
for (const node of skin.scene.children) {
|
|
||||||
skinNodes.set(node.name, [...(skinNodes.get(node.name) ?? []), node]);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [name, material] of skin.materials) {
|
for (const [name, material] of skin.materials) {
|
||||||
materials.set(name, material);
|
materials.set(name, material);
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ export function skinTree(node: Object3D, skins: Skin[], mergers: Mergers) {
|
|||||||
Object.assign(states, skin.scene.userData.states);
|
Object.assign(states, skin.scene.userData.states);
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchChildren(object3d: Object3D, childMergers: Mergers) {
|
function switchChildren(object3d: Object3D) {
|
||||||
if (object3d.userData.state === undefined) return;
|
if (object3d.userData.state === undefined) return;
|
||||||
|
|
||||||
const name = nameOf(object3d);
|
const name = nameOf(object3d);
|
||||||
@@ -38,37 +38,31 @@ export function skinTree(node: Object3D, skins: Skin[], mergers: Mergers) {
|
|||||||
|
|
||||||
return object3d.children
|
return object3d.children
|
||||||
.filter((child) => nameOf(child) === state)
|
.filter((child) => nameOf(child) === state)
|
||||||
.map((child) => jsxTree(child, childMergers));
|
.map((child) => jsxTree(child, skinMergers));
|
||||||
}
|
}
|
||||||
|
|
||||||
const partMergers: Mergers = {
|
|
||||||
...mergers,
|
|
||||||
group(object3d, props, key) {
|
|
||||||
return (
|
|
||||||
<group key={key} {...props}>
|
|
||||||
{switchChildren(object3d, partMergers) ?? props.children}
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const skinMergers: Mergers = {
|
const skinMergers: Mergers = {
|
||||||
mesh(mesh, props, key) {
|
mesh(mesh, props, key) {
|
||||||
const material = materials.get((mesh.material as Material).name);
|
const material = materials.get((mesh.material as Material).name);
|
||||||
|
|
||||||
return mergers!.mesh!(
|
return mergers.mesh!(
|
||||||
mesh,
|
mesh,
|
||||||
material ? { ...props, material } : props,
|
material ? { ...props, material } : props,
|
||||||
key,
|
key,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
group(object3d, props, key) {
|
group(object3d, props, key) {
|
||||||
const parts = skinNodes.get(object3d.name) ?? [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group key={key} {...props}>
|
<group key={key} {...props}>
|
||||||
{switchChildren(object3d, skinMergers) ?? props.children}
|
{switchChildren(object3d) ?? props.children}
|
||||||
{parts.flatMap((part) =>
|
{skins.flatMap((skin) =>
|
||||||
part.children.map((child) => jsxTree(child, partMergers)),
|
(skin.parts.get(object3d.name) ?? []).map((part) =>
|
||||||
|
mergers.primitive ? (
|
||||||
|
mergers.primitive(part, part.uuid)
|
||||||
|
) : (
|
||||||
|
<primitive key={part.uuid} object={part} />
|
||||||
|
),
|
||||||
|
),
|
||||||
)}
|
)}
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { parseHierarchies, type Sc2ReadStream } from "@blitzkit/core";
|
import {
|
||||||
import { times } from "lodash-es";
|
parseHierarchies,
|
||||||
|
type Hierarchy,
|
||||||
|
type Sc2ReadStream,
|
||||||
|
} from "@blitzkit/core";
|
||||||
|
import { sortBy, times } from "lodash-es";
|
||||||
|
|
||||||
interface Value {
|
interface Value {
|
||||||
"any.type": string;
|
"any.type": string;
|
||||||
@@ -17,12 +21,15 @@ interface ScriptNode {
|
|||||||
|
|
||||||
interface Script {
|
interface Script {
|
||||||
nodes: Record<string, ScriptNode>;
|
nodes: Record<string, ScriptNode>;
|
||||||
|
variables?: Record<string, Value>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Action {
|
interface Action {
|
||||||
"act.delay": number;
|
"act.delay": number;
|
||||||
"act.entityName": string;
|
"act.entityName": string;
|
||||||
"act.event": number;
|
"act.event": number;
|
||||||
|
"act.eventToTrigger": string;
|
||||||
|
"act.stopAfterNRepeats": number;
|
||||||
"act.switchIndex": number;
|
"act.switchIndex": number;
|
||||||
"act.type": number;
|
"act.type": number;
|
||||||
"act.userEventId": string;
|
"act.userEventId": string;
|
||||||
@@ -32,12 +39,17 @@ interface Entity {
|
|||||||
name: string;
|
name: string;
|
||||||
parent?: Entity;
|
parent?: Entity;
|
||||||
children: Entity[];
|
children: Entity[];
|
||||||
|
hierarchy?: Hierarchy;
|
||||||
|
repeats?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SELF_ENTITY = "*** Self ***";
|
const SELF_ENTITY = "*** Self ***";
|
||||||
const SCENE_LOADED_EVENT = 1;
|
const SCENE_LOADED_EVENT = 1;
|
||||||
const USER_EVENT = 2;
|
const USER_EVENT = 2;
|
||||||
|
const ANIMATION_END_EVENTS = [3, 8];
|
||||||
|
const START_ANIMATION_ACTION = 6;
|
||||||
const SWITCH_ACTIONS = [8, 9];
|
const SWITCH_ACTIONS = [8, 9];
|
||||||
|
const TRIGGER_EVENT_ACTION = 10;
|
||||||
// what the game fires as a tank is shown in the garage, in order
|
// what the game fires as a tank is shown in the garage, in order
|
||||||
const LOAD_EVENTS = ["Added", "TankAliveInHangar", "TankIsShownInNewScreen"];
|
const LOAD_EVENTS = ["Added", "TankAliveInHangar", "TankIsShownInNewScreen"];
|
||||||
const MAX_DEPTH = 64;
|
const MAX_DEPTH = 64;
|
||||||
@@ -72,25 +84,141 @@ function resolveTarget(owner: Entity, entityName: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* replays what picks each state switcher's state as a tank is shown in the
|
* replays what the game does as a tank is shown in the garage, with the camo's
|
||||||
* garage, with the camo's preset applied and its items attached; delayed
|
* preset applied and its items attached; `animate` follows customization parts
|
||||||
* actions are skipped so states stay as they are when the tank appears
|
* through their motions, delays and alarms until they settle
|
||||||
*/
|
*/
|
||||||
export function switcherStates(
|
function replay(
|
||||||
sc2: ReturnType<Sc2ReadStream["sc2"]>,
|
sc2: ReturnType<Sc2ReadStream["sc2"]>,
|
||||||
preset: string | undefined,
|
preset: string | undefined,
|
||||||
attachments: Map<string, string[]>,
|
attachments: Map<string, string[]>,
|
||||||
|
animate: boolean,
|
||||||
) {
|
) {
|
||||||
const scripts: Record<string, Script> = {};
|
const scripts: Record<string, Script> = {};
|
||||||
|
const root: Entity = { name: "", children: [] };
|
||||||
const stateNames = new Map<Entity, string[]>();
|
const stateNames = new Map<Entity, string[]>();
|
||||||
const scenarios: { owner: Entity; script: Script }[] = [];
|
const scenarios: {
|
||||||
|
owner: Entity;
|
||||||
|
script: Script;
|
||||||
|
variables: Map<string, unknown>;
|
||||||
|
}[] = [];
|
||||||
const actions: { owner: Entity; action: Action }[] = [];
|
const actions: { owner: Entity; action: Action }[] = [];
|
||||||
const states: Record<string, string | null> = {};
|
// names can repeat, so states are kept per entity
|
||||||
|
const states = new Map<Entity, string | null>();
|
||||||
|
const looping = new Set<Entity>();
|
||||||
|
const finished = new Set<Entity>();
|
||||||
|
const pending: (() => void)[] = [];
|
||||||
|
const fired = new Set<unknown>();
|
||||||
|
|
||||||
function activateState(entity: Entity | undefined, state: number) {
|
function activateState(entity: Entity | undefined, state: number) {
|
||||||
const names = entity && stateNames.get(entity);
|
const names = entity && stateNames.get(entity);
|
||||||
|
|
||||||
if (names) states[entity.name] = names[state] ?? null;
|
if (!names) return;
|
||||||
|
|
||||||
|
const previous = states.get(entity);
|
||||||
|
|
||||||
|
states.set(entity, names[state] ?? null);
|
||||||
|
|
||||||
|
if (!animate || previous === states.get(entity)) return;
|
||||||
|
|
||||||
|
for (const scenario of scenarios) {
|
||||||
|
if (scenario.owner === entity) {
|
||||||
|
runScenario(scenario, "State Activated", state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isShown(entity: Entity) {
|
||||||
|
for (let child = entity; child.parent; child = child.parent) {
|
||||||
|
if (
|
||||||
|
stateNames.get(child.parent)?.includes(child.name) &&
|
||||||
|
states.get(child.parent) !== child.name
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAnimation(entity: Entity, repeats?: number) {
|
||||||
|
if (entity.repeats === undefined) {
|
||||||
|
for (const child of entity.children) startAnimation(child, repeats);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (looping.has(entity)) return;
|
||||||
|
|
||||||
|
looping.add(entity);
|
||||||
|
|
||||||
|
// some clips restart themselves when they end, so each finishes once
|
||||||
|
if ((repeats ?? entity.repeats) > 0 && !finished.has(entity)) {
|
||||||
|
pending.push(() => finishAnimation(entity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishAnimation(entity: Entity) {
|
||||||
|
const statesBefore = new Map(states);
|
||||||
|
const shownBefore = [...looping].filter(isShown);
|
||||||
|
|
||||||
|
looping.delete(entity);
|
||||||
|
finished.add(entity);
|
||||||
|
|
||||||
|
for (const { owner, action } of actions) {
|
||||||
|
if (
|
||||||
|
owner === entity &&
|
||||||
|
ANIMATION_END_EVENTS.includes(action["act.event"])
|
||||||
|
) {
|
||||||
|
runAction(owner, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (
|
||||||
|
let ancestor: Entity | undefined = entity;
|
||||||
|
ancestor;
|
||||||
|
ancestor = ancestor.parent
|
||||||
|
) {
|
||||||
|
const owned = scenarios.filter(({ owner }) => owner === ancestor);
|
||||||
|
|
||||||
|
for (const scenario of owned) {
|
||||||
|
runScenario(scenario, "Motion Fragment Stop");
|
||||||
|
runScenario(scenario, "Motion Sequence Stop");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (owned.length > 0) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// as in game, a clip stops on whatever it switched to or revealed; one that
|
||||||
|
// changes nothing is the only animation there is, so it keeps looping
|
||||||
|
if (
|
||||||
|
[...states].every(
|
||||||
|
([other, state]) => statesBefore.get(other) === state,
|
||||||
|
) &&
|
||||||
|
[...looping].every(
|
||||||
|
(other) => !isShown(other) || shownBefore.includes(other),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
looping.add(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runAction(owner: Entity, action: Action) {
|
||||||
|
const target = resolveTarget(owner, action["act.entityName"]);
|
||||||
|
|
||||||
|
if (SWITCH_ACTIONS.includes(action["act.type"])) {
|
||||||
|
// what shows for a while in game stays, as its animation loops here
|
||||||
|
if (action["act.delay"] > 0 && action["act.switchIndex"] < 0) return;
|
||||||
|
|
||||||
|
activateState(target, action["act.switchIndex"]);
|
||||||
|
} else if (animate && action["act.type"] === START_ANIMATION_ACTION) {
|
||||||
|
if (target) {
|
||||||
|
const repeats = action["act.stopAfterNRepeats"];
|
||||||
|
|
||||||
|
startAnimation(target, repeats > 0 ? repeats : undefined);
|
||||||
|
}
|
||||||
|
} else if (animate && action["act.type"] === TRIGGER_EVENT_ACTION) {
|
||||||
|
fireEvent(action["act.eventToTrigger"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const component of Object.values(
|
for (const component of Object.values(
|
||||||
@@ -103,9 +231,14 @@ export function switcherStates(
|
|||||||
|
|
||||||
parseHierarchies<Entity>(
|
parseHierarchies<Entity>(
|
||||||
sc2["#hierarchy"],
|
sc2["#hierarchy"],
|
||||||
{ name: "", children: [] },
|
root,
|
||||||
(hierarchy, components, parent) => {
|
(hierarchy, components, parent) => {
|
||||||
const entity: Entity = { name: hierarchy.name, parent, children: [] };
|
const entity: Entity = {
|
||||||
|
name: hierarchy.name,
|
||||||
|
parent,
|
||||||
|
children: [],
|
||||||
|
hierarchy,
|
||||||
|
};
|
||||||
|
|
||||||
parent.children.push(entity);
|
parent.children.push(entity);
|
||||||
|
|
||||||
@@ -122,14 +255,24 @@ export function switcherStates(
|
|||||||
);
|
);
|
||||||
|
|
||||||
stateNames.set(entity, names);
|
stateNames.set(entity, names);
|
||||||
states[entity.name] = names[component["ssc.activeState"]] ?? null;
|
states.set(entity, names[component["ssc.activeState"]] ?? null);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "ScenarioComponent": {
|
case "ScenarioComponent": {
|
||||||
const script = scripts[component["scenario.scriptId"]];
|
const script = scripts[component["scenario.scriptId"]];
|
||||||
|
|
||||||
if (script) scenarios.push({ owner: entity, script });
|
if (script) {
|
||||||
|
scenarios.push({
|
||||||
|
owner: entity,
|
||||||
|
script,
|
||||||
|
variables: new Map(
|
||||||
|
Object.entries(script.variables ?? {}).map(
|
||||||
|
([name, variable]) => [name, variable["any.value"]],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +284,12 @@ export function switcherStates(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "MotionComponent":
|
||||||
|
if (animate) {
|
||||||
|
entity.repeats = component["simpleMotion.repeatsCount"];
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,9 +298,11 @@ export function switcherStates(
|
|||||||
);
|
);
|
||||||
|
|
||||||
function runScenario(
|
function runScenario(
|
||||||
{ owner, script }: (typeof scenarios)[number],
|
scenario: (typeof scenarios)[number],
|
||||||
event: string,
|
event: string,
|
||||||
|
value?: number,
|
||||||
) {
|
) {
|
||||||
|
const { owner, script, variables } = scenario;
|
||||||
const { nodes } = script;
|
const { nodes } = script;
|
||||||
const links = (id: string) => Object.values(nodes[id].connections ?? {});
|
const links = (id: string) => Object.values(nodes[id].connections ?? {});
|
||||||
|
|
||||||
@@ -168,30 +319,78 @@ export function switcherStates(
|
|||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "ScenarioSceneInfoNode":
|
case "ScenarioSceneInfoNode":
|
||||||
return node["scenarioSceneInfoNode.infoKind"] === "GetCurrentEntity"
|
switch (node["scenarioSceneInfoNode.infoKind"]) {
|
||||||
? owner
|
case "GetCurrentEntity":
|
||||||
: undefined;
|
return owner;
|
||||||
|
case "GetCurrentScene":
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
|
||||||
case "ScenarioGetMemberNode":
|
case "ScenarioGetVarNode":
|
||||||
return node["scenarioGetMemberNode.fieldName"] === "parent" &&
|
return variables.get(node["scenarioGetVarNode.varPath"] as string);
|
||||||
pin === "parent"
|
|
||||||
? (input(id, "object") as Entity | undefined)?.parent
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
case "ScenarioFunctionNode":
|
case "ScenarioSetVarNode":
|
||||||
switch (
|
return variables.get(node["scenarioSetVarNode.varPath"] as string);
|
||||||
`${node["scenarioFunctionNode.className"]}.${node["scenarioFunctionNode.functionName"]}`
|
|
||||||
) {
|
case "ScenarioGetMemberNode": {
|
||||||
|
const object = input(id, "object") as Entity | undefined;
|
||||||
|
|
||||||
|
switch (node["scenarioGetMemberNode.fieldName"]) {
|
||||||
|
case "parent":
|
||||||
|
return pin === "parent" ? object?.parent : undefined;
|
||||||
|
case "name":
|
||||||
|
return object?.name;
|
||||||
|
case "stateSwitcherVisibility":
|
||||||
|
return object && isShown(object);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "ScenarioFunctionNode": {
|
||||||
|
const name = node["scenarioFunctionNode.functionName"] as string;
|
||||||
|
const number = (pin: string) => Number(input(id, pin) ?? 0);
|
||||||
|
|
||||||
|
switch (`${node["scenarioFunctionNode.className"]}.${name}`) {
|
||||||
case "Entity.FindChildByName": {
|
case "Entity.FindChildByName": {
|
||||||
const entity = input(id, "entity") as Entity | undefined;
|
const entity = input(id, "entity") as Entity | undefined;
|
||||||
return entity && findChild(entity, input(id, "name") as string);
|
return entity && findChild(entity, input(id, "name") as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "Entity.GetVisibility": {
|
||||||
|
const entity = input(id, "arg0") as Entity | undefined;
|
||||||
|
return entity ? isShown(entity) : false;
|
||||||
|
}
|
||||||
|
|
||||||
case "TankComponent.GetAppliedSkinMaterialConfigName":
|
case "TankComponent.GetAppliedSkinMaterialConfigName":
|
||||||
return preset;
|
return preset;
|
||||||
|
|
||||||
case "String.Compare":
|
case "String.Compare":
|
||||||
return input(id, "1") === input(id, "2");
|
return input(id, "1") === input(id, "2");
|
||||||
|
|
||||||
|
case "String.Merge":
|
||||||
|
// graphs use Separator as the prefix and Postfix as the number
|
||||||
|
return ["Suffix", "Separator", "Postfix"]
|
||||||
|
.map((pin) => input(id, pin) ?? "")
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
case "bool.Or":
|
||||||
|
return Boolean(input(id, "a")) || Boolean(input(id, "b"));
|
||||||
|
|
||||||
|
case "Random.Random":
|
||||||
|
case "Random.RandomFloat":
|
||||||
|
case "TankComponent.GetSpeed":
|
||||||
|
case "TankComponent.GetTurretAngle":
|
||||||
|
case "TankComponent.GetGunAngle":
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "==") return number("a") === number("b");
|
||||||
|
if (name === ">") return number("a") > number("b");
|
||||||
|
if (name === ">=") return number("a") >= number("b");
|
||||||
|
if (name === "<=") return number("a") <= number("b");
|
||||||
|
if (name.startsWith("Add")) return number("a") + number("b");
|
||||||
|
if (name.startsWith("Subtract")) return number("a") - number("b");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,13 +405,41 @@ export function switcherStates(
|
|||||||
pin = "event";
|
pin = "event";
|
||||||
} else if (type === "ScenarioBranchNode") {
|
} else if (type === "ScenarioBranchNode") {
|
||||||
pin = input(id, "condition") ? "true" : "false";
|
pin = input(id, "condition") ? "true" : "false";
|
||||||
} else if (
|
} else if (type === "ScenarioSetVarNode") {
|
||||||
node["scenarioFunctionNode.functionName"] === "ActivateState"
|
variables.set(
|
||||||
) {
|
node["scenarioSetVarNode.varPath"] as string,
|
||||||
activateState(
|
input(id, "set"),
|
||||||
input(id, "entity") as Entity | undefined,
|
|
||||||
Number(input(id, "state")),
|
|
||||||
);
|
);
|
||||||
|
} else if (type === "ScenarioSetAlarmClockNode") {
|
||||||
|
const entity = (input(id, "entity") as Entity | undefined) ?? owner;
|
||||||
|
|
||||||
|
// alarms re-arm themselves
|
||||||
|
if (animate && !fired.has(node)) {
|
||||||
|
fired.add(node);
|
||||||
|
pending.push(() => {
|
||||||
|
for (const other of scenarios) {
|
||||||
|
if (other.owner === entity) runScenario(other, "Alarm Clock");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const entity = input(id, "entity") as Entity | undefined;
|
||||||
|
|
||||||
|
switch (node["scenarioFunctionNode.functionName"]) {
|
||||||
|
case "ActivateState":
|
||||||
|
activateState(entity, Number(input(id, "state")));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "StartAnimation":
|
||||||
|
if (animate && entity) {
|
||||||
|
startAnimation(entity, Number(input(id, "repeats") ?? 0));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "TriggerCustomEvent":
|
||||||
|
if (animate) fireEvent(input(id, "event name") as string);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const link of links(id)) {
|
for (const link of links(id)) {
|
||||||
@@ -227,29 +454,87 @@ export function switcherStates(
|
|||||||
? node.defaults?.["Event Name"]?.["any.value"]
|
? node.defaults?.["Event Name"]?.["any.value"]
|
||||||
: node["ScenarioEventNode.eventName"];
|
: node["ScenarioEventNode.eventName"];
|
||||||
|
|
||||||
if (type === "ScenarioEventNode" && name === event) execute(id, 0);
|
if (
|
||||||
|
type === "ScenarioEventNode" &&
|
||||||
|
name === event &&
|
||||||
|
(event !== "State Activated" ||
|
||||||
|
Number(node.defaults?.State?.["any.value"] ?? 0) === value)
|
||||||
|
) {
|
||||||
|
execute(id, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const event of LOAD_EVENTS) {
|
function fireEvent(event: string) {
|
||||||
for (const { owner, action } of actions) {
|
if (fired.has(event)) return;
|
||||||
|
|
||||||
|
fired.add(event);
|
||||||
|
|
||||||
|
// the game runs delayed actions once their delay is up, so they go in that
|
||||||
|
// order rather than the order they're stored in
|
||||||
|
for (const { owner, action } of sortBy(
|
||||||
|
actions,
|
||||||
|
({ action }) => action["act.delay"],
|
||||||
|
)) {
|
||||||
if (
|
if (
|
||||||
action["act.delay"] === 0 &&
|
(animate || action["act.delay"] === 0) &&
|
||||||
SWITCH_ACTIONS.includes(action["act.type"]) &&
|
|
||||||
(event === LOAD_EVENTS[0]
|
(event === LOAD_EVENTS[0]
|
||||||
? action["act.event"] === SCENE_LOADED_EVENT
|
? action["act.event"] === SCENE_LOADED_EVENT
|
||||||
: action["act.event"] === USER_EVENT &&
|
: action["act.event"] === USER_EVENT &&
|
||||||
action["act.userEventId"] === event)
|
action["act.userEventId"] === event)
|
||||||
) {
|
) {
|
||||||
activateState(
|
runAction(owner, action);
|
||||||
resolveTarget(owner, action["act.entityName"]),
|
|
||||||
action["act.switchIndex"],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const scenario of scenarios) runScenario(scenario, event);
|
for (const scenario of scenarios) runScenario(scenario, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const event of LOAD_EVENTS) fireEvent(event);
|
||||||
|
|
||||||
|
for (let run = pending.shift(); run; run = pending.shift()) run();
|
||||||
|
|
||||||
|
return {
|
||||||
|
states: new Map(
|
||||||
|
[...states].map(([entity, state]) => [
|
||||||
|
entity.hierarchy as Hierarchy,
|
||||||
|
state,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
motions: new Set(
|
||||||
|
[...looping]
|
||||||
|
.filter(isShown)
|
||||||
|
.map((entity) => entity.hierarchy as Hierarchy),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* picks each state switcher's state; delayed actions are skipped so states stay
|
||||||
|
* as they are when the tank appears
|
||||||
|
*/
|
||||||
|
export function switcherStates(
|
||||||
|
sc2: ReturnType<Sc2ReadStream["sc2"]>,
|
||||||
|
preset: string | undefined,
|
||||||
|
attachments: Map<string, string[]>,
|
||||||
|
) {
|
||||||
|
const states: Record<string, string | null> = {};
|
||||||
|
|
||||||
|
for (const [hierarchy, state] of replay(sc2, preset, attachments, false)
|
||||||
|
.states) {
|
||||||
|
states[hierarchy.name] = state;
|
||||||
|
}
|
||||||
|
|
||||||
return states;
|
return states;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* where a customization part ends up: each state switcher's state and the
|
||||||
|
* motions left looping
|
||||||
|
*/
|
||||||
|
export function partGarage(
|
||||||
|
sc2: ReturnType<Sc2ReadStream["sc2"]>,
|
||||||
|
preset: string | undefined,
|
||||||
|
) {
|
||||||
|
return replay(sc2, preset, new Map(), true);
|
||||||
|
}
|
||||||
@@ -1,13 +1,25 @@
|
|||||||
import { alias } from "@blitzkit/core";
|
import { alias } from "@blitzkit/core";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import type { Group, Material } from "three";
|
import {
|
||||||
|
AnimationMixer,
|
||||||
|
Mesh,
|
||||||
|
SkinnedMesh,
|
||||||
|
type Group,
|
||||||
|
type Material,
|
||||||
|
type Object3D,
|
||||||
|
} from "three";
|
||||||
import { GLTFLoader } from "three-stdlib";
|
import { GLTFLoader } from "three-stdlib";
|
||||||
import { TRANSITION_ANIMATION_TIME } from "../components/Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
import { TRANSITION_ANIMATION_TIME } from "../components/Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
||||||
|
import { prepareMaterial } from "../core/blitzkit/jsxTree";
|
||||||
|
import { nameOf } from "../core/blitzkit/skinTree";
|
||||||
import { Tankopedia } from "../stores/tankopedia";
|
import { Tankopedia } from "../stores/tankopedia";
|
||||||
|
|
||||||
export interface Skin {
|
export interface Skin {
|
||||||
scene: Group;
|
scene: Group;
|
||||||
materials: Map<string, Material>;
|
materials: Map<string, Material>;
|
||||||
|
/** customization parts by the tank node they attach to */
|
||||||
|
parts: Map<string, Object3D[]>;
|
||||||
|
mixer?: AnimationMixer;
|
||||||
}
|
}
|
||||||
|
|
||||||
const skins = new Map<string, Promise<Skin>>();
|
const skins = new Map<string, Promise<Skin>>();
|
||||||
@@ -23,13 +35,46 @@ async function loadSkin(path: string) {
|
|||||||
.filter((material) => material.userData.camouflage)
|
.filter((material) => material.userData.camouflage)
|
||||||
.map((material) => [material.name, material]),
|
.map((material) => [material.name, material]),
|
||||||
),
|
),
|
||||||
|
// taken now because rendering moves the parts out of the scene
|
||||||
|
parts: new Map(
|
||||||
|
gltf.scene.children.map((target) => [target.name, [...target.children]]),
|
||||||
|
),
|
||||||
|
mixer:
|
||||||
|
gltf.animations.length > 0 ? new AnimationMixer(gltf.scene) : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// bound now for the same reason, every exported clip loops in the garage
|
||||||
|
for (const clip of gltf.animations) skin.mixer!.clipAction(clip).play();
|
||||||
|
|
||||||
|
for (const parts of skin.parts.values()) {
|
||||||
|
for (const part of parts) preparePart(part);
|
||||||
|
}
|
||||||
|
|
||||||
loadedSkins.set(path, skin);
|
loadedSkins.set(path, skin);
|
||||||
|
|
||||||
return skin;
|
return skin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parts render as they are, so what jsxTree and skinTree would do happens here
|
||||||
|
function preparePart(part: Object3D) {
|
||||||
|
part.traverse((object) => {
|
||||||
|
if (object.userData.state !== undefined) {
|
||||||
|
for (const child of object.children) {
|
||||||
|
child.visible = nameOf(child) === object.userData.state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(object instanceof Mesh)) return;
|
||||||
|
|
||||||
|
object.castShadow = true;
|
||||||
|
object.receiveShadow = true;
|
||||||
|
// bounds are from the bind pose
|
||||||
|
if (object instanceof SkinnedMesh) object.frustumCulled = false;
|
||||||
|
|
||||||
|
prepareMaterial(object.material);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function skinPath(id: number, skin: number) {
|
function skinPath(id: number, skin: number) {
|
||||||
return alias("models_legendary", `/${id}/skins/${skin}.glb`);
|
return alias("models_legendary", `/${id}/skins/${skin}.glb`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { invalidate, useFrame } from "@react-three/fiber";
|
||||||
|
import type { Skin } from "./useSkin";
|
||||||
|
|
||||||
|
export function useSkinAnimation(skins: Skin[]) {
|
||||||
|
useFrame((_, delta) => {
|
||||||
|
for (const { mixer } of skins) {
|
||||||
|
if (!mixer) continue;
|
||||||
|
|
||||||
|
mixer.update(delta);
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -172,13 +172,7 @@ export async function GET({ props }: APIContext<{ skin: number }>) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const part = await extractModel(vfs, path, true);
|
const part = await extractModel(vfs, path, true, preset);
|
||||||
const partSc2 = new Sc2ReadStream(
|
|
||||||
(await vfs.file(`Data/3d/${path}.sc2`)).buffer as ArrayBuffer,
|
|
||||||
).sc2();
|
|
||||||
|
|
||||||
Object.assign(states, switcherStates(partSc2, preset, new Map()));
|
|
||||||
|
|
||||||
const map = mergeDocuments(document, part);
|
const map = mergeDocuments(document, part);
|
||||||
const partScene = map.get(part.getRoot().listScenes()[0]) as Scene;
|
const partScene = map.get(part.getRoot().listScenes()[0]) as Scene;
|
||||||
const node = document
|
const node = document
|
||||||
|
|||||||
@@ -8,18 +8,35 @@ import {
|
|||||||
toUniqueId,
|
toUniqueId,
|
||||||
VertexAttribute,
|
VertexAttribute,
|
||||||
type ConfigArchive,
|
type ConfigArchive,
|
||||||
|
type Hierarchy,
|
||||||
type TankParameters,
|
type TankParameters,
|
||||||
type Textures,
|
type Textures,
|
||||||
type VehicleDefinitionList,
|
type VehicleDefinitionList,
|
||||||
} from "@blitzkit/core";
|
} from "@blitzkit/core";
|
||||||
import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
|
import type { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
|
||||||
import { Document, Material, Node, NodeIO, Scene } from "@gltf-transform/core";
|
import {
|
||||||
|
Document,
|
||||||
|
Material,
|
||||||
|
Node,
|
||||||
|
NodeIO,
|
||||||
|
Scene,
|
||||||
|
type Skin,
|
||||||
|
} from "@gltf-transform/core";
|
||||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||||
import { dedup, prune } from "@gltf-transform/functions";
|
import { dedup, prune } from "@gltf-transform/functions";
|
||||||
import type { APIContext } from "astro";
|
import type { APIContext } from "astro";
|
||||||
import { isEqual, times } from "lodash-es";
|
import { isEqual, times } from "lodash-es";
|
||||||
import { dirname } from "path";
|
import { dirname } from "path";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
|
import type { Motion } from "../../../core/blitzkit/skinAnimations/motion";
|
||||||
|
import {
|
||||||
|
bindSocket,
|
||||||
|
createMotionClip,
|
||||||
|
createSkeleton,
|
||||||
|
loadMotion,
|
||||||
|
skinPrimitive,
|
||||||
|
} from "../../../core/blitzkit/skinAnimations/skeleton";
|
||||||
|
import { partGarage } from "../../../core/blitzkit/switcherStates";
|
||||||
import { vfs } from "../../../core/blitzkit/vfs";
|
import { vfs } from "../../../core/blitzkit/vfs";
|
||||||
import {
|
import {
|
||||||
vertexAttributeGLTFName,
|
vertexAttributeGLTFName,
|
||||||
@@ -79,6 +96,7 @@ export async function extractModel(
|
|||||||
vfs: AbstractVFS,
|
vfs: AbstractVFS,
|
||||||
path: string,
|
path: string,
|
||||||
isPart = false,
|
isPart = false,
|
||||||
|
preset?: string,
|
||||||
) {
|
) {
|
||||||
const sc2Path = `Data/3d/${path}.sc2`;
|
const sc2Path = `Data/3d/${path}.sc2`;
|
||||||
const scgPath = `Data/3d/${path}.scg`;
|
const scgPath = `Data/3d/${path}.scg`;
|
||||||
@@ -92,6 +110,35 @@ export async function extractModel(
|
|||||||
const scene = document.createScene();
|
const scene = document.createScene();
|
||||||
const buffer = document.createBuffer();
|
const buffer = document.createBuffer();
|
||||||
const materials = await extractMaterials(document, sc2, path);
|
const materials = await extractMaterials(document, sc2, path);
|
||||||
|
const skeletons = new Map<Hierarchy, ReturnType<typeof createSkeleton>>();
|
||||||
|
const jointBindings: [
|
||||||
|
owner: Node,
|
||||||
|
entity: string,
|
||||||
|
joint: Node,
|
||||||
|
skin: Skin,
|
||||||
|
][] = [];
|
||||||
|
const motions = new Map<Hierarchy, Motion>();
|
||||||
|
const garage = isPart ? partGarage(sc2, preset) : undefined;
|
||||||
|
|
||||||
|
if (isPart) {
|
||||||
|
const loading: Promise<void>[] = [];
|
||||||
|
|
||||||
|
parseHierarchies(sc2["#hierarchy"], null, (hierarchy, components) => {
|
||||||
|
for (const component of components) {
|
||||||
|
if (component["comp.typename"] !== "MotionComponent") continue;
|
||||||
|
|
||||||
|
loading.push(
|
||||||
|
loadMotion(vfs, path, component).then((motion) => {
|
||||||
|
motions.set(hierarchy, motion);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(loading);
|
||||||
|
}
|
||||||
|
|
||||||
parseHierarchies<Scene | Node>(
|
parseHierarchies<Scene | Node>(
|
||||||
sc2["#hierarchy"],
|
sc2["#hierarchy"],
|
||||||
@@ -105,6 +152,40 @@ export async function extractModel(
|
|||||||
|
|
||||||
const node = document.createNode(hierarchy.name);
|
const node = document.createNode(hierarchy.name);
|
||||||
|
|
||||||
|
if (isPart) {
|
||||||
|
const skeleton = components.find(
|
||||||
|
(component) => component["comp.typename"] === "SkeletonComponent",
|
||||||
|
);
|
||||||
|
const motionComponent = components.find(
|
||||||
|
(component) => component["comp.typename"] === "MotionComponent",
|
||||||
|
);
|
||||||
|
const motion = motions.get(hierarchy);
|
||||||
|
|
||||||
|
if (skeleton) {
|
||||||
|
const created = createSkeleton(
|
||||||
|
document,
|
||||||
|
buffer,
|
||||||
|
node,
|
||||||
|
skeleton,
|
||||||
|
motion,
|
||||||
|
);
|
||||||
|
|
||||||
|
skeletons.set(hierarchy, created);
|
||||||
|
|
||||||
|
// only what loops in the garage is exported
|
||||||
|
if (motion && motionComponent && garage?.motions.has(hierarchy)) {
|
||||||
|
createMotionClip(
|
||||||
|
document,
|
||||||
|
buffer,
|
||||||
|
`motion${document.getRoot().listAnimations().length}`,
|
||||||
|
created.joints,
|
||||||
|
motion,
|
||||||
|
motionComponent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
components.forEach((component) => {
|
components.forEach((component) => {
|
||||||
switch (component["comp.typename"]) {
|
switch (component["comp.typename"]) {
|
||||||
case "LodComponent":
|
case "LodComponent":
|
||||||
@@ -128,10 +209,36 @@ export async function extractModel(
|
|||||||
|
|
||||||
case "StateSwitcherComponent":
|
case "StateSwitcherComponent":
|
||||||
node.setExtras({
|
node.setExtras({
|
||||||
state: component[`ssc.state${component["ssc.activeState"]}`],
|
// a part's scenarios can pick its state as it loads
|
||||||
|
state: garage
|
||||||
|
? (garage.states.get(hierarchy) ?? null)
|
||||||
|
: component[`ssc.state${component["ssc.activeState"]}`],
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "SkeletonJointBindingComponent": {
|
||||||
|
const skeleton = skeletons.get(hierarchy);
|
||||||
|
|
||||||
|
if (!skeleton) break;
|
||||||
|
|
||||||
|
times(component.count, (index) => {
|
||||||
|
const binding = component.map[
|
||||||
|
index.toString().padStart(4, "0")
|
||||||
|
] as unknown as { entityUid: string; jointUid: string };
|
||||||
|
const joint = skeleton.joints.get(binding.jointUid);
|
||||||
|
|
||||||
|
if (!joint) throw new Error(`Missing joint ${binding.jointUid}`);
|
||||||
|
|
||||||
|
jointBindings.push([
|
||||||
|
node,
|
||||||
|
binding.entityUid,
|
||||||
|
joint,
|
||||||
|
skeleton.skin,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "RenderComponent": {
|
case "RenderComponent": {
|
||||||
const renderObject = component["rc.renderObj"];
|
const renderObject = component["rc.renderObj"];
|
||||||
|
|
||||||
@@ -200,10 +307,24 @@ export async function extractModel(
|
|||||||
primitive.setAttribute(name, attributeAccessor);
|
primitive.setAttribute(name, attributeAccessor);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const skeleton = skeletons.get(hierarchy);
|
||||||
const mesh = document
|
const mesh = document
|
||||||
.createMesh(batch["##name"])
|
.createMesh(batch["##name"])
|
||||||
.addPrimitive(primitive);
|
.addPrimitive(primitive);
|
||||||
lodNode.setMesh(mesh);
|
lodNode.setMesh(mesh);
|
||||||
|
|
||||||
|
if (
|
||||||
|
skeleton &&
|
||||||
|
skinPrimitive(
|
||||||
|
document,
|
||||||
|
buffer,
|
||||||
|
primitive,
|
||||||
|
attributes,
|
||||||
|
renderObject[`skinnedObject.batch${batchIndex}.targetsData`],
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
lodNode.setSkin(skeleton.skin);
|
||||||
|
}
|
||||||
node.addChild(lodNode);
|
node.addChild(lodNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -226,6 +347,20 @@ export async function extractModel(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
for (const [owner, entity, joint, skin] of jointBindings) {
|
||||||
|
// the game only binds the skeleton entity's own children
|
||||||
|
const socket = owner
|
||||||
|
.listChildren()
|
||||||
|
.find(
|
||||||
|
(child) =>
|
||||||
|
child.getName() === entity && !skin.listJoints().includes(child),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!socket) throw new Error(`Missing socket ${entity}`);
|
||||||
|
|
||||||
|
bindSocket(socket, joint, skin);
|
||||||
|
}
|
||||||
|
|
||||||
scene.addChild(document.createNode("test"));
|
scene.addChild(document.createNode("test"));
|
||||||
|
|
||||||
await document.transform(prune({ keepAttributes: true }), dedup());
|
await document.transform(prune({ keepAttributes: true }), dedup());
|
||||||
|
|||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
cd packages/scripts
|
||||||
|
bun src/protos.ts
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
cd packages/website
|
||||||
|
bun x --bun astro build
|
||||||
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
rm -rf \
|
||||||
|
packages/website/.astro \
|
||||||
|
packages/website/node_modules/.astro \
|
||||||
|
packages/website/node_modules/.vite
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
cd packages/website
|
||||||
|
bun x --bun astro dev
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
cd packages/scripts
|
||||||
|
bun src/discoverIds/index.ts
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
cd packages/scripts
|
||||||
|
bun src/downloadClient/index.ts
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
cd packages/closed
|
||||||
|
bun src/dava/downloadSmartDLC.ts
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
cd packages/website
|
||||||
|
bun x --bun astro check
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
cd packages/website
|
||||||
|
bun x --bun astro preview
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
cd packages/scripts
|
||||||
|
bun src/recordPerformance/index.ts
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
chmod +x scripts/*.sh
|
||||||
|
bun install
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
cd packages/scripts
|
||||||
|
bun src/vfs/index.ts
|
||||||
Reference in new issue
Block a user