Skip to content

add Auxiliary.HasMentionedCounter and mentioned_counter#3231

Open
purerosefallen wants to merge 1 commit into
masterfrom
patch-counter-mention
Open

add Auxiliary.HasMentionedCounter and mentioned_counter#3231
purerosefallen wants to merge 1 commit into
masterfrom
patch-counter-mention

Conversation

@purerosefallen

Copy link
Copy Markdown
Collaborator

updated cards by the following script

import fs from 'fs';
import path from 'path';
import initSqlJs from 'sql.js';
import { YGOProCdb } from 'ygopro-cdb-encode';

export interface CounterMention {
  value: number;
  hex: string;
  text: string;
}

export interface MentionedCounterCard {
  code: number;
  name: string;
  counters: CounterMention[];
}

export interface MentionedCounterScanOutput {
  source: {
    cardsCdb: string;
    stringsConf: string;
    scriptDir: string;
    field: 'desc';
  };
  cards: MentionedCounterCard[];
}

interface ScanOptions {
  rootDir?: string;
  cardsCdb?: string;
  stringsConf?: string;
  scriptDir?: string;
}

const outputJson = 'src/workspace/20260703-add-counter-mention.json';

const defaultPaths = (rootDir = process.cwd()) => ({
  rootDir,
  cardsCdb: path.resolve(rootDir, 'ygopro/cards.cdb'),
  stringsConf: path.resolve(rootDir, 'ygopro/strings.conf'),
  scriptDir: path.resolve(rootDir, 'workspace/script-fh'),
});

const toPosixRelative = (rootDir: string, targetPath: string) =>
  path.relative(rootDir, targetPath).split(path.sep).join('/');

export const formatCounterHex = (value: number) => `0x${value.toString(16)}`;

export const parseCounterStrings = (content: string): CounterMention[] => {
  const counters: CounterMention[] = [];

  for (const line of content.split(/\r?\n/)) {
    const match = line.match(/^!counter\s+(0x[0-9a-fA-F]+|\d+)\s+(.+?)\s*$/);
    if (!match) {
      continue;
    }

    const text = match[2].trim();
    if (/^.*$/.test(text)) {
      continue;
    }

    const value = Number(match[1]);
    counters.push({
      value,
      hex: formatCounterHex(value),
      text,
    });
  }

  return counters.sort((a, b) => a.value - b.value);
};

const overlaps = (
  ranges: Array<[number, number]>,
  start: number,
  end: number,
) =>
  ranges.some(([rangeStart, rangeEnd]) => start < rangeEnd && end > rangeStart);

export const findCounterMentions = (
  desc: string,
  counters: CounterMention[],
): CounterMention[] => {
  const candidates = counters
    .flatMap((counter) => {
      const matches: Array<{
        counter: CounterMention;
        start: number;
        end: number;
      }> = [];
      let start = desc.indexOf(counter.text);

      while (start >= 0) {
        matches.push({
          counter,
          start,
          end: start + counter.text.length,
        });
        start = desc.indexOf(counter.text, start + 1);
      }

      return matches;
    })
    .sort(
      (a, b) =>
        b.counter.text.length - a.counter.text.length ||
        a.start - b.start ||
        a.counter.value - b.counter.value,
    );

  const selectedRanges: Array<[number, number]> = [];
  const selectedCounters = new Map<number, CounterMention>();

  for (const candidate of candidates) {
    if (overlaps(selectedRanges, candidate.start, candidate.end)) {
      continue;
    }

    selectedRanges.push([candidate.start, candidate.end]);
    selectedCounters.set(candidate.counter.value, candidate.counter);
  }

  return Array.from(selectedCounters.values()).sort(
    (a, b) => a.value - b.value,
  );
};

const readScriptCodes = (scriptDir: string) =>
  new Set(
    fs
      .readdirSync(scriptDir)
      .flatMap((fileName) => {
        const match = fileName.match(/^c(\d+)\.lua$/);
        return match ? [Number(match[1])] : [];
      })
      .sort((a, b) => a - b),
  );

export const scanCounterMentions = async (
  options: ScanOptions = {},
): Promise<MentionedCounterScanOutput> => {
  const paths = {
    ...defaultPaths(options.rootDir),
    ...options,
  };
  const rootDir = paths.rootDir ?? process.cwd();
  const counters = parseCounterStrings(
    fs.readFileSync(paths.stringsConf, 'utf8'),
  );
  const scriptCodes = readScriptCodes(paths.scriptDir);
  const SQL = await initSqlJs();
  const db = new SQL.Database(fs.readFileSync(paths.cardsCdb));
  const cdb = new YGOProCdb(db);

  try {
    const cards = cdb
      .find()
      .filter((card) => scriptCodes.has(card.code))
      .flatMap((card): MentionedCounterCard[] => {
        const mentionedCounters = findCounterMentions(
          card.desc ?? '',
          counters,
        );
        if (mentionedCounters.length === 0) {
          return [];
        }

        return [
          {
            code: card.code,
            name: card.name,
            counters: mentionedCounters,
          },
        ];
      })
      .sort((a, b) => a.code - b.code);

    return {
      source: {
        cardsCdb: toPosixRelative(rootDir, paths.cardsCdb),
        stringsConf: toPosixRelative(rootDir, paths.stringsConf),
        scriptDir: toPosixRelative(rootDir, paths.scriptDir),
        field: 'desc',
      },
      cards,
    };
  } finally {
    db.close();
  }
};

const escapeRegExp = (value: string) =>
  value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

const detectNewline = (content: string) =>
  content.includes('\r\n') ? '\r\n' : '\n';

const findScriptTableName = (content: string, code: number) => {
  const match = content.match(
    new RegExp(
      `^function\\s+([A-Za-z_][A-Za-z0-9_]*|c${code})\\.initial_effect\\s*\\(`,
      'm',
    ),
  );
  if (!match) {
    throw new Error(`Cannot find initial_effect for c${code}.lua`);
  }

  return match[1];
};

const removeMentionedCounterBlock = (content: string, tableName: string) =>
  content.replace(
    new RegExp(
      `^${escapeRegExp(tableName)}\\.mentioned_counter\\s*=\\s*\\{\\r?\\n(?:.*\\r?\\n)*?\\}\\r?\\n?`,
      'm',
    ),
    '',
  );

const createMentionedCounterBlock = (
  tableName: string,
  counters: CounterMention[],
  newline: string,
) =>
  [
    `${tableName}.mentioned_counter={`,
    ...counters.map((counter) => `\t[${counter.hex}]=true,`),
    '}',
    '',
  ].join(newline);

const insertMentionedCounterBlock = (
  content: string,
  tableName: string,
  block: string,
) => {
  const initialEffect = content.match(
    new RegExp(
      `^function\\s+${escapeRegExp(tableName)}\\.initial_effect\\s*\\([^\\n]*\\)`,
      'm',
    ),
  );
  if (!initialEffect || initialEffect.index == null) {
    throw new Error(`Cannot find ${tableName}.initial_effect.`);
  }

  const afterInitialEffectStart = initialEffect.index + initialEffect[0].length;
  const initialEffectTail = content.slice(afterInitialEffectStart);
  const endMatch = initialEffectTail.match(/^end\r?\n/m);
  if (!endMatch || endMatch.index == null) {
    throw new Error(`Cannot find end of ${tableName}.initial_effect.`);
  }

  const afterInitialEffectEnd =
    afterInitialEffectStart + endMatch.index + endMatch[0].length;
  const afterInitialEffect = content.slice(afterInitialEffectEnd);
  const nextFunctionOffset = afterInitialEffect.search(/^function\s+/m);
  const insertIndex =
    nextFunctionOffset >= 0
      ? afterInitialEffectEnd + nextFunctionOffset
      : content.length;

  return `${content.slice(0, insertIndex)}${block}${content.slice(insertIndex)}`;
};

export const updateMentionedCounterScript = (
  content: string,
  code: number,
  counters: CounterMention[],
) => {
  const newline = detectNewline(content);
  const tableName = findScriptTableName(content, code);
  const withoutMentionedCounter = removeMentionedCounterBlock(
    content,
    tableName,
  );

  if (counters.length === 0) {
    return withoutMentionedCounter;
  }

  return insertMentionedCounterBlock(
    withoutMentionedCounter,
    tableName,
    createMentionedCounterBlock(tableName, counters, newline),
  );
};

export const applyMentionedCounterScripts = (
  scanOutput: MentionedCounterScanOutput,
  scriptDir = path.resolve(process.cwd(), scanOutput.source.scriptDir),
) => {
  const mentionsByCode = new Map(
    scanOutput.cards.map((card) => [card.code, card.counters]),
  );
  let changed = 0;

  for (const fileName of fs.readdirSync(scriptDir)) {
    const match = fileName.match(/^c(\d+)\.lua$/);
    if (!match) {
      continue;
    }

    const code = Number(match[1]);
    const filePath = path.resolve(scriptDir, fileName);
    const before = fs.readFileSync(filePath, 'utf8');
    const after = updateMentionedCounterScript(
      before,
      code,
      mentionsByCode.get(code) ?? [],
    );

    if (after !== before) {
      fs.writeFileSync(filePath, after);
      changed += 1;
    }
  }

  return changed;
};

export const writeMentionedCounterScanOutput = (
  scanOutput: MentionedCounterScanOutput,
  outputPath = path.resolve(process.cwd(), outputJson),
) => {
  fs.writeFileSync(outputPath, `${JSON.stringify(scanOutput, null, 2)}\n`);
};

export const runMentionedCounterCli = async () => {
  const scanOutput = await scanCounterMentions();
  const changed = applyMentionedCounterScripts(scanOutput);
  writeMentionedCounterScanOutput(scanOutput);

  console.log(`Mentioned counter cards: ${scanOutput.cards.length}`);
  console.log(`Updated scripts: ${changed}`);
  console.log(`Wrote ${outputJson}`);
};

if (require.main === module) {
  runMentionedCounterCli().catch((error) => {
    console.error(error instanceof Error ? error.message : error);
    process.exitCode = 1;
  });
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant