@hieuzest/koishi-plugin-mahjongpub 0.2.13 → 0.2.14
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/lib/index.js +205 -8
- package/lib/lark.d.ts +7 -0
- package/lib/manager.d.ts +16 -1
- package/package.json +14 -2
package/lib/index.js
CHANGED
|
@@ -336,6 +336,105 @@ var MahjongPub = class _MahjongPub {
|
|
|
336
336
|
}
|
|
337
337
|
};
|
|
338
338
|
|
|
339
|
+
// src/lark.tsx
|
|
340
|
+
var import_jsx_runtime = require("@satorijs/element/jsx-runtime");
|
|
341
|
+
var LarkExtension = class {
|
|
342
|
+
constructor(ctx, manager) {
|
|
343
|
+
this.ctx = ctx;
|
|
344
|
+
this.manager = manager;
|
|
345
|
+
ctx.command("mahjongpub.manager.manage").channelFields(["mahjongpub/bind-contest"]).action(async ({ session }) => {
|
|
346
|
+
const cid = +(session.channel["mahjongpub/bind-contest"] || 0);
|
|
347
|
+
if (!cid) return "Unauthorized.";
|
|
348
|
+
const cextra = this.manager.extra[cid] ??= new ContestExtra(ctx);
|
|
349
|
+
const contest = await this.manager.mahjongpub.getContest(cid);
|
|
350
|
+
const rounds = await this.manager.mahjongpub.getRounds(cid);
|
|
351
|
+
const classes = [];
|
|
352
|
+
for (const r of Object.values(rounds)) {
|
|
353
|
+
if (r.round !== (cextra.round ?? contest.c_round)) continue;
|
|
354
|
+
classes.push([r.t_class, await this.manager.getClsStatus(cid, r.t_class)]);
|
|
355
|
+
}
|
|
356
|
+
this.ctx.logger.info("Contest management statuses:", classes);
|
|
357
|
+
const stringifyStatus = /* @__PURE__ */ __name((status) => {
|
|
358
|
+
switch (status.status) {
|
|
359
|
+
case "pending":
|
|
360
|
+
return `<text_tag color='gray'>等待中</text_tag>`;
|
|
361
|
+
case "running":
|
|
362
|
+
return `<text_tag color='green'>进行中</text_tag>`;
|
|
363
|
+
case "finished":
|
|
364
|
+
return `<text_tag color='blue'>已结束</text_tag>`;
|
|
365
|
+
case "suspended":
|
|
366
|
+
return `<text_tag color='orange'>已停止</text_tag>`;
|
|
367
|
+
case "error":
|
|
368
|
+
return `<text_tag color='red'>错误</text_tag>`;
|
|
369
|
+
}
|
|
370
|
+
}, "stringifyStatus");
|
|
371
|
+
const renderCls = /* @__PURE__ */ __name((cls, status) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("lark:column-set", { backgroundStyle: "blue-50", children: [
|
|
372
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("lark:column", { weight: 3, width: "weighted", children: [
|
|
373
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
374
|
+
" 组",
|
|
375
|
+
cls + 1,
|
|
376
|
+
"-",
|
|
377
|
+
status.rowi,
|
|
378
|
+
" ",
|
|
379
|
+
stringifyStatus(status)
|
|
380
|
+
] }),
|
|
381
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("lark:column-set", { children: Array(4).fill(0).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("lark:column", { weight: 1, width: "weighted", backgroundStyle: "bg-white", children: [
|
|
382
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { align: "center", children: [
|
|
383
|
+
" ",
|
|
384
|
+
status.next?.[i]?.player ?? status.last?.[i]?.player,
|
|
385
|
+
" "
|
|
386
|
+
] }),
|
|
387
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { align: "center", children: [
|
|
388
|
+
" ",
|
|
389
|
+
status.next?.[i]?.point ?? status.last?.[i]?.point,
|
|
390
|
+
" "
|
|
391
|
+
] })
|
|
392
|
+
] })) })
|
|
393
|
+
] }),
|
|
394
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("lark:column", { weight: 1, width: "weighted", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("lark:column-set", { children: [
|
|
395
|
+
status.status === "pending" || status.status === "suspended" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("lark:column", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", text: `mahjongpub.manager.callback -a 1 -c ${cls}`, children: "开赛" }) }) : null,
|
|
396
|
+
status.status === "pending" || status.status === "running" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("lark:column", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", text: `mahjongpub.manager.callback -a 2 -c ${cls}`, children: "停止" }) }) : null
|
|
397
|
+
] }) })
|
|
398
|
+
] }), "renderCls");
|
|
399
|
+
const msg = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("lark:card", { width: "fill", title: "比赛管理", color: "blue", children: [
|
|
400
|
+
classes.map(([i, status]) => renderCls(i, status)),
|
|
401
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("lark:column-set", { children: [
|
|
402
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("lark:column", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", text: `mahjongpub.manager.callback -a 1 -A`, children: "全部开赛" }) }),
|
|
403
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("lark:column", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", text: `mahjongpub.manager.callback -a 2 -A`, children: "全部停止" }) }),
|
|
404
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("lark:column", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", text: `mahjongpub.manager.manage`, children: "刷新" }) })
|
|
405
|
+
] })
|
|
406
|
+
] }) });
|
|
407
|
+
if (session.type === "interaction/command") {
|
|
408
|
+
await session.bot.editMessage(session.channelId, session.referrer.event.context.open_message_id, msg);
|
|
409
|
+
} else {
|
|
410
|
+
await session.send(msg);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
ctx.command("mahjongpub.manager.callback").option("action", "-a <action:number>").option("cls", "-c <cls:number>").option("all", "-A").action(async ({ session, options }) => {
|
|
414
|
+
this.ctx.logger.info("Contest management callback:", session.userId, options);
|
|
415
|
+
switch (options.action) {
|
|
416
|
+
case 1:
|
|
417
|
+
await session.execute({
|
|
418
|
+
name: `mahjongpub.manager.start`,
|
|
419
|
+
args: options.all ? [] : [options.cls]
|
|
420
|
+
});
|
|
421
|
+
return `Callback executed: start class ${options.all ? "all" : options.cls}.`;
|
|
422
|
+
case 2:
|
|
423
|
+
await session.execute({
|
|
424
|
+
name: `mahjongpub.manager.stop`,
|
|
425
|
+
args: options.all ? [] : [options.cls]
|
|
426
|
+
});
|
|
427
|
+
return `Callback executed: stop class ${options.all ? "all" : options.cls}.`;
|
|
428
|
+
default:
|
|
429
|
+
return "Unknown action.";
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
static {
|
|
434
|
+
__name(this, "LarkExtension");
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
339
438
|
// src/manager.ts
|
|
340
439
|
var ContestExtra = class {
|
|
341
440
|
constructor(ctx) {
|
|
@@ -353,6 +452,7 @@ var ContestExtra = class {
|
|
|
353
452
|
lobby;
|
|
354
453
|
playerIndex;
|
|
355
454
|
ver = 0;
|
|
455
|
+
maxRow;
|
|
356
456
|
async broadcast(msg) {
|
|
357
457
|
this.subscribers.forEach((channel) => this.ctx.sendMessage(channel, msg).catch((e) => this.ctx.logger.debug(e)));
|
|
358
458
|
}
|
|
@@ -366,13 +466,13 @@ function getRowPlayer(contest, team, rowi, index) {
|
|
|
366
466
|
return team.players[index]?.split("##", 1)[0];
|
|
367
467
|
}
|
|
368
468
|
__name(getRowPlayer, "getRowPlayer");
|
|
369
|
-
var
|
|
469
|
+
var ContestManager2 = class {
|
|
370
470
|
constructor(ctx, config) {
|
|
371
471
|
this.ctx = ctx;
|
|
372
472
|
this.config = config;
|
|
373
473
|
this.mahjongpub = MahjongPub.new(ctx);
|
|
374
474
|
ctx.command("mahjongpub.manager", { authority: 3 }).action(import_koishi2.noop);
|
|
375
|
-
ctx.command("mahjongpub.manager.init").channelFields(["mahjongpub/bind-contest"]).option("clear", "-c").option("type", "-t [type:string]").option("ver", "-v [ver:number]").option("round", "-r [round:number]").option("lobby", "-l [lobby:string]").option("index", "-i [index:number]").action(async ({ session, options }) => {
|
|
475
|
+
ctx.command("mahjongpub.manager.init").channelFields(["mahjongpub/bind-contest"]).option("clear", "-c").option("type", "-t [type:string]").option("ver", "-v [ver:number]").option("round", "-r [round:number]").option("lobby", "-l [lobby:string]").option("index", "-i [index:number]").option("maxRow", "-m [maxRow:number]").action(async ({ session, options }) => {
|
|
376
476
|
const cid = +(session.channel["mahjongpub/bind-contest"] || 0);
|
|
377
477
|
if (!cid) return "Unauthorized.";
|
|
378
478
|
this.extra[cid] ??= new ContestExtra(ctx);
|
|
@@ -383,6 +483,7 @@ var ContestManager = class {
|
|
|
383
483
|
if (!(0, import_koishi2.isNullable)(options.round)) this.extra[cid].round = options.round;
|
|
384
484
|
if (!(0, import_koishi2.isNullable)(options.lobby)) this.extra[cid].lobby = options.lobby;
|
|
385
485
|
if (!(0, import_koishi2.isNullable)(options.index)) this.extra[cid].playerIndex = options.index;
|
|
486
|
+
if (!(0, import_koishi2.isNullable)(options.maxRow)) this.extra[cid].maxRow = options.maxRow;
|
|
386
487
|
return "Finished.";
|
|
387
488
|
});
|
|
388
489
|
ctx.command("mahjongpub.manager.deinit").channelFields(["mahjongpub/bind-contest"]).option("clear", "-c").action(async ({ session, options }) => {
|
|
@@ -440,6 +541,7 @@ var ContestManager = class {
|
|
|
440
541
|
kCtx.status = 200;
|
|
441
542
|
return true;
|
|
442
543
|
});
|
|
544
|
+
ctx.plugin(LarkExtension, this);
|
|
443
545
|
}
|
|
444
546
|
static {
|
|
445
547
|
__name(this, "ContestManager");
|
|
@@ -447,6 +549,101 @@ var ContestManager = class {
|
|
|
447
549
|
static inject = ["server", "mahjong", "mahjong.database", "mahjongpub", "sendMessage", "zx-dhs", "majsoul-dhs"];
|
|
448
550
|
mahjongpub;
|
|
449
551
|
extra = /* @__PURE__ */ Object.create(null);
|
|
552
|
+
async getClsStatus(cid, cls) {
|
|
553
|
+
const cextra = this.extra[cid];
|
|
554
|
+
if (!cextra) {
|
|
555
|
+
return {
|
|
556
|
+
status: "error",
|
|
557
|
+
rowi: -1,
|
|
558
|
+
msg: "比赛未初始化"
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
const contest = await this.mahjongpub.getContest(cid);
|
|
562
|
+
const rounds = await this.mahjongpub.getRounds(cid);
|
|
563
|
+
const teams = await this.mahjongpub.getTeams(cid);
|
|
564
|
+
const [lastRecord, secondRecord] = await this.ctx.mahjong.database.db("scoreboard").collection("matches").find({
|
|
565
|
+
cid: `${cid}`,
|
|
566
|
+
round: cextra.round ?? +contest.c_round,
|
|
567
|
+
cls,
|
|
568
|
+
ver: cextra.ver
|
|
569
|
+
}).sort({ rowi: -1 }).limit(2).toArray();
|
|
570
|
+
if (lastRecord && !lastRecord._finished) {
|
|
571
|
+
return {
|
|
572
|
+
status: "running",
|
|
573
|
+
rowi: lastRecord.rowi,
|
|
574
|
+
last: secondRecord?.results.map((x) => ({
|
|
575
|
+
player: x.name,
|
|
576
|
+
point: x.num
|
|
577
|
+
})) || lastRecord.players.map((x) => ({
|
|
578
|
+
player: x,
|
|
579
|
+
point: 0
|
|
580
|
+
}))
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
if (cextra.type === "ti" && lastRecord && lastRecord.results?.some((x) => x.num < 0) && !this.config.allowNegativeScore) {
|
|
584
|
+
return {
|
|
585
|
+
status: "finished",
|
|
586
|
+
rowi: lastRecord.rowi,
|
|
587
|
+
last: lastRecord.results.map((x) => ({
|
|
588
|
+
player: x.name,
|
|
589
|
+
point: x.num
|
|
590
|
+
}))
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
const rowi = lastRecord ? lastRecord.rowi + 1 : 0;
|
|
594
|
+
if (rowi >= (cextra.maxRow ?? contest.t_type.split(/\s+/).filter((x) => x).length)) {
|
|
595
|
+
return {
|
|
596
|
+
status: "finished",
|
|
597
|
+
rowi: lastRecord.rowi,
|
|
598
|
+
last: lastRecord.results.map((x) => ({
|
|
599
|
+
player: x.name,
|
|
600
|
+
point: x.num
|
|
601
|
+
}))
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
const round = Object.values(rounds).find((x) => x.round === (cextra.round ?? contest.c_round) && x.t_class === cls);
|
|
605
|
+
const players = round.tids.map((tid) => getRowPlayer(contest, teams[tid], rowi, cextra.playerIndex));
|
|
606
|
+
if (!players.every((x) => x)) {
|
|
607
|
+
return {
|
|
608
|
+
status: "error",
|
|
609
|
+
rowi,
|
|
610
|
+
msg: "名单未填写"
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
if (cextra.type === "ti") {
|
|
615
|
+
return {
|
|
616
|
+
status: cextra.stopCls.has(cls) ? "suspended" : "pending",
|
|
617
|
+
rowi,
|
|
618
|
+
next: players.map((x, i) => ({
|
|
619
|
+
player: x,
|
|
620
|
+
point: lastRecord ? lastRecord.results?.[i].num : 1e5
|
|
621
|
+
}))
|
|
622
|
+
};
|
|
623
|
+
} else if (cextra.type === "ssb") {
|
|
624
|
+
return {
|
|
625
|
+
status: cextra.stopCls.has(cls) ? "suspended" : "pending",
|
|
626
|
+
rowi,
|
|
627
|
+
next: players.map((x) => ({
|
|
628
|
+
player: x,
|
|
629
|
+
point: 25e3
|
|
630
|
+
}))
|
|
631
|
+
};
|
|
632
|
+
} else {
|
|
633
|
+
return {
|
|
634
|
+
status: "error",
|
|
635
|
+
rowi,
|
|
636
|
+
msg: `未知的比赛类型:${cextra.type}`
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
} catch (e) {
|
|
640
|
+
return {
|
|
641
|
+
status: "error",
|
|
642
|
+
rowi,
|
|
643
|
+
msg: `未知错误:${e}`
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
}
|
|
450
647
|
async _startMatch(cid, cls, timeout = 0, force = false) {
|
|
451
648
|
const cextra = this.extra[cid];
|
|
452
649
|
const tag = `${cls}组`;
|
|
@@ -477,7 +674,7 @@ var ContestManager = class {
|
|
|
477
674
|
return;
|
|
478
675
|
}
|
|
479
676
|
const rowi = lastRecord ? lastRecord.rowi + 1 : 0;
|
|
480
|
-
if (rowi >= contest.t_type.split(/\s+/).filter((x) => x).length) {
|
|
677
|
+
if (rowi >= (cextra.maxRow ?? contest.t_type.split(/\s+/).filter((x) => x).length)) {
|
|
481
678
|
cextra.broadcast(`[${tag}] 已经全部完成`);
|
|
482
679
|
return;
|
|
483
680
|
}
|
|
@@ -541,8 +738,8 @@ var ContestManager = class {
|
|
|
541
738
|
return this.startMatch(+record.cid, record.cls);
|
|
542
739
|
}
|
|
543
740
|
};
|
|
544
|
-
((
|
|
545
|
-
|
|
741
|
+
((ContestManager3) => {
|
|
742
|
+
ContestManager3.Config = import_koishi2.Schema.object({
|
|
546
743
|
token: import_koishi2.Schema.string(),
|
|
547
744
|
startInterval: import_koishi2.Schema.number().default(import_koishi2.Time.second).description("比赛开始间隔"),
|
|
548
745
|
startTimeoutInterval: import_koishi2.Schema.number().default(import_koishi2.Time.second * 30).description("比赛开始重试间隔"),
|
|
@@ -550,7 +747,7 @@ var ContestManager = class {
|
|
|
550
747
|
finishInterval: import_koishi2.Schema.number().default(import_koishi2.Time.second * 10).description("比赛结束间隔"),
|
|
551
748
|
allowNegativeScore: import_koishi2.Schema.boolean().default(false).description("允许负分")
|
|
552
749
|
});
|
|
553
|
-
})(
|
|
750
|
+
})(ContestManager2 || (ContestManager2 = {}));
|
|
554
751
|
|
|
555
752
|
// src/index.ts
|
|
556
753
|
function parsePlatform(target) {
|
|
@@ -565,7 +762,7 @@ var MahjongPub2 = class extends import_koishi3.Service {
|
|
|
565
762
|
super(ctx, "mahjongpub", true);
|
|
566
763
|
this.ctx = ctx;
|
|
567
764
|
this.config = config;
|
|
568
|
-
ctx.plugin(
|
|
765
|
+
ctx.plugin(ContestManager2, config.manager);
|
|
569
766
|
ctx.i18n.define("zh-CN", require_zh_CN());
|
|
570
767
|
this.contest = MahjongPub.new(ctx);
|
|
571
768
|
ctx.model.extend("user", {
|
|
@@ -965,7 +1162,7 @@ var MahjongPub2 = class extends import_koishi3.Service {
|
|
|
965
1162
|
endpoint: import_koishi3.Schema.string().default("https://cdn.r-mj.com/"),
|
|
966
1163
|
batchInterval: import_koishi3.Schema.natural().default(300),
|
|
967
1164
|
batchCount: import_koishi3.Schema.natural().default(10),
|
|
968
|
-
manager:
|
|
1165
|
+
manager: ContestManager2.Config
|
|
969
1166
|
});
|
|
970
1167
|
})(MahjongPub2 || (MahjongPub2 = {}));
|
|
971
1168
|
var index_default = MahjongPub2;
|
package/lib/lark.d.ts
ADDED
package/lib/manager.d.ts
CHANGED
|
@@ -21,7 +21,20 @@ interface MatchRecord {
|
|
|
21
21
|
_finished?: boolean;
|
|
22
22
|
results?: PlayerResult[];
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
export interface ClsStatus {
|
|
25
|
+
status: 'pending' | 'running' | 'finished' | 'suspended' | 'error';
|
|
26
|
+
rowi: number;
|
|
27
|
+
msg?: string;
|
|
28
|
+
last?: {
|
|
29
|
+
player: string;
|
|
30
|
+
point: number;
|
|
31
|
+
}[];
|
|
32
|
+
next?: {
|
|
33
|
+
player: string;
|
|
34
|
+
point: number;
|
|
35
|
+
}[];
|
|
36
|
+
}
|
|
37
|
+
export declare class ContestExtra {
|
|
25
38
|
ctx: Context;
|
|
26
39
|
subscribers: Set<string>;
|
|
27
40
|
stopCls: Set<number>;
|
|
@@ -30,6 +43,7 @@ declare class ContestExtra {
|
|
|
30
43
|
lobby?: string;
|
|
31
44
|
playerIndex?: number;
|
|
32
45
|
ver: number;
|
|
46
|
+
maxRow?: number;
|
|
33
47
|
constructor(ctx: Context);
|
|
34
48
|
broadcast(msg: string): Promise<void>;
|
|
35
49
|
}
|
|
@@ -40,6 +54,7 @@ export declare class ContestManager {
|
|
|
40
54
|
mahjongpub: MahjongPub;
|
|
41
55
|
extra: Dict<ContestExtra>;
|
|
42
56
|
constructor(ctx: Context, config: ContestManager.Config);
|
|
57
|
+
getClsStatus(cid: number, cls: number): Promise<ClsStatus>;
|
|
43
58
|
_startMatch(cid: number, cls: number, timeout?: number, force?: boolean): Promise<void>;
|
|
44
59
|
startMatch(cid: number, cls: number, timeout?: number, force?: boolean): Promise<void>;
|
|
45
60
|
onMatchFinished(record: MatchRecord): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hieuzest/koishi-plugin-mahjongpub",
|
|
3
3
|
"description": "Mahjong.pub API",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.14",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -20,6 +20,18 @@
|
|
|
20
20
|
"koishi": "^4.17.1"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@hieuzest/cachetools": "^0.1.5"
|
|
23
|
+
"@hieuzest/cachetools": "^0.1.5",
|
|
24
|
+
"@satorijs/element": "^3.1.8"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@hieuzest/koishi-buildtools": "^1.0.2",
|
|
28
|
+
"@hieuzest/koishi-plugin-mahjong": "^3.0.0",
|
|
29
|
+
"@hieuzest/koishi-plugin-majsoul-dhs": "^2.5.0",
|
|
30
|
+
"@hieuzest/koishi-plugin-riichi-city": "^0.5.2",
|
|
31
|
+
"@hieuzest/koishi-plugin-send": "^1.3.7",
|
|
32
|
+
"@koishijs/assets": "^1.1.2",
|
|
33
|
+
"@koishijs/cache": "^2.1.0",
|
|
34
|
+
"@koishijs/plugin-server": "^3.2.7",
|
|
35
|
+
"koishi": "^4.18.9"
|
|
24
36
|
}
|
|
25
37
|
}
|