fortnite-replay-analysis 1.0.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CSproj/bin/Release/net8.0/linux-x64/FortniteReplayAnalysis.dll +0 -0
- package/CSproj/bin/Release/net8.0/linux-x64/FortniteReplayAnalysis.pdb +0 -0
- package/CSproj/bin/Release/net8.0/linux-x64/publish/FortniteReplayAnalysis +0 -0
- package/CSproj/bin/Release/net8.0/linux-x64/publish/FortniteReplayAnalysis.pdb +0 -0
- package/CSproj/bin/Release/net8.0/win-x64/FortniteReplayAnalysis.dll +0 -0
- package/CSproj/bin/Release/net8.0/win-x64/FortniteReplayAnalysis.exe +0 -0
- package/CSproj/bin/Release/net8.0/win-x64/FortniteReplayAnalysis.pdb +0 -0
- package/CSproj/bin/Release/net8.0/win-x64/publish/FortniteReplayAnalysis.exe +0 -0
- package/CSproj/bin/Release/net8.0/win-x64/publish/FortniteReplayAnalysis.pdb +0 -0
- package/README.md +69 -74
- package/index.js +84 -54
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/README.md
CHANGED
|
@@ -1,117 +1,112 @@
|
|
|
1
1
|
# Fortnite Replay Analysis
|
|
2
2
|
|
|
3
|
-
Fortnite
|
|
3
|
+
FortniteのリプレイファイルをNode.jsで解析し、プレイヤーデータを取得・集計・ソートできるモジュールです。
|
|
4
4
|
|
|
5
5
|
## 特徴
|
|
6
6
|
|
|
7
|
-
* OS
|
|
8
|
-
* bot
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* OS判定でビルド済みの自己完結バイナリを呼び出し、高速に解析できます。
|
|
8
|
+
* botプレイヤーの除外や順位ソートのオプションに対応しています。
|
|
9
|
+
* 複数マッチのスコアをパーティ単位でマージして集計できます。
|
|
10
|
+
* 公式準拠のルールでスコアをソートできます。
|
|
11
11
|
|
|
12
12
|
## インストール
|
|
13
13
|
|
|
14
|
-
```
|
|
14
|
+
```bash
|
|
15
15
|
npm install fortnite-replay-analysis@latest
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## 使い方
|
|
19
19
|
|
|
20
|
+
以下は、1試合のリプレイ解析からスコア計算、複数マッチのマージまでを実行する例です。
|
|
21
|
+
|
|
20
22
|
```js
|
|
21
|
-
const {
|
|
23
|
+
const {
|
|
24
|
+
ReplayAnalysis,
|
|
25
|
+
calculateScore,
|
|
26
|
+
sortScores,
|
|
27
|
+
mergeScores
|
|
28
|
+
} = require('fortnite-replay-analysis');
|
|
22
29
|
|
|
23
30
|
(async () => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
// リプレイ解析(ディレクトリ指定時は最初に見つけた .replay を処理、ファイル指定時はそのファイルを使用)
|
|
32
|
+
const {
|
|
33
|
+
rawReplayData,
|
|
34
|
+
rawPlayerData,
|
|
35
|
+
processedPlayerInfo
|
|
36
|
+
} = await ReplayAnalysis(
|
|
37
|
+
'./path/to/replayDirOrFile',
|
|
38
|
+
{ bot: false, sort: true }
|
|
39
|
+
);
|
|
29
40
|
|
|
30
41
|
console.log('Raw Data:', rawPlayerData);
|
|
31
42
|
console.log('Processed Player Info:', processedPlayerInfo);
|
|
32
43
|
|
|
33
|
-
//
|
|
44
|
+
// 公式ルールでソート
|
|
34
45
|
const sortedScores = sortScores(processedPlayerInfo);
|
|
35
46
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
]);
|
|
47
|
+
// ポイント&キル計算
|
|
48
|
+
const score = await calculateScore({
|
|
49
|
+
matchData: processedPlayerInfo,
|
|
50
|
+
points: { 1: 11, 2: 6, 3: 5, 4: 4, 5: 3, 6: 2 },
|
|
51
|
+
killCountUpperLimit: 10, // 省略可能、デフォルト null(無制限)
|
|
52
|
+
killPointMultiplier: 1 // 1撃破あたりの倍率(1の場合1撃破1pt, 2の場合1撃破2ポイント)、省略可能、デフォルト 1
|
|
53
|
+
});
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
const finalSorted = sortScores(mergedScores);
|
|
55
|
+
console.log('Score:', score);
|
|
47
56
|
|
|
48
|
-
|
|
57
|
+
// 複数マッチのマージと再ソート
|
|
58
|
+
const merged = mergeScores([ sortedScores, sortedScores2 ]);
|
|
59
|
+
const finalSorted = sortScores(merged);
|
|
49
60
|
|
|
50
|
-
|
|
51
|
-
console.error(e);
|
|
52
|
-
}
|
|
61
|
+
console.log('Merged & Sorted:', finalSorted);
|
|
53
62
|
})();
|
|
54
63
|
```
|
|
55
64
|
|
|
56
|
-
## calculateScoreの使い方
|
|
57
|
-
|
|
58
|
-
リプレイ解析済みの`ReplayAnalysis` の `result.processedPlayerInfo` を保存した JSON ファイル(ファイル名は任意でOK)から、大会形式のスコアを計算したいときに使える。
|
|
59
|
-
|
|
60
|
-
```js
|
|
61
|
-
const { calculateScore } = require('fortnite-replay-analysis');
|
|
62
|
-
|
|
63
|
-
const score = await calculateScore({
|
|
64
|
-
matchDataPath: './output/matchA1/playerInfo.json',
|
|
65
|
-
points: {
|
|
66
|
-
1: 11, 2: 6, 3: 5, 4: 4, 5: 3,
|
|
67
|
-
6: 2, 7: 1, 8: 1, 9: 1, 10: 1
|
|
68
|
-
},
|
|
69
|
-
killPointMultiplier: 1,
|
|
70
|
-
killCountUpperLimit: 10
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
console.log(score);
|
|
74
|
-
```
|
|
75
|
-
|
|
76
65
|
## API
|
|
77
66
|
|
|
78
|
-
### `ReplayAnalysis(
|
|
67
|
+
### `ReplayAnalysis(inputPath, options)`
|
|
79
68
|
|
|
80
|
-
* `
|
|
81
|
-
* `options
|
|
69
|
+
* `inputPath`: .replayファイルがあるディレクトリまたはファイルのパス
|
|
70
|
+
* `options`(省略可):
|
|
82
71
|
|
|
83
|
-
* `bot`(boolean
|
|
84
|
-
* `sort`(boolean
|
|
85
|
-
*
|
|
72
|
+
* `bot`(boolean): botプレイヤーを含めるか(デフォルト: `false`)
|
|
73
|
+
* `sort`(boolean): 順位でソートするか(デフォルト: `true`)
|
|
74
|
+
* 戻り値: Promise<{
|
|
75
|
+
rawReplayData: Object,
|
|
76
|
+
rawPlayerData: Array,
|
|
77
|
+
processedPlayerInfo: Array
|
|
78
|
+
}>
|
|
86
79
|
|
|
87
|
-
### `
|
|
80
|
+
### `calculateScore({ matchData, points, killCountUpperLimit, killPointMultiplier })`
|
|
88
81
|
|
|
89
|
-
*
|
|
90
|
-
*
|
|
82
|
+
* `matchData`: `ReplayAnalysis`の`processedPlayerInfo`配列、またはそのJSONファイルへのパス
|
|
83
|
+
* `points`: 順位ごとのポイント設定オブジェクト(例: `{1:11,2:6,...}`)
|
|
84
|
+
* `killCountUpperLimit`: キル数の上限(省略可能、デフォルト: `null` で無制限)
|
|
85
|
+
* `killPointMultiplier`: 1撃破あたりの倍率(1の場合1撃破1pt, 2の場合1撃破2ポイント)、省略可能、デフォルト: `1`
|
|
86
|
+
* 戻り値: Promise(パーティごとの集計結果)
|
|
91
87
|
|
|
92
88
|
### `sortScores(scoreArray)`
|
|
93
89
|
|
|
94
|
-
*
|
|
95
|
-
*
|
|
90
|
+
* 公式準拠のルールでスコアをソートして返します。
|
|
91
|
+
* 引数: `calculateScore`や`mergeScores`の戻り値として得られる配列
|
|
92
|
+
* ソート順:
|
|
96
93
|
|
|
97
|
-
|
|
94
|
+
1. 累計ポイント降順
|
|
95
|
+
2. Victory Royale 回数降順
|
|
96
|
+
3. 平均撃破数降順
|
|
97
|
+
4. 平均順位昇順
|
|
98
|
+
5. 合計生存時間降順
|
|
99
|
+
6. 最初のパーティ番号昇順
|
|
98
100
|
|
|
99
|
-
|
|
100
|
-
* `points`:順位に対するポイント設定(例:{ 1: 11, 2: 6, ... })
|
|
101
|
-
* `killCountUpperLimit`:キル数制限(nullで無制限)
|
|
102
|
-
* `killPointMultiplier`:キル数倍率(例:1なら1キル1ポイント、2なら1キル2ポイント)
|
|
103
|
-
|
|
104
|
-
## 動作環境
|
|
101
|
+
### `mergeScores(scoreArrays)`
|
|
105
102
|
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
103
|
+
* 複数マッチ分のスコア配列をパーティ単位でマージします。
|
|
104
|
+
* 引数: ソート済みスコア配列の配列(例: `[sorted1, sorted2, ...]`)
|
|
105
|
+
* 戻り値: マージ後のスコア配列
|
|
109
106
|
|
|
110
107
|
## 注意事項
|
|
111
108
|
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
git cloneして新しく別リポジトリを作るのではなく、GitHub上のフォーク機能を使っていただけると、変更履歴を正しく追えます。
|
|
117
|
-
ご協力よろしくお願いします!
|
|
109
|
+
* ディレクトリ指定時は最初に見つけた `.replay` を処理します。
|
|
110
|
+
* 直接ファイルを指定した場合はそのファイルを処理し、`.replay` が存在しない場合でも最初に見つけたものを使用します。
|
|
111
|
+
* 問題が発生しても責任は負いかねます。
|
|
112
|
+
* フォークする場合はGitHubのFork機能を利用し、履歴を追いやすくしてください。
|
package/index.js
CHANGED
|
@@ -17,24 +17,27 @@ function getBinaryPath() { // OS判定して自己完結バイナリの実行フ
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function ReplayAnalysis(
|
|
20
|
+
function ReplayAnalysis(inputPath, { bot = false, sort = true } = {}) { // Fortniteのリプレイファイルを解析してプレイヤーデータを返す
|
|
21
21
|
return new Promise((resolve, reject) => {
|
|
22
22
|
|
|
23
|
-
let
|
|
23
|
+
let replayFilePath;
|
|
24
|
+
|
|
24
25
|
try {
|
|
25
|
-
|
|
26
|
+
const stat = fs.statSync(inputPath);
|
|
27
|
+
if (stat.isDirectory()) {
|
|
28
|
+
const replayFiles = fs.readdirSync(inputPath).filter(f => f.endsWith('.replay'));
|
|
29
|
+
if (replayFiles.length === 0) {
|
|
30
|
+
return reject(new Error(`No .replay files found in directory: ${inputPath}`));
|
|
31
|
+
}
|
|
32
|
+
replayFilePath = path.join(inputPath, replayFiles[0]);
|
|
33
|
+
} else if (stat.isFile()) {
|
|
34
|
+
replayFilePath = inputPath;
|
|
35
|
+
} else {
|
|
36
|
+
return reject(new Error(`Invalid input path: ${inputPath}`));
|
|
37
|
+
}
|
|
26
38
|
} catch (e) {
|
|
27
|
-
reject(new Error(`Failed to
|
|
28
|
-
return;
|
|
39
|
+
return reject(new Error(`Failed to access path: ${e.message}`));
|
|
29
40
|
}
|
|
30
|
-
|
|
31
|
-
if (replayFiles.length === 0) {
|
|
32
|
-
reject(new Error(`No replay file found in directory: ${replayFileDir}`));
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// とりあえず1個目のファイルを処理(複数ある場合は要拡張)
|
|
37
|
-
const replayFilePath = path.join(replayFileDir, replayFiles[0]);
|
|
38
41
|
const binPath = getBinaryPath();
|
|
39
42
|
|
|
40
43
|
execFile(binPath, [replayFilePath], (error, stdout, stderr) => {
|
|
@@ -46,8 +49,17 @@ function ReplayAnalysis(replayFileDir, { bot = false, sort = true } = {}) { // F
|
|
|
46
49
|
console.warn(`Warning: ${stderr}`);
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(stdout);
|
|
55
|
+
} catch (jsonErr) {
|
|
56
|
+
reject(new Error(`JSON parse error: ${jsonErr.message}`));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const playerData = parsed.PlayerData;
|
|
61
|
+
|
|
49
62
|
try {
|
|
50
|
-
const playerData = JSON.parse(stdout);
|
|
51
63
|
|
|
52
64
|
if (!Array.isArray(playerData)) {
|
|
53
65
|
reject(new Error(`Unexpected JSON format: playerData is not an array.`));
|
|
@@ -94,7 +106,9 @@ function ReplayAnalysis(replayFileDir, { bot = false, sort = true } = {}) { // F
|
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
resolve({
|
|
97
|
-
|
|
109
|
+
rawReplayData: parsed,
|
|
110
|
+
rawPlayerData: playerData,
|
|
111
|
+
processedPlayerInfo: filteredAndSortedPlayerInfo
|
|
98
112
|
});
|
|
99
113
|
} catch (jsonErr) {
|
|
100
114
|
reject(new Error(`JSON parse error: ${jsonErr.message}`));
|
|
@@ -103,18 +117,24 @@ function ReplayAnalysis(replayFileDir, { bot = false, sort = true } = {}) { // F
|
|
|
103
117
|
});
|
|
104
118
|
}
|
|
105
119
|
|
|
106
|
-
async function calculateScore({
|
|
107
|
-
if (!
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
throw new Error(
|
|
120
|
+
async function calculateScore({ matchData, points, killCountUpperLimit = null, killPointMultiplier = 1 } = {}) {
|
|
121
|
+
if (!matchData) throw new Error('matchData is required');
|
|
122
|
+
|
|
123
|
+
let playerInfo;
|
|
124
|
+
if (typeof matchData === 'string') {
|
|
125
|
+
if (!fs.existsSync(matchData)) throw new Error(`Match data path does not exist: ${matchData}`);
|
|
126
|
+
const rawData = fs.readFileSync(matchData, 'utf8');
|
|
127
|
+
try {
|
|
128
|
+
playerInfo = JSON.parse(rawData);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
throw new Error(`Failed to parse JSON from file: ${e.message}`);
|
|
131
|
+
}
|
|
112
132
|
}
|
|
113
|
-
if (
|
|
114
|
-
|
|
133
|
+
else if (Array.isArray(matchData)) playerInfo = matchData;
|
|
134
|
+
else {
|
|
135
|
+
throw new Error('matchData must be either a file path (string) or parsed JSON array');
|
|
115
136
|
}
|
|
116
137
|
|
|
117
|
-
const playerInfo = JSON.parse(fs.readFileSync(path.join(matchDataPath), 'utf8'));
|
|
118
138
|
const partyScore = playerInfo.reduce((acc, player) => {
|
|
119
139
|
if (!acc[player.partyNumber]) {
|
|
120
140
|
const limitedKills = killCountUpperLimit == null
|
|
@@ -157,39 +177,38 @@ function mergeScores(scoreArrays) { // 複数マッチの結果をマージし
|
|
|
157
177
|
const key = JSON.stringify([...p.partyMemberIdList].sort());
|
|
158
178
|
if (!map.has(key)) {
|
|
159
179
|
map.set(key, {
|
|
160
|
-
partyPlacement: null,
|
|
161
|
-
partyNumber: p.partyNumber,
|
|
162
180
|
partyScore: p.partyScore,
|
|
163
181
|
partyPoint: p.partyPoint,
|
|
164
182
|
partyKills: p.partyKills,
|
|
165
|
-
|
|
166
|
-
matchList: [p.matchName],
|
|
183
|
+
partyKillsNoLimit: p.partyKillsNoLimit,
|
|
167
184
|
partyVictoryRoyaleCount: p.partyVictoryRoyale ? 1 : 0,
|
|
185
|
+
matchList: [p.matchName],
|
|
186
|
+
partyMemberList: [...p.partyMemberList],
|
|
168
187
|
partyAliveTimeByMatch: [
|
|
169
188
|
{ match: p.matchName, times: [...(p.partyAliveTimeList || [])] }
|
|
170
189
|
],
|
|
171
|
-
partyPlacementList: [p.partyPlacement]
|
|
190
|
+
partyPlacementList: [p.partyPlacement],
|
|
191
|
+
matchs: { [p.matchName]: { ...p } }
|
|
172
192
|
});
|
|
173
193
|
} else {
|
|
174
194
|
const ex = map.get(key);
|
|
175
|
-
ex.matchList.push(p.matchName);
|
|
176
195
|
ex.partyScore += p.partyScore;
|
|
177
|
-
ex.partyKills += p.partyKills;
|
|
178
196
|
ex.partyPoint += p.partyPoint;
|
|
197
|
+
ex.partyKills += p.partyKills;
|
|
198
|
+
ex.partyKillsNoLimit += p.partyKillsNoLimit;
|
|
179
199
|
ex.partyVictoryRoyaleCount += p.partyVictoryRoyale ? 1 : 0;
|
|
200
|
+
ex.matchList.push(p.matchName);
|
|
180
201
|
ex.partyAliveTimeByMatch.push({
|
|
181
202
|
match: p.matchName,
|
|
182
203
|
times: [...(p.partyAliveTimeList || [])]
|
|
183
204
|
});
|
|
184
205
|
ex.partyPlacementList.push(p.partyPlacement);
|
|
206
|
+
ex.matchs[p.matchName] = { ...p };
|
|
185
207
|
}
|
|
186
208
|
})
|
|
187
209
|
);
|
|
188
210
|
|
|
189
|
-
return Array.from(map.values())
|
|
190
|
-
...p,
|
|
191
|
-
partyPlacement: p.partyPlacementList.reduce((a, b) => a + b, 0) / p.partyPlacementList.length
|
|
192
|
-
}));
|
|
211
|
+
return Array.from(map.values());
|
|
193
212
|
}
|
|
194
213
|
|
|
195
214
|
function sortScores(arr) { // 公式準拠のスコアソート関数
|
|
@@ -197,47 +216,58 @@ function sortScores(arr) { // 公式準拠のスコアソート関数
|
|
|
197
216
|
if (!Array.isArray(arr) || arr.length === 0) return arr;
|
|
198
217
|
|
|
199
218
|
arr.forEach(p => {
|
|
200
|
-
const matchCount = (p.
|
|
201
|
-
|
|
202
|
-
).length || 1;
|
|
203
|
-
p.result = {
|
|
219
|
+
const matchCount = (p.matchList || []).length || 1;
|
|
220
|
+
p.summary = {
|
|
204
221
|
point: p.partyScore || 0,
|
|
205
222
|
victoryCount: p.partyVictoryRoyaleCount ?? (p.partyVictoryRoyale ? 1 : 0),
|
|
206
223
|
matchCount,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
? (p.
|
|
210
|
-
: p.
|
|
224
|
+
// Decimalを使って計算
|
|
225
|
+
avgKills: matchCount > 0
|
|
226
|
+
? new Decimal(p.partyKills || 0).dividedBy(matchCount)
|
|
227
|
+
: new Decimal(p.partyKills || 0),
|
|
228
|
+
avgPlacement: Array.isArray(p.partyPlacementList) && p.partyPlacementList.length > 0 && matchCount > 0
|
|
229
|
+
? new Decimal(p.partyPlacementList.reduce((sum, val) => sum + val, 0)).dividedBy(matchCount)
|
|
230
|
+
: new Decimal(p.partyPlacement || 0),
|
|
211
231
|
totalAliveTime: sumMaxAliveTime(p.partyAliveTimeList, p.partyAliveTimeByMatch),
|
|
212
232
|
};
|
|
213
233
|
});
|
|
214
234
|
|
|
215
235
|
return arr.sort((a, b) => {
|
|
216
236
|
// 1. 累計獲得ポイント
|
|
217
|
-
if (b.
|
|
218
|
-
return b.
|
|
237
|
+
if (b.summary.point !== a.summary.point) {
|
|
238
|
+
return b.summary.point - a.summary.point;
|
|
219
239
|
}
|
|
220
240
|
// 2. セッション中の累計 Victory Royale 回数
|
|
221
|
-
if (b.
|
|
222
|
-
return b.
|
|
241
|
+
if (b.summary.victoryCount !== a.summary.victoryCount) {
|
|
242
|
+
return b.summary.victoryCount - a.summary.victoryCount;
|
|
223
243
|
}
|
|
224
244
|
|
|
225
245
|
// 3. 平均撃破数
|
|
226
|
-
|
|
227
|
-
|
|
246
|
+
const cmpAvgKills = b.summary.avgKills.comparedTo(a.summary.avgKills);
|
|
247
|
+
if (cmpAvgKills !== 0) {
|
|
248
|
+
return cmpAvgKills;
|
|
228
249
|
}
|
|
229
250
|
|
|
230
251
|
// 4. 平均順位(小さいほうが上位)
|
|
231
|
-
|
|
232
|
-
|
|
252
|
+
const cmpAvgPlacement = b.summary.avgPlacement.comparedTo(a.summary.avgPlacement);
|
|
253
|
+
if (cmpAvgPlacement !== 0) {
|
|
254
|
+
return cmpAvgPlacement;
|
|
233
255
|
}
|
|
234
256
|
|
|
235
257
|
// 5. 全マッチの合計生存時間
|
|
236
|
-
const
|
|
237
|
-
if (
|
|
258
|
+
const cmpTime = b.summary.totalAliveTime.comparedTo(a.summary.totalAliveTime);
|
|
259
|
+
if (cmpTime !== 0) {
|
|
260
|
+
return cmpTime;
|
|
261
|
+
}
|
|
238
262
|
|
|
239
263
|
// 6. 最終手段:1マッチ目のパーティ番号が小さい順
|
|
240
|
-
|
|
264
|
+
const numA = Array.isArray(a.matchList) && a.matchList.length > 0
|
|
265
|
+
? a.matchs[a.matchList[0]].partyNumber
|
|
266
|
+
: a.partyNumber;
|
|
267
|
+
const numB = Array.isArray(b.matchList) && b.matchList.length > 0
|
|
268
|
+
? b.matchs[b.matchList[0]].partyNumber
|
|
269
|
+
: b.partyNumber;
|
|
270
|
+
return numA - numB;
|
|
241
271
|
});
|
|
242
272
|
}
|
|
243
273
|
|