Skip to content

Commit a83c332

Browse files
committed
Initial commit
1 parent dc0b5e3 commit a83c332

18 files changed

+5441
-2
lines changed

.eslintignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/dist
2+
*.js

.eslintrc.js

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const offTsRules = [
2+
'strict-boolean-expressions',
3+
'prefer-nullish-coalescing',
4+
'no-non-null-assertion',
5+
'promise-function-async',
6+
'explicit-function-return-type',
7+
'no-floating-promises',
8+
'no-misused-promises',
9+
'restrict-plus-operands',
10+
'no-explicit-any',
11+
'no-empty-function',
12+
'no-confusing-void-expression',
13+
];
14+
15+
module.exports = {
16+
root: true,
17+
env: {
18+
node: true,
19+
},
20+
parser: '@typescript-eslint/parser',
21+
extends: [
22+
'standard-with-typescript',
23+
'eslint:recommended',
24+
'plugin:@typescript-eslint/recommended',
25+
'plugin:import/recommended',
26+
'plugin:import/typescript',
27+
'prettier',
28+
],
29+
parserOptions: {
30+
ecmaVersion: 'latest',
31+
sourceType: 'module',
32+
project: './tsconfig.json',
33+
createDefaultProgram: true,
34+
},
35+
settings: {
36+
'import/resolver': {
37+
typescript: true,
38+
node: true,
39+
},
40+
},
41+
rules: {
42+
...Object.fromEntries(offTsRules.map(name => [`@typescript-eslint/${name}`, 'off'])),
43+
'@typescript-eslint/consistent-type-imports': ['warn', { disallowTypeAnnotations: false }],
44+
'import/order': [
45+
'warn',
46+
{
47+
alphabetize: {
48+
order: 'asc',
49+
caseInsensitive: true,
50+
},
51+
},
52+
],
53+
}
54+
};

.github/workflows/publish.yml

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Publish
2+
3+
on:
4+
push:
5+
branches:
6+
- '!*'
7+
tags:
8+
- 'v*'
9+
10+
jobs:
11+
publish:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@v3
16+
- name: Install dependencies
17+
run: yarn install --frozen-lockfile
18+
- name: Build
19+
run: yarn build
20+
- name: Publish
21+
run: npm publish
22+
env:
23+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
24+
- name: Release
25+
uses: marvinpinto/action-automatic-releases@latest
26+
with:
27+
repo_token: ${{ secrets.BOT_TOKEN }}
28+
prerelease: false

.prettierrc

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"printWidth": 100,
3+
"singleQuote": true,
4+
"trailingComma": "all",
5+
"arrowParens": "avoid",
6+
"useTabs": false,
7+
"tabWidth": 2,
8+
"semi": true
9+
}

.typesyncrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"ignoreDeps": ["dev"]
3+
}

.vscode/settings.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"editor.codeActionsOnSave": {
3+
"source.fixAll.eslint": true
4+
},
5+
"editor.formatOnSave": true
6+
}

README.md

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,26 @@
1-
# unity-ts
2-
1+
# unity-js
2+
3+
[![NPM version](https://img.shields.io/npm/v/@arkntools/unity-js?style=flat-square)](https://www.npmjs.com/package/@arkntools/unity-js)
4+
5+
Unity ab 解包的 js 实现,抄自 [yuanyan3060/unity-rs](https://github.com/yuanyan3060/unity-rs)
6+
7+
仅做了项目所需的最低限度实现,如果需要较完整的功能建议还是去用大佬的 ↑
8+
9+
目前仅支持:
10+
11+
- TextAsset
12+
13+
```js
14+
import fs from 'fs';
15+
import { loadAssetBundle, AssetType } from '@arkntools/unity-js';
16+
17+
(async () => {
18+
const bundle = await loadAssetBundle(fs.createReadStream('character_table003334.ab'));
19+
bundle.objects().forEach(obj => {
20+
if (obj.type === AssetType.TextAsset) {
21+
const textAsset = obj.load();
22+
fs.writeFileSync(`${textAsset.name}.bytes`, textAsset.data);
23+
}
24+
});
25+
})();
26+
```

package.json

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "@arkntools/unity-js",
3+
"version": "1.0.0-alpha.0",
4+
"main": "./dist/index.js",
5+
"author": "神代綺凛 <[email protected]>",
6+
"license": "AGPL-3.0",
7+
"publishConfig": {
8+
"access": "public"
9+
},
10+
"files": [
11+
"dist"
12+
],
13+
"scripts": {
14+
"build": "rimraf dist && tsc",
15+
"lint:fix": "eslint . --ext .ts --fix",
16+
"postversion": "tpv"
17+
},
18+
"dependencies": {
19+
"async-binary-stream": "^1.0.2",
20+
"buffer-reader": "^0.1.0",
21+
"helpful-decorators": "^2.1.0",
22+
"lz4-napi": "^2.2.0"
23+
},
24+
"devDependencies": {
25+
"@tsuk1ko/postversion": "^1.0.2",
26+
"@types/buffer-reader": "^0.1.0",
27+
"@typescript-eslint/eslint-plugin": "^5.60.0",
28+
"@typescript-eslint/parser": "^5.60.0",
29+
"eslint": "^8.43.0",
30+
"eslint-config-prettier": "^8.8.0",
31+
"eslint-config-standard-with-typescript": "^35.0.0",
32+
"eslint-import-resolver-typescript": "^3.5.5",
33+
"eslint-plugin-import": "^2.27.5",
34+
"eslint-plugin-n": "^16.0.0",
35+
"eslint-plugin-promise": "^6.1.1",
36+
"jest": "^29.5.0",
37+
"lint-staged": "^13.2.2",
38+
"prettier": "^2.8.8",
39+
"rimraf": "^5.0.1",
40+
"ts-node": "^10.9.1",
41+
"typescript": "^5.1.3",
42+
"yorkie": "^2.0.0"
43+
},
44+
"gitHooks": {
45+
"pre-commit": "lint-staged"
46+
},
47+
"lint-staged": {
48+
"*.ts": [
49+
"eslint --fix",
50+
"prettier --write"
51+
]
52+
}
53+
}

src/asset.ts

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { bind } from 'helpful-decorators';
2+
import { createAssetObject } from './classes';
3+
import type { BufferReaderExtended } from './utils/reader';
4+
import { createExtendedBufferReader } from './utils/reader';
5+
6+
interface AssetHeader {
7+
metadataSize: number;
8+
fileSize: number;
9+
version: number;
10+
dataOffset: number;
11+
endianness: number;
12+
}
13+
14+
interface TypeInfo {
15+
classId: number;
16+
}
17+
18+
export interface ObjectInfo {
19+
getReader: () => BufferReaderExtended;
20+
buildType: string;
21+
assetVersion: number;
22+
bytesStart: number;
23+
bytesSize: number;
24+
typeId: number;
25+
classId: number;
26+
isDestroyed: number;
27+
stripped: number;
28+
pathId: number;
29+
version: string;
30+
}
31+
32+
export class Asset {
33+
private readonly reader: BufferReaderExtended;
34+
private readonly header: AssetHeader;
35+
private readonly fileEndianness: number = 0;
36+
private readonly unityVersion: string = '';
37+
private readonly targetPlatform: number = 0;
38+
private readonly enableTypeTree: boolean = false;
39+
private readonly enableBigId: boolean = false;
40+
private readonly types: TypeInfo[] = [];
41+
private readonly objectInfos: ObjectInfo[] = [];
42+
43+
constructor(data: Buffer) {
44+
const r = createExtendedBufferReader(data);
45+
this.reader = r;
46+
47+
const header: AssetHeader = (this.header = {
48+
metadataSize: r.nextUInt32BE(),
49+
fileSize: r.nextUInt32BE(),
50+
version: r.nextUInt32BE(),
51+
dataOffset: r.nextUInt32BE(),
52+
endianness: 0,
53+
});
54+
55+
if (header.version >= 9) {
56+
this.fileEndianness = header.endianness = r.nextUInt8();
57+
r.move(3);
58+
} else {
59+
r.seek(header.fileSize - header.metadataSize);
60+
this.fileEndianness = r.nextUInt8();
61+
}
62+
r.setEndianness(this.fileEndianness);
63+
64+
if (header.version >= 22) {
65+
header.metadataSize = r.nextUInt32();
66+
header.fileSize = r.nextInt64();
67+
header.dataOffset = r.nextInt64();
68+
r.move(8);
69+
}
70+
if (header.version >= 7) {
71+
this.unityVersion = r.nextStringZero();
72+
}
73+
if (header.version >= 8) {
74+
this.targetPlatform = r.nextInt32();
75+
}
76+
if (header.version >= 13) {
77+
this.enableTypeTree = !!r.nextUInt8();
78+
}
79+
80+
const typeCount = r.nextInt32();
81+
for (let i = 0; i < typeCount; i++) {
82+
this.readType(false);
83+
}
84+
85+
if (header.version >= 7 && header.version < 14) {
86+
this.enableBigId = !!r.nextInt32();
87+
}
88+
89+
const objectCount = r.nextUInt32();
90+
for (let i = 0; i < objectCount; i++) {
91+
const info: ObjectInfo = {
92+
getReader: this.cloneReader,
93+
buildType: '',
94+
assetVersion: 0,
95+
bytesStart: 0,
96+
bytesSize: 0,
97+
typeId: 0,
98+
classId: 0,
99+
isDestroyed: 0,
100+
stripped: 0,
101+
pathId: 0,
102+
version: '',
103+
};
104+
105+
if (this.enableBigId) r.move(8);
106+
else if (header.version < 14) r.move(4);
107+
else {
108+
r.align(4);
109+
r.move(8);
110+
}
111+
info.bytesStart = header.version >= 22 ? r.nextInt64() : r.nextUInt32();
112+
info.bytesStart += header.dataOffset;
113+
info.bytesSize = r.nextUInt32();
114+
info.typeId = r.nextInt32();
115+
if (header.version < 16) info.classId = r.nextUInt16();
116+
else info.classId = this.types[info.typeId].classId;
117+
if (header.version < 11) info.isDestroyed = r.nextUInt16();
118+
if (header.version >= 11 && header.version < 17) r.move(2);
119+
if (header.version === 15 || header.version === 16) info.stripped = r.nextUInt8();
120+
121+
this.objectInfos.push(info);
122+
}
123+
124+
// 未实现
125+
}
126+
127+
public objects() {
128+
return this.objectInfos.map(createAssetObject);
129+
}
130+
131+
@bind
132+
private cloneReader() {
133+
return this.reader.clone();
134+
}
135+
136+
// 未完整实现,只用于跳过
137+
private readType(isRefType: boolean) {
138+
const r = this.reader;
139+
const { version } = this.header;
140+
141+
const info: TypeInfo = {
142+
classId: r.nextInt32(),
143+
};
144+
145+
if (version >= 16) r.move(1);
146+
if (version >= 17) r.move(2);
147+
if (version >= 13) r.move(16);
148+
if (this.enableTypeTree) {
149+
if (version >= 12 || version === 10) this.readTypeTreeBlob();
150+
else throw new Error(`Unsupported asset version: ${version}`);
151+
if (version >= 21) {
152+
if (isRefType) {
153+
r.nextStringZero();
154+
r.nextStringZero();
155+
r.nextStringZero();
156+
} else {
157+
const size = r.nextInt32();
158+
r.move(size * 4);
159+
}
160+
}
161+
}
162+
163+
this.types.push(info);
164+
}
165+
166+
// 未实现,只用于跳过
167+
private readTypeTreeBlob() {
168+
const r = this.reader;
169+
170+
const nodeNumber = r.nextInt32();
171+
const stringBufferSize = r.nextInt32();
172+
173+
for (let i = 0; i < nodeNumber; i++) {
174+
r.move(24);
175+
if (this.header.version >= 19) r.move(8);
176+
}
177+
r.move(stringBufferSize);
178+
}
179+
}

0 commit comments

Comments
 (0)