Commit 4fd99c63 authored by nanahira's avatar nanahira

optimize ygom 29 bit thing

parent 8851e0ef
import { fromBase64Url, toBase64Url } from "./base64"; import { fromBase64Url, toBase64Url } from "./base64";
// === 常量区域 === // === 常量 ===
const QUERY_YGO_TYPE = 'ygotype'; const QUERY_YGO_TYPE = 'ygotype';
const QUERY_VERSION = 'v'; const QUERY_VERSION = 'v';
const ARG_DECK = 'deck'; const ARG_DECK = 'deck';
const QUERY_DECK = 'd'; const QUERY_DECK = 'd';
const QUERY_NAME = 'name'; const QUERY_NAME = 'name';
const URL_SCHEME_HTTP = 'http'; const URL_SCHEME_HTTP = 'http';
const URL_HOST_DECK = 'deck.ourygo.top'; const URL_HOST_DECK = 'deck.ourygo.top';
// === 工具函数 === // === BitWriter 类 ===
function toBinary(value: number, length: number): string { class BitWriter {
return value.toString(2).padStart(length, '0'); private buffer: number[] = [];
} private current = 0;
private bitPos = 0;
writeBits(value: number, length: number) {
while (length > 0) {
const remain = 8 - this.bitPos;
const take = Math.min(remain, length);
const shift = length - take;
this.current |= ((value >> shift) & ((1 << take) - 1)) << (remain - take);
this.bitPos += take;
length -= take;
if (this.bitPos === 8) {
this.buffer.push(this.current);
this.current = 0;
this.bitPos = 0;
}
}
}
function encodeCards(cards: number[]): string { finish(): Uint8Array {
const bits: string[] = []; if (this.bitPos > 0) this.buffer.push(this.current);
for (let i = 0; i < cards.length; ) { return new Uint8Array(this.buffer);
const id = cards[i];
let count = 1;
while (i + count < cards.length && cards[i + count] === id && count < 3) count++;
const prefix = count === 2 ? '10' : count === 3 ? '11' : '01';
bits.push(prefix + toBinary(id, 27));
i += count;
} }
return bits.join('');
} }
function countUnique(cards: number[]): number { // === BitReader 类 ===
let num = 0; class BitReader {
for (let i = 0; i < cards.length; i++) { private index = 0;
const id = cards[i]; private bitPos = 0;
if (id > 0) {
num++; constructor(private bytes: Uint8Array) {}
if (i < cards.length - 1 && cards[i + 1] === id) {
i++; readBits(length: number): number {
if (i < cards.length - 1 && cards[i + 1] === id) { let result = 0;
i++; while (length > 0) {
const remain = 8 - this.bitPos;
const take = Math.min(remain, length);
const bits = (this.bytes[this.index] >> (remain - take)) & ((1 << take) - 1);
result = (result << take) | bits;
this.bitPos += take;
length -= take;
if (this.bitPos === 8) {
this.bitPos = 0;
this.index++;
} }
} }
return result;
} }
}
return num;
} }
// === 主函数 === // === 工具函数 ===
function countUnique(cards: number[]): number {
let count = 0;
for (let i = 0; i < cards.length; ) {
const id = cards[i];
count++;
while (i < cards.length && cards[i] === id) i++;
}
return count;
}
// === 主函数:编码 ===
export function toYGOMobileDeckURL( export function toYGOMobileDeckURL(
main: number[], main: number[],
extra: number[], extra: number[],
...@@ -57,20 +86,32 @@ export function toYGOMobileDeckURL( ...@@ -57,20 +86,32 @@ export function toYGOMobileDeckURL(
const eNum = countUnique(extra); const eNum = countUnique(extra);
const sNum = countUnique(side); const sNum = countUnique(side);
const header = toBinary(mNum, 8) + toBinary(eNum, 4) + toBinary(sNum, 4); const writer = new BitWriter();
let bitString = header + encodeCards(main) + encodeCards(extra) + encodeCards(side); writer.writeBits(mNum, 8);
while (bitString.length % 8 !== 0) bitString += '0'; writer.writeBits(eNum, 4);
writer.writeBits(sNum, 4);
const bytes = new Uint8Array(bitString.length / 8); const encodeSection = (cards: number[]) => {
for (let i = 0; i < bytes.length; i++) { for (let i = 0; i < cards.length; ) {
bytes[i] = parseInt(bitString.slice(i * 8, i * 8 + 8), 2); const id = cards[i];
let count = 1;
while (i + count < cards.length && cards[i + count] === id && count < 3) count++;
const prefix = count === 2 ? 0b10 : count === 3 ? 0b11 : 0b01;
writer.writeBits(prefix, 2);
writer.writeBits(id, 27);
i += count;
} }
};
let encoded = toBase64Url(bytes) encodeSection(main);
encodeSection(extra);
encodeSection(side);
const encoded = toBase64Url(writer.finish());
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(customParams)) { for (const [k, v] of Object.entries(customParams)) {
searchParams.set(key, value); searchParams.set(k, v);
} }
searchParams.set(QUERY_YGO_TYPE, ARG_DECK); searchParams.set(QUERY_YGO_TYPE, ARG_DECK);
searchParams.set(QUERY_VERSION, '1'); searchParams.set(QUERY_VERSION, '1');
...@@ -79,7 +120,7 @@ export function toYGOMobileDeckURL( ...@@ -79,7 +120,7 @@ export function toYGOMobileDeckURL(
return `${URL_SCHEME_HTTP}://${URL_HOST_DECK}?${searchParams.toString()}`; return `${URL_SCHEME_HTTP}://${URL_HOST_DECK}?${searchParams.toString()}`;
} }
// === 主函数:解码 ===
export function fromYGOMobileDeckURL(uri: string): { export function fromYGOMobileDeckURL(uri: string): {
main: number[]; main: number[];
extra: number[]; extra: number[];
...@@ -88,46 +129,39 @@ export function fromYGOMobileDeckURL(uri: string): { ...@@ -88,46 +129,39 @@ export function fromYGOMobileDeckURL(uri: string): {
} { } {
const url = new URL(uri); const url = new URL(uri);
if (url.searchParams.get(QUERY_YGO_TYPE) !== ARG_DECK) { if (url.searchParams.get(QUERY_YGO_TYPE) !== ARG_DECK) {
throw new Error('Not a YGO Mobile deck URI'); throw new Error('Invalid deck URL');
} }
let encoded = url.searchParams.get(QUERY_DECK); const encoded = url.searchParams.get(QUERY_DECK);
if (!encoded) throw new Error('Missing deck data'); if (!encoded) throw new Error('Missing deck data');
const bytes = fromBase64Url(encoded); const bytes = fromBase64Url(encoded);
const reader = new BitReader(bytes);
const bits = Array.from(bytes) const mNum = reader.readBits(8);
.map((b) => b.toString(2).padStart(8, '0')) const eNum = reader.readBits(4);
.join(''); const sNum = reader.readBits(4);
const total = mNum + eNum + sNum;
const mNum = parseInt(bits.slice(0, 8), 2);
const eNum = parseInt(bits.slice(8, 12), 2);
const sNum = parseInt(bits.slice(12, 16), 2);
const all = mNum + eNum + sNum;
const cards: number[] = [];
let pos = 16;
const res = { const result = {
main: [] as number[], main: [] as number[],
extra: [] as number[], extra: [] as number[],
side: [] as number[], side: [] as number[],
} };
let i = 0;
while (pos + 29 <= bits.length && cards.length < all) { for (let i = 0; i < total; i++) {
const countBits = bits.slice(pos, pos + 2); const prefix = reader.readBits(2);
const count = countBits === '10' ? 2 : countBits === '11' ? 3 : 1; const count = prefix === 0b10 ? 2 : prefix === 0b11 ? 3 : 1;
const id = parseInt(bits.slice(pos + 2, pos + 29), 2); const id = reader.readBits(27);
const field = i < mNum ? 'main' : i < mNum + eNum ? 'extra' : 'side'; const target =
res[field].push(...Array(count).fill(id)); i < mNum ? result.main : i < mNum + eNum ? result.extra : result.side;
pos += 29; for (let j = 0; j < count; j++) {
i++; target.push(id);
}
} }
return { return {
...res, ...result,
name: url.searchParams.get(QUERY_NAME), name: url.searchParams.get(QUERY_NAME) ?? undefined,
}; };
} }
...@@ -55,6 +55,98 @@ describe('Sample test.', () => { ...@@ -55,6 +55,98 @@ describe('Sample test.', () => {
it('should encode and decode ygomobile', () => { it('should encode and decode ygomobile', () => {
const uri = 'http://deck.ourygo.top?name=%E7%99%BD%E9%93%B6%E5%9F%8E%E7%A0%81&ygotype=deck&v=1&d=J-xK4Mka02AuEAMf2dV6mj7aRemuJNQJK8BwrcEYh0MqOJqBJSgYqUwxPEVhOG8R18WWVKmzkT-xEdwxbmGVkkOdrVIpufaYI3Hs8oOrcya8Bi40h9G79iFW80rq-o6P-AHsusPY5nmvHLol0DIqEykESVlf6VSbxVJp-j7XZtTE0XvmJW80rqH28R4rgyRovOusVJzbutenYFBA_cyK6d3UWcQkJQlLjaroWavH-INFA56k5DQNWOQ1gpvxrKVBLgEk1olpolKmSgriramLlgtBK1EQ6C6oi94ZyHe7N6T7mqE6peds7mahrORP6A'; const uri = 'http://deck.ourygo.top?name=%E7%99%BD%E9%93%B6%E5%9F%8E%E7%A0%81&ygotype=deck&v=1&d=J-xK4Mka02AuEAMf2dV6mj7aRemuJNQJK8BwrcEYh0MqOJqBJSgYqUwxPEVhOG8R18WWVKmzkT-xEdwxbmGVkkOdrVIpufaYI3Hs8oOrcya8Bi40h9G79iFW80rq-o6P-AHsusPY5nmvHLol0DIqEykESVlf6VSbxVJp-j7XZtTE0XvmJW80rqH28R4rgyRovOusVJzbutenYFBA_cyK6d3UWcQkJQlLjaroWavH-INFA56k5DQNWOQ1gpvxrKVBLgEk1olpolKmSgriramLlgtBK1EQ6C6oi94ZyHe7N6T7mqE6peds7mahrORP6A';
const correctYdk = `#created by OURYGO
#main
22812963
55410872
102380
89631139
89631139
89631139
95718355
95718355
95718355
59323650
22938501
96540807
6637331
33854624
72656408
63198739
63198739
14558127
14558127
23434538
23434538
94145021
17947697
17947697
97268402
17725109
18144507
25311006
31786838
31786838
31786838
80326401
80326401
80326401
24224830
24224830
29095457
29095457
48130397
65681983
67171933
67171933
67171933
71143015
56506740
56506740
24382602
10045474
10045474
22634473
43219114
27781371
62089826
85442146
48130397
8240199
22812963
22812963
#extra
12381100
43228023
56532353
2129638
11443677
11765832
21123811
89604813
63436931
10515412
59822133
59822133
74586817
29301450
42097666
!side
20292186
85103922
34267821
87126721
87126721
84192580
7608148
12444060
15693423
15693423
20899496
30748475
82732705
94145021
94145021`
const deck = new YGOProDeck().fromYGOMobileDeckURL(uri); const deck = new YGOProDeck().fromYGOMobileDeckURL(uri);
expect(deck.main).toHaveLength(58); expect(deck.main).toHaveLength(58);
expect(deck.extra).toHaveLength(15); expect(deck.extra).toHaveLength(15);
...@@ -62,6 +154,12 @@ describe('Sample test.', () => { ...@@ -62,6 +154,12 @@ describe('Sample test.', () => {
expect(deck.main[0]).toBe(22812963); expect(deck.main[0]).toBe(22812963);
expect(deck.extra[0]).toBe(12381100); expect(deck.extra[0]).toBe(12381100);
expect(deck.side[0]).toBe(20292186); expect(deck.side[0]).toBe(20292186);
const correctDeck = new YGOProDeck().fromYdkString(correctYdk);
expect(deck.main).toStrictEqual(correctDeck.main);
expect(deck.extra).toStrictEqual(correctDeck.extra);
expect(deck.side).toStrictEqual(correctDeck.side);
const uri2 = deck.toYGOMobileDeckURL(); const uri2 = deck.toYGOMobileDeckURL();
expect(uri2).toBe(uri); expect(uri2).toBe(uri);
......
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