import { intro, outro, select, spinner, isCancel, cancel, } from '@clack/prompts'; import color from 'picocolors'; import { match, P } from 'ts-pattern'; import csvToJson from 'convert-csv-to-json'; import itemProto from './input/item_proto.txt' with { type: 'text' }; if (!itemProto) { throw new Error('The file input/item_proto.txt was not found'); } intro(color.inverse('JSON Transform')); const fileType = await select({ message: 'Pick a file to transform', options: [ { value: 'special_item_group', label: 'special_item_group.txt', }, { value: 'blend', label: 'blend.txt', }, ], }); const getProto = () => csvToJson.fieldDelimiter('\t').csvStringToJson(itemProto); if (isCancel(fileType)) { cancel('Operation cancelled'); process.exit(0); } await match(fileType) .with('special_item_group', async () => { const s = spinner(); const proto = getProto(); s.start('Transforming special_item_group.txt'); const input = Bun.file('./input/special_item_group.txt'); if (!input.exists()) { throw new Error('The file input/special_item_group.txt was not found'); } const specialItemGroup = await input.text(); const REGEX = /group\s+([^\n]+)\s*\{\s*vnum\s+([^\n]+)\s*(?:type\s+([^\n]+)\s*)?((?:\d+\s+[^\n]+\s*)+)\s*(?:effect\s+([^\n]+)\s*)?}/gim; const matches = Array.from(specialItemGroup.matchAll(REGEX)).map( (instance) => { const [, name, vnum, baseType, rowsBlock, effect] = instance; const type = match(baseType?.trim().toLocaleLowerCase()) .with('pct', () => 'percentage' as const) .with('attr', () => 'attribute' as const) .with('quest', () => 'quest' as const) .with('special', () => 'special' as const) .otherwise(() => 'normal' as const); const STANDARD_ROW_REGEX = /^[ \t]*\d+\s+(\S+)\s+(\d+)\s+(\d+)(?:\s+(\S+))?/gm; const ATTR_ROW_REGEX = /^[ \t]*\d+\s+(\d+)\s+(\d+)?/gm; const rows = Array.from( rowsBlock.matchAll( type === 'attribute' ? ATTR_ROW_REGEX : STANDARD_ROW_REGEX, ), ).map((row) => { const value1 = Number(row[2]); const value2 = Number(row[3]); const value3 = Number(row[4] || 0); return match(type) .with('attribute', () => ({ apply_type: Number(row[1]), apply_value: value1, })) .otherwise(() => ({ vnum: match(row[1].trim()) .with(P.union('경험치', 'exp'), () => 'exp') .with( P.union('mob', 'slow', 'drain_hp', 'poison', 'group'), (v) => v, ) .when( (v) => Number.isNaN(Number(v)), (v) => { const itemMatch = proto.find( (item) => item['ITEM_NAME(K)'] === v, ); if (!itemMatch) { throw new Error(`Item not found ${v}`); } return Number(itemMatch['ITEM번호']); }, ) .otherwise((value) => Number(value)), count: value1, probability: value2, rare_percentage: value3, })); }); return { name: name.trim(), type, vnum: Number(vnum), rows, effect: effect ? effect.trim().substring(1, effect.trim().length - 1) : null, }; }, ); s.stop('special_item_group.txt was transformed'); const out = Bun.file('./output/special_item_group.json'); if (await out.exists()) { await out.delete(); } out.write( JSON.stringify( { $schema: 'https://raw.githubusercontent.com/WildEgo/m2-json-schemas/refs/heads/main/special_item_group.json', rows: matches, }, null, 2, ), ); }) .with('blend', async () => { const s = spinner(); s.start('Transforming blend.txt'); const input = Bun.file('./input/blend.txt'); if (!input.exists()) { throw new Error('The file input/special_item_group.txt was not found'); } const blend = await input.text(); const REGEX = /section\s+[\s\S]*?item_vnum\s+(\d+)[\s\S]*?apply_type\s+(\S+)[\s\S]*?apply_value\s+([\d\s]+)[\s\S]*?apply_duration\s+([\d\s]+)[\s\S]*?end/gim; const matches = Array.from(blend.matchAll(REGEX)).map( ([_, vnum, apply_type, values, durations]) => { const validDurations = durations.trim().split(/\t/); return { vnum: parseInt(vnum, 10), apply_type, apply_data: values .trim() .split(/\t/) .map((value, index) => ({ value: parseInt(value, 10), duration: parseInt(validDurations[index], 10), })), }; }, ); s.stop('blend.txt was transformed'); const out = Bun.file('./output/blend.json'); if (await out.exists()) { await out.delete(); } out.write( JSON.stringify( { $schema: 'https://raw.githubusercontent.com/WildEgo/m2-json-schemas/refs/heads/main/blend.json', rows: matches, }, null, 2, ), ); }) .otherwise(() => { outro(color.bgRed('Invalid file type')); process.exit(1); }); outro("You're all set now");