import { fromBase64Url, toBase64Url } from './src/base64'; import { arrayEquals, BufferReader, BufferWriter, countConsecutiveItems, countItems, } from './src/utils'; import { fromYdkeURL, toYdkeURL } from './src/ydke'; import { fromYGOMobileDeckURL, toYGOMobileDeckURL } from './src/ygom'; export interface YGOProDeckLike { main: number[]; extra: number[]; side: number[]; name?: string; } export default class YGOProDeck implements YGOProDeckLike { main: number[] = []; extra: number[] = []; side: number[] = []; name?: string; constructor(init: Partial = {}) { this.main = init.main ? [...init.main] : []; this.extra = init.extra ? [...init.extra] : []; this.side = init.side ? [...init.side] : []; this.name = init.name; } bufferLength(noCompact = false) { const counted = [this.main, this.extra, this.side].map( noCompact ? countConsecutiveItems : countItems, ); return counted.reduce((a, b) => a + b.length * 4, 0); } toUint8Array(noCompact = false) { const counted = [this.main, this.extra, this.side].map( noCompact ? countConsecutiveItems : countItems, ); const writer = new BufferWriter( counted.reduce((a, b) => a + b.length * 4, 0), ); const writeCards = ( countMap: { item: number; count: number }[], type: number, ) => { // each card: 28 bits for id, 2 bits(0, 1, 2, 3) for type(0: main, 1: extra, 2: side, 3: unknown), 2 bits for count (0: 1, 1: 2, 2: 3, 3: 4) for (const { item, count } of countMap) { if (count > 4) { throw new Error(`Too many cards: ${item}`); } const value = (item & 0xfffffff) | (type << 28) | ((count - 1) << 30); writer.writeUint32LE(value); } }; counted.forEach(writeCards); return writer.buffer; } toEncodedString(noCompact = false) { return toBase64Url(this.toUint8Array(noCompact)); } toString(noCompact = false) { return this.toEncodedString(noCompact); } fromUint8Array(buf: Uint8Array) { for (let i = 0; i < buf.length - 3; i += 4) { const value = buf[i] | (buf[i + 1] << 8) | (buf[i + 2] << 16) | (buf[i + 3] << 24); const id = value & 0xfffffff; const type = (value >>> 28) & 0x3; const count = ((value >>> 30) & 0x3) + 1; const cards = [this.main, this.extra, this.side][type]; for (let j = 0; j < count; j++) { cards.push(id); } } return this; } static fromUint8Array(buf: Uint8Array) { return new YGOProDeck().fromUint8Array(buf); } fromEncodedString(str: string) { return this.fromUint8Array(fromBase64Url(str)); } static fromEncodedString(str: string) { return new YGOProDeck().fromEncodedString(str); } toYdkString() { return [ '#created by ygopro-deck-encode', '#main', ...this.main.map((id) => id.toString()), '#extra', ...this.extra.map((id) => id.toString()), '!side', ...this.side.map((id) => id.toString()), ].join('\n'); } fromYdkString(str: string) { const lines = str.split(/\r?\n/); let current = this.main; for (const _line of lines) { const line = _line.trim(); if (line === '#main') { current = this.main; } if (line === '#extra') { current = this.extra; } if (line === '!side') { current = this.side; } if (line.match(/^\d+$/)) { current.push(parseInt(line, 10)); } } return this; } static fromYdkString(str: string) { return new YGOProDeck().fromYdkString(str); } fromUpdateDeckPayload( buf: Uint8Array, isExtraDeckCard: (code: number, i: number, mainc: number) => boolean = () => false, ) { const reader = new BufferReader(buf); const mainc = reader.readUint32LE(); const sidec = reader.readUint32LE(); this.main = []; this.extra = []; this.side = []; for (let i = 0; i < mainc; i++) { const id = reader.readUint32LE(); if (isExtraDeckCard(id, i, mainc)) { this.extra.push(id); } else { this.main.push(id); } } for (let i = 0; i < sidec; i++) { const id = reader.readUint32LE(); this.side.push(id); } return this; } static fromUpdateDeckPayload( buf: Uint8Array, isExtraDeckCard: (code: number, i: number, mainc: number) => boolean = () => false, ) { return new YGOProDeck().fromUpdateDeckPayload(buf, isExtraDeckCard); } toUpdateDeckPayload() { const cards = [...this.main, ...this.extra, ...this.side]; const writer = new BufferWriter(cards.length * 4 + 8) .writeUint32LE(this.main.length + this.extra.length) .writeUint32LE(this.side.length); cards.forEach((id) => writer.writeUint32LE(id)); return writer.buffer; } fromYGOMobileDeckURL(uri: string): YGOProDeck { const parsed = fromYGOMobileDeckURL(uri); this.main = parsed.main; this.extra = parsed.extra; this.side = parsed.side; this.name = parsed.name; return this; } static fromYGOMobileDeckURL(uri: string): YGOProDeck { return new YGOProDeck().fromYGOMobileDeckURL(uri); } toYGOMobileDeckURL(): string { return toYGOMobileDeckURL( this.main, this.extra, this.side, this.name && { name: this.name, }, ); } fromYdkeURL(uri: string): YGOProDeck { const parsed = fromYdkeURL(uri); this.main = parsed.main; this.extra = parsed.extra; this.side = parsed.side; return this; } static fromYdkeURL(uri: string): YGOProDeck { return new YGOProDeck().fromYdkeURL(uri); } toYdkeURL(): string { return toYdkeURL({ main: this.main, extra: this.extra, side: this.side, }); } isEqual( other: YGOProDeckLike, options: { ignoreOrder?: boolean; } = {}, ): boolean { const { ignoreOrder = false } = options; const fields = ['main', 'extra', 'side'] as const; const getField = (o: YGOProDeckLike, field: (typeof fields)[number]) => o[field] || []; if ( fields.some( (field) => getField(this, field).length !== getField(other, field).length, ) ) { return false; } const normalize = (arr: number[]) => ignoreOrder ? [...arr].sort((a, b) => a - b) : arr; return fields.every((field) => arrayEquals( normalize(getField(this, field)), normalize(getField(other, field)), ), ); } }