Commit 43274a7f authored by nanahira's avatar nanahira

support yaml deck

parent f2131f12
This diff is collapsed.
......@@ -21,13 +21,15 @@ export class DrawPluginConfig {
})
maxDepth: number;
async loadFileList() {
async loadFileList(...suffixes: string[]) {
return _.flatten(
await Promise.all(
this.deckPaths.map(async (deckPath) => {
const files = await fs.promises.readdir(deckPath);
return files
.filter((file) => file.endsWith('.json'))
.filter((file) =>
suffixes.some((suffix) => file.endsWith('.' + suffix)),
)
.map((file) => path.join(deckPath, file));
}),
),
......
import { Logger, Random } from 'koishi';
import _ from 'lodash';
import { OneDice } from 'onedice';
export type Decks = Record<string, string[]>;
export class Drawer {
constructor(
public decks: Decks,
private logger: Logger,
private maxDepth: number,
) {}
protected parseEntry(name: string, entry: string, depth = 1): string {
let result = entry.replace(/\[([^\]]+)\]/g, (dicePattern) =>
new OneDice().calculate(dicePattern.slice(1, -1)).toString(),
);
if (depth > this.maxDepth) {
this.logger.warn(`Max depth ${this.maxDepth} reached in deck ${name}.`);
return entry;
}
const usedUniqueIndex = new Map<string, Set<number>>();
result = result
.replace(/\{[^%\}]+\}/g, (refPattern) => {
let deckName = refPattern.slice(1, -1);
if (deckName.startsWith('$')) {
deckName = deckName.slice(1);
}
return this.drawFromDeck(deckName, depth + 1, name) || refPattern;
})
.replace(/\{%[^%\}]+\}/g, (uniqRefPattern) => {
const refName = uniqRefPattern.slice(2, -1);
let indexArray = usedUniqueIndex.get(refName);
if (!indexArray) {
indexArray = new Set();
usedUniqueIndex.set(refName, indexArray);
}
const deck = this.decks[refName];
if (!deck) {
this.logger.warn(
`Referenced deck ${refName} not found in deck ${name}.`,
);
return uniqRefPattern;
}
const availableIndexes = _.range(deck.length).filter(
(index) => !indexArray.has(index),
);
if (availableIndexes.length === 0) {
this.logger.warn(
`No more unique entries left for ${refName} in deck ${name}.`,
);
return uniqRefPattern;
}
const index = Random.pick(availableIndexes);
indexArray.add(index);
const entry = deck[index];
return this.parseEntry(refName, entry, depth + 1);
});
return result;
}
drawFromDeck(name: string, depth = 1, referencedDeck?: string): string {
const deck = this.decks[name];
if (!deck) {
this.logger.warn(
`${referencedDeck ? 'Referenced deck' : 'Deck'} ${name} not found${
referencedDeck ? `in deck ${referencedDeck}` : ''
}.`,
);
return null;
}
const entry = Random.pick(deck);
return this.parseEntry(name, entry, depth);
}
}
......@@ -14,17 +14,18 @@ import {
PutRenderer,
StarterPlugin,
} from 'koishi-thirdeye';
import { Logger, Random } from 'koishi';
import { Logger } from 'koishi';
import path from 'path';
import loadJsonFile from 'load-json-file';
import _ from 'lodash';
import * as localeZh from './locales/zh';
import * as localeEn from './locales/en';
import leven from 'leven';
import { OneDice } from 'onedice';
import { Decks, Drawer } from './drawer';
import { YamlDrawer, YamlStruct } from './yaml-plugin';
export * from './config';
type Decks = Record<string, string[]>;
import yaml from 'js-yaml';
import fs from 'fs';
@DefinePlugin()
export default class DrawPlugin
......@@ -34,121 +35,110 @@ export default class DrawPlugin
@InjectLogger()
private logger: Logger;
deckFiles = new Map<string, string[]>();
decks: Decks = {};
jsonDeckFiles = new Map<string, string[]>();
drawer: Drawer;
yamlDecks = new Map<string, YamlDrawer>();
yamlDeckFiles = new Map<string, YamlDrawer>();
onApply() {
this.drawer = new Drawer({}, this.logger, this.config.maxDepth);
}
async loadDeck(file: string) {
async loadJsonDeck(file: string) {
const filename = path.basename(file);
if (this.deckFiles.has(filename)) return;
if (this.jsonDeckFiles.has(filename)) return;
try {
const content: Decks = await loadJsonFile(file);
const deckTitles = Object.keys(content);
this.deckFiles.set(filename, deckTitles);
this.jsonDeckFiles.set(filename, deckTitles);
for (const key of deckTitles) {
if (this.decks[key]) {
if (this.drawer.decks[key]) {
this.logger.warn(`Duplicate deck ${key} in ${file}`);
}
this.decks[key] = content[key];
this.drawer.decks[key] = content[key];
}
} catch (e) {
this.logger.error(`Load deck file ${file} failed: ${e.message}`);
}
}
async loadDecks() {
this.deckFiles.clear();
this.decks = {};
const files = await this.config.loadFileList();
await Promise.all(files.map((file) => this.loadDeck(file)));
async loadYamlDeck(file: string) {
const filename = path.basename(file);
if (this.yamlDeckFiles.has(filename)) return;
try {
const content = yaml.load(
await fs.promises.readFile(file, 'utf8'),
) as YamlStruct;
if (!content.includes || !content.command) {
throw new Error('Invalid yaml deck');
}
const deck = new YamlDrawer(content, this.logger, this.config.maxDepth);
this.yamlDeckFiles.set(filename, deck);
this.yamlDecks.set(deck.meta.command, deck);
} catch (e) {
this.logger.error(`Load deck file ${file} failed: ${e.message}`);
}
}
async loadJsonDecks() {
this.jsonDeckFiles.clear();
this.drawer.decks = {};
const files = await this.config.loadFileList('json');
await Promise.all(files.map((file) => this.loadJsonDeck(file)));
const deckCount = _.sumBy(
Array.from(this.deckFiles.values()),
Array.from(this.jsonDeckFiles.values()),
(v) => v.length,
);
const deckFileCount = this.deckFiles.size;
const deckFileCount = this.jsonDeckFiles.size;
this.logger.info(
`Loaded ${deckCount} decks from ${deckFileCount} deck files.`,
`Loaded ${deckCount} JSON decks from ${deckFileCount} deck files.`,
);
return { deckCount, deckFileCount };
}
parseEntry(name: string, entry: string, depth = 1): string {
let result = entry.replace(/\[([^\]]+)\]/g, (dicePattern) =>
new OneDice().calculate(dicePattern.slice(1, -1)).toString(),
async loadYamlDecks() {
this.yamlDeckFiles.clear();
this.yamlDecks.clear();
const files = await this.config.loadFileList('yaml', 'yml');
await Promise.all(files.map((file) => this.loadYamlDeck(file)));
const deckCount = this.yamlDecks.size;
const deckFileCount = deckCount;
this.logger.info(
`Loaded ${deckCount} YAML decks from ${deckFileCount} deck files.`,
);
if (depth > this.config.maxDepth) {
this.logger.warn(
`Max depth ${this.config.maxDepth} reached in deck ${name}.`,
);
return entry;
}
const usedUniqueIndex = new Map<string, Set<number>>();
result = result
.replace(
/\{[^%\}]+\}/g,
(refPattern) =>
this.drawFromDeck(refPattern.slice(1, -1), depth + 1, name) ||
refPattern,
)
.replace(/\{%[^%\}]+\}/g, (uniqRefPattern) => {
const refName = uniqRefPattern.slice(2, -1);
let indexArray = usedUniqueIndex.get(refName);
if (!indexArray) {
indexArray = new Set();
usedUniqueIndex.set(refName, indexArray);
}
const deck = this.decks[refName];
if (!deck) {
this.logger.warn(
`Referenced deck ${refName} not found in deck ${name}.`,
);
return uniqRefPattern;
}
const availableIndexes = _.range(deck.length).filter(
(index) => !indexArray.has(index),
);
if (availableIndexes.length === 0) {
this.logger.warn(
`No more unique entries left for ${refName} in deck ${name}.`,
);
return uniqRefPattern;
}
const index = Random.pick(availableIndexes);
indexArray.add(index);
const entry = deck[index];
return this.parseEntry(refName, entry, depth + 1);
});
return result;
return { deckCount, deckFileCount };
}
drawFromDeck(name: string, depth = 1, referencedDeck?: string): string {
const deck = this.decks[name];
if (!deck) {
this.logger.warn(
`${referencedDeck ? 'Referenced deck' : 'Deck'} ${name} not found${
referencedDeck ? `in deck ${referencedDeck}` : ''
}.`,
);
return null;
}
const entry = Random.pick(deck);
return this.parseEntry(name, entry, depth);
async loadDecks() {
const results = await Promise.all([
this.loadJsonDecks(),
this.loadYamlDecks(),
]);
return {
deckCount: results[0].deckCount + results[1].deckCount,
deckFileCount: results[0].deckFileCount + results[1].deckFileCount,
};
}
async onConnect() {
await this.loadDecks();
}
@UseCommand('draw <name:text>', { checkArgCount: true })
@UseCommand('draw <name> [param]', { checkArgCount: true })
@CommandLocale('zh', localeZh.commands.draw)
@CommandLocale('en', localeEn.commands.draw)
drawCommand(
@PutArg(0) name: string,
@PutArg(1) param: string,
@PutUserName() user: string,
@PutCommonRenderer() renderer: CRenderer,
) {
const result = this.drawFromDeck(name);
if (this.yamlDecks.has(name)) {
const deck = this.yamlDecks.get(name);
const result = deck.draw(param);
return result ?? renderer('.notFound');
}
const result = this.drawer.drawFromDeck(name);
if (!result) {
return renderer('.notFound');
}
......@@ -164,35 +154,59 @@ export default class DrawPlugin
@UseCommand('draw.list')
@CommandLocale('zh', localeZh.commands.list)
@CommandLocale('en', localeEn.commands.list)
onListCommand(@PutRenderer('.fileInfo') renderer: Renderer) {
const entries = Array.from(this.deckFiles.entries()).map(
onListCommand(
@PutRenderer('.fileInfo') renderer: Renderer,
@PutRenderer('.yamlFileInfo') yamlRenderer: Renderer,
) {
const jsonEntries = Array.from(this.jsonDeckFiles.entries()).map(
([file, titles]) => ({
file,
count: titles.length,
}),
);
return entries.map((entry) => renderer(entry)).join('\n');
const yamlEntries = Array.from(this.yamlDeckFiles.entries()).map(
([file, deck]) => ({ ...deck.meta, file }),
);
return [
...jsonEntries.map((entry) => renderer(entry)),
...yamlEntries.map((entry) => yamlRenderer(entry)),
].join('\n');
}
@UseCommand('draw.help')
@CommandLocale('zh', localeZh.commands.help)
@CommandLocale('en', localeEn.commands.help)
onHelpCommand(@PutRenderer('.fileInfo') renderer: Renderer) {
const entries = Array.from(this.deckFiles.entries()).map(
onHelpCommand(
@PutRenderer('.fileInfo') renderer: Renderer,
@PutRenderer('.yamlFileInfo') yamlRenderer: Renderer,
) {
const jsonEntries = Array.from(this.jsonDeckFiles.entries()).map(
([file, titles]) => ({
file,
count: titles.length,
titles,
}),
);
return entries
.map(
const yamlDecks = Array.from(this.yamlDeckFiles.entries()).map(
([file, deck]) => ({ deck, file }),
);
return [
...jsonEntries.map(
(entry) =>
renderer(entry) +
'\n' +
entry.titles.map((title) => `draw ${title}`).join('\n'),
)
.join('\n');
),
...yamlDecks.map(
(entry) =>
yamlRenderer(entry.deck.meta) +
'\n' +
entry.deck
.getCommands()
.map((command) => `draw ${command}`)
.join('\n'),
),
].join('\n');
}
@UseCommand('draw.reload')
......@@ -211,9 +225,10 @@ export default class DrawPlugin
@PutRenderer('.result') renderer: Renderer,
@PutRenderer('.notFound') notFoundRenderer: Renderer,
) {
const allDecks = _.flatten(Array.from(this.deckFiles.values())).filter(
(d) => d.includes(word),
);
const allDecks = _.flatten([
...Array.from(this.jsonDeckFiles.values()),
...Array.from(this.yamlDeckFiles.values()).map((v) => v.getCommands()),
]).filter((d) => d.includes(word));
if (!allDecks.length) {
return notFoundRenderer({ word });
}
......
......@@ -2,6 +2,8 @@ import { Dict } from 'koishi';
import { CommandLocaleDef } from 'koishi-thirdeye';
export const fileInfo = 'File: {file} Count: {count}';
const yamlFileInfo = '{name} Author: {author} Version: {version}\n{desc}';
const listMessages = { fileInfo, yamlFileInfo };
export const commands: Dict<CommandLocaleDef> = {
draw: {
description: 'Draw from deck.',
......@@ -12,15 +14,11 @@ export const commands: Dict<CommandLocaleDef> = {
},
list: {
description: 'Check deck file list.',
messages: {
fileInfo,
},
messages: listMessages,
},
help: {
description: 'Check deck list.',
messages: {
fileInfo,
},
messages: listMessages,
},
reload: {
description: 'Reload deck files.',
......
import { CommandLocaleDef } from 'koishi-thirdeye';
import { Dict } from 'koishi';
export const fileInfo = '文件名: {file} 数量: {count}';
const fileInfo = '文件名: {file} 数量: {count}';
const yamlFileInfo = '{name} 文件名: {file} 作者: {author} 版本: {version}\n{desc}';
const listMessages = { fileInfo, yamlFileInfo };
export const commands: Dict<CommandLocaleDef> = {
draw: {
description: '进行牌堆抽取',
......@@ -12,15 +14,11 @@ export const commands: Dict<CommandLocaleDef> = {
},
list: {
description: '查看载入了哪些牌堆文件',
messages: {
fileInfo,
},
messages: listMessages,
},
help: {
description: '查看有哪些牌堆名可以使用',
messages: {
fileInfo,
},
messages: listMessages,
},
reload: {
description: '重新载入牌堆数据',
......
import { Logger } from 'koishi';
import { Drawer } from './drawer';
export interface YamlMeta {
name: string;
author: string;
version: number;
command: string;
desc: string;
includes: string[];
}
export type YamlStruct = YamlMeta & Record<string, string[]>;
export class YamlDrawer {
private availableDecks = new Set<string>();
private drawer = new Drawer(this.meta, this.logger, this.maxDepth);
constructor(
public meta: YamlStruct,
private logger: Logger,
private maxDepth: number,
) {
for (const deck of meta.includes ?? Object.keys(meta)) {
this.availableDecks.add(deck);
}
}
draw(deck: string, username?: string) {
deck ??= 'default';
if (!this.availableDecks.has(deck)) {
this.logger.warn(`Deck ${deck} not found.`);
return;
}
let result = this.drawer.drawFromDeck(deck);
if (result && username) {
result = result.replace(/【name】/g, username);
}
return result;
}
getCommands() {
return this.meta.includes.map((deck) =>
deck === 'default' ? this.meta.command : `${this.meta.command} ${deck}`,
);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment