Skip to content

Commit 5df0f43

Browse files
authored
lr2hook: add fast-slow support (#1367)
* build(client): add 'lintfix' target matching the server * docs(lr2hook): suggest lr2_chainload instead of BokutachiLauncher.exe BokutachiLauncher.exe is no longer a part of BokutachiHook. * refactor(server/lr2hook): G-ATTACK, P-ATTACK and HAZARD are not HARD * feat(server/lr2hook): parse extendedJudgements * feat(server/lr2hook): use fast-slow and early/late judgements * test(server/lr2hook): make clear that in this test notesPlayed != notesTotal * feat(server/lr2hook): calculate BP by hand when exiting chart early * feat(server/lr2hook): parse rseed * feat(server/lr2hook): parse extendedHpGraphs * feat(server/lr2hook): parse unixTimestamp * feat(server/lr2hook): use unixTimestamp as score play time Especially useful as BokutachiHook now retries sending scores after some time on connections errors. * feat(common/bms+server/lr2hook+docs): add gaugeHistory{Easy,Groove,Hard} Could also save graphs for unimplemented gauges, not done here as I don't know if they are ever going to be implemented while they take some decent space. * feat(client/bms): support several gauges Ugly IIFE inside IIFE is work-around for eslint bug `Expected a 'break' statement before 'case'`.
1 parent 6bfd051 commit 5df0f43

File tree

14 files changed

+412
-34
lines changed

14 files changed

+412
-34
lines changed

client/Justfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@ start:
99
# Test that the client passes typechecking and linting.
1010
test:
1111
pnpm typecheck
12-
pnpm lint
12+
pnpm lint
13+
14+
# Clean up the code and fix any automatically fixable mistakes.
15+
lintfix:
16+
pnpm lint-fix

client/src/app/pages/dashboard/import/LR2HookPage.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export default function LR2HookPage() {
2525
Download the latest version of the LR2Hook{" "}
2626
<ExternalLink href="https://github.com/MatVeiQaaa/BokutachiHook/releases">
2727
here
28+
</ExternalLink>{" "}
29+
and lr2_chainload{" "}
30+
<ExternalLink href="https://github.com/SayakaIsBaka/lr2_chainload/releases">
31+
here
2832
</ExternalLink>
2933
.
3034
</li>
@@ -39,14 +43,18 @@ export default function LR2HookPage() {
3943
Place this file in the same folder as <b>LR2Body.exe</b>.
4044
</li>
4145
<li>
42-
That's it! Launch the game with <code>BokutachiLauncher.exe</code> and start
43-
playing, your scores will automatically submit to the server.
46+
Create file named <b>chainload.txt</b> in the same folder as <b>LR2Body.exe</b>{" "}
47+
and add new line "BokutachiHook.dll" into it.
48+
</li>
49+
<li>
50+
That's it! Launch the game as usual, your scores will automatically submit to
51+
the server.
4452
</li>
4553
</ol>
4654
<Divider />
4755
<Muted>
4856
Note: If you submit a score on a chart that {TachiConfig.NAME} doesn't recognise,
49-
you'll need to wait until atleast 2 other players submit scores for that chart
57+
you'll need to wait until at least 2 other players submit scores for that chart
5058
before it'll show up. This is to combat accidental IR spam.
5159
</Muted>
5260
</div>

client/src/components/tables/dropdowns/components/BMSScoreDropdownParts.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,24 @@ export function BMSGraphsComponent({
1313
score: ScoreDocument<"bms:7K" | "bms:14K"> | PBScoreDocument<"bms:7K" | "bms:14K">;
1414
chart: ChartDocument<"bms:7K" | "bms:14K">;
1515
}) {
16-
const gaugeHistory = score.scoreData.optional.gaugeHistory;
17-
1816
const [lamp, setLamp] = useState<LampTypes>(LampToKey(score));
1917

18+
let gaugeStatus: "none" | "single" | "gas" = "none";
19+
20+
if (
21+
score.scoreData.optional.gaugeHistoryEasy &&
22+
score.scoreData.optional.gaugeHistoryGroove &&
23+
score.scoreData.optional.gaugeHistoryHard
24+
) {
25+
gaugeStatus = "gas";
26+
} else if (score.scoreData.optional.gaugeHistory) {
27+
gaugeStatus = "single";
28+
}
29+
2030
const shouldDisable = (r: LampTypes) => {
21-
if (gaugeHistory) {
31+
if (gaugeStatus === "gas") {
32+
return false;
33+
} else if (gaugeStatus === "single") {
2234
return r !== LampToKey(score);
2335
}
2436

@@ -29,6 +41,28 @@ export function BMSGraphsComponent({
2941
setLamp(LampToKey(score));
3042
}, [score]);
3143

44+
const gaugeHistory = (() => {
45+
switch (gaugeStatus) {
46+
case "gas":
47+
return (() => {
48+
switch (lamp) {
49+
case "Normal":
50+
return score.scoreData.optional.gaugeHistoryGroove!;
51+
case "Easy":
52+
return score.scoreData.optional.gaugeHistoryEasy!;
53+
case "Hard":
54+
return score.scoreData.optional.gaugeHistoryHard!;
55+
case "EXHard":
56+
return null;
57+
}
58+
})();
59+
case "single":
60+
return score.scoreData.optional.gaugeHistory!;
61+
case "none":
62+
return null;
63+
}
64+
})();
65+
3266
return (
3367
<>
3468
<div className="col-12 d-flex justify-content-center">

common/src/config/game-support/bms.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,21 @@ export const BMS_7K_CONF = {
230230
description:
231231
"A snapshot of the gauge percent throughout the chart. The values should be null from the point the user dies until the end of the chart.",
232232
},
233+
gaugeHistoryEasy: {
234+
type: "GRAPH",
235+
validate: p.isBetween(0, 100),
236+
description: "The easy gauge history.",
237+
},
238+
gaugeHistoryGroove: {
239+
type: "GRAPH",
240+
validate: p.isBetween(0, 100),
241+
description: "The groove gauge history.",
242+
},
243+
gaugeHistoryHard: {
244+
type: "GRAPH",
245+
validate: p.isBetween(0, 100),
246+
description: "The hard gauge history.",
247+
},
233248
epg: {
234249
type: "INTEGER",
235250
validate: p.isPositive,

docs/docs/game-support/games/bms-7K.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ For more information on what metrics are and how they work, see [TODO]!
3333
| `bp` | Integer | The total bads + poors in this score. |
3434
| `gauge` | Decimal | The life in percent (between 0 and 100) that was on the gauge at the end of the chart. |
3535
| `gaugeHistory` | Array&lt;Decimal&gt; | A snapshot of the gauge percent throughout the chart. The values should be null from the point the user dies until the end of the chart. |
36+
| `gaugeHistoryEasy` | Array&lt;Decimal&gt; | The easy gauge history. |
37+
| `gaugeHistoryGroove` | Array&lt;Decimal&gt; | The groove gauge history. |
38+
| `gaugeHistoryHard` | Array&lt;Decimal&gt; | The hard gauge history. |
3639
| `epg` | Integer | The amount of early PGreats in this score. |
3740
| `egr` | Integer | The amount of early greats in this score. |
3841
| `egd` | Integer | The amount of early goods in this score. |

server/src/game-implementations/games/bms-pms.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ const BMS_PMS_MERGERS: Array<PBMergeFunction<GPTStrings["bms" | "pms"]>> = [
2222
// legal value for these properties it works out.
2323
base.scoreData.optional.gauge = lamp.scoreData.optional.gauge;
2424
base.scoreData.optional.gaugeHistory = lamp.scoreData.optional.gaugeHistory;
25+
base.scoreData.optional.gaugeHistoryEasy = lamp.scoreData.optional.gaugeHistoryEasy;
26+
base.scoreData.optional.gaugeHistoryGroove = lamp.scoreData.optional.gaugeHistoryGroove;
27+
base.scoreData.optional.gaugeHistoryHard = lamp.scoreData.optional.gaugeHistoryHard;
2528
}),
2629
CreatePBMergeFor("smallest", "optional.bp", "Lowest BP", (base, bp) => {
2730
base.scoreData.optional.bp = bp.scoreData.optional.bp;

server/src/lib/score-import/framework/pb/create-pb-doc.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
} from "test-utils/test-data";
1414
import type { PBScoreDocumentNoRank } from "./create-pb-doc";
1515
import type { KtLogger } from "lib/logger/logger";
16-
import type { ScoreDocument } from "tachi-common";
1716

1817
const IIDXScore = TestingIIDXSPScore;
1918

@@ -231,6 +230,9 @@ t.test("#CreatePBDoc", (t) => {
231230
bp: 15,
232231
gauge: 12,
233232
gaugeHistory: [20, 20, 21, 12],
233+
gaugeHistoryEasy: [20, 20, 21, 13],
234+
gaugeHistoryGroove: [20, 20, 21, 14],
235+
gaugeHistoryHard: [20, 20, 21, 15],
234236
},
235237
enumIndexes: {
236238
lamp: IIDX_LAMPS.FULL_COMBO,
@@ -254,6 +256,9 @@ t.test("#CreatePBDoc", (t) => {
254256
{
255257
gauge: 12,
256258
gaugeHistory: [20, 20, 21, 12],
259+
gaugeHistoryEasy: [20, 20, 21, 13],
260+
gaugeHistoryGroove: [20, 20, 21, 14],
261+
gaugeHistoryHard: [20, 20, 21, 15],
257262
},
258263
"Should select the lampPBs gauge data and not the score PBs."
259264
);

server/src/lib/score-import/import-types/ir/lr2hook/converter.test.ts

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import t from "tap";
44
import { dmf } from "test-utils/misc";
55
import ResetDBState from "test-utils/resets";
66
import { TestingLR2HookScore } from "test-utils/test-data";
7+
import { ApplyNTimes } from "utils/misc";
78

89
const logger = CreateLogCtx(__filename);
910

@@ -13,7 +14,84 @@ t.test("#ConverterLR2Hook", (t) => {
1314
t.test("Should match a score with its song and chart.", async (t) => {
1415
const res = await ConverterLR2Hook(
1516
TestingLR2HookScore,
16-
{ timeReceived: 10 },
17+
{ timeReceived: 10_000 },
18+
"ir/lr2hook",
19+
logger
20+
);
21+
22+
t.hasStrict(res, {
23+
song: {
24+
id: 27339,
25+
},
26+
chart: {
27+
chartID: "88eb6cc5683e2740cbd07f588a5f3db1db8d467b",
28+
data: {
29+
hashMD5: TestingLR2HookScore.md5,
30+
},
31+
},
32+
dryScore: {
33+
timeAchieved: 10_000,
34+
scoreData: {
35+
score: TestingLR2HookScore.scoreData.exScore,
36+
optional: {
37+
bp: 56,
38+
epg: undefined,
39+
lpg: undefined,
40+
egr: undefined,
41+
lgr: undefined,
42+
egd: undefined,
43+
lgd: undefined,
44+
ebd: undefined,
45+
lbd: undefined,
46+
epr: undefined,
47+
lpr: undefined,
48+
fast: undefined,
49+
slow: undefined,
50+
},
51+
},
52+
game: "bms",
53+
importType: "ir/lr2hook",
54+
scoreMeta: {
55+
client: "LR2",
56+
},
57+
},
58+
});
59+
60+
t.end();
61+
});
62+
63+
t.test("Should use optional and extended properties.", async (t) => {
64+
const res = await ConverterLR2Hook(
65+
dmf(TestingLR2HookScore, {
66+
scoreData: {
67+
extendedJudgements: {
68+
epg: 1,
69+
lpg: 2,
70+
egr: 3,
71+
lgr: 4,
72+
egd: 5,
73+
lgd: 6,
74+
ebd: 7,
75+
lbd: 8,
76+
epr: 9,
77+
lpr: 10,
78+
cb: 11,
79+
fast: 12,
80+
slow: 13,
81+
notesPlayed: TestingLR2HookScore.scoreData.notesPlayed,
82+
},
83+
extendedHpGraphs: {
84+
groove: ApplyNTimes(1000, () => 1),
85+
hard: ApplyNTimes(1000, () => 2),
86+
hazard: ApplyNTimes(1000, () => 3),
87+
easy: ApplyNTimes(1000, () => 4),
88+
pattack: ApplyNTimes(1000, () => 5),
89+
gattack: ApplyNTimes(1000, () => 6),
90+
},
91+
},
92+
unixTimestamp: 8,
93+
} as any),
94+
{ timeReceived: 10_000 },
1795
"ir/lr2hook",
1896
logger
1997
);
@@ -29,10 +107,26 @@ t.test("#ConverterLR2Hook", (t) => {
29107
},
30108
},
31109
dryScore: {
110+
timeAchieved: 8_000,
32111
scoreData: {
33112
score: TestingLR2HookScore.scoreData.exScore,
34113
optional: {
35114
bp: 56,
115+
epg: 1,
116+
lpg: 2,
117+
egr: 3,
118+
lgr: 4,
119+
egd: 5,
120+
lgd: 6,
121+
ebd: 7,
122+
lbd: 8,
123+
epr: 9,
124+
lpr: 10,
125+
fast: 12,
126+
slow: 13,
127+
gaugeHistoryGroove: ApplyNTimes(1000, () => 1),
128+
gaugeHistoryHard: ApplyNTimes(1000, () => 2),
129+
gaugeHistoryEasy: ApplyNTimes(1000, () => 4),
36130
},
37131
},
38132
game: "bms",
@@ -50,7 +144,7 @@ t.test("#ConverterLR2Hook", (t) => {
50144
const res = await ConverterLR2Hook(
51145
dmf(TestingLR2HookScore, {
52146
scoreData: {
53-
notesPlayed: 10,
147+
notesPlayed: TestingLR2HookScore.scoreData.notesTotal - 1,
54148
},
55149
} as any),
56150
{ timeReceived: 10 },
@@ -86,6 +180,52 @@ t.test("#ConverterLR2Hook", (t) => {
86180
t.end();
87181
});
88182

183+
t.test(
184+
"Should calculate BP if the score was exited early but there is extendedJudgements.",
185+
async (t) => {
186+
const res = await ConverterLR2Hook(
187+
dmf(TestingLR2HookScore, {
188+
scoreData: {
189+
notesPlayed: TestingLR2HookScore.scoreData.notesTotal - 1,
190+
extendedJudgements: {
191+
notesPlayed: TestingLR2HookScore.scoreData.notesTotal - 1,
192+
},
193+
},
194+
} as any),
195+
{ timeReceived: 10 },
196+
"ir/lr2hook",
197+
logger
198+
);
199+
200+
t.hasStrict(res, {
201+
song: {
202+
id: 27339,
203+
},
204+
chart: {
205+
chartID: "88eb6cc5683e2740cbd07f588a5f3db1db8d467b",
206+
data: {
207+
hashMD5: TestingLR2HookScore.md5,
208+
},
209+
},
210+
dryScore: {
211+
scoreData: {
212+
score: TestingLR2HookScore.scoreData.exScore,
213+
optional: {
214+
bp: 57,
215+
},
216+
},
217+
game: "bms",
218+
importType: "ir/lr2hook",
219+
scoreMeta: {
220+
client: "LR2",
221+
},
222+
},
223+
});
224+
225+
t.end();
226+
}
227+
);
228+
89229
t.test("Should throw an error if song or chart can't be found.", (t) => {
90230
t.rejects(
91231
() =>

0 commit comments

Comments
 (0)