Strip parent mentions from NPC relationships text
Parents now live in the father/mother fields, so "son/daughter/child of
X [and Y]" is redundant noise in Relationships / Notes.
- app:npc:strip-parent-text removes that clause and tidies leftover
punctuation ("Son of X, husband of Y" -> "Husband of Y"; standalone
"Son of X and Y" -> cleared). Keeps "grandson of", "father of",
"daughter-in-law of". 92 rows cleaned.
- data/npcs.csv regenerated from the DB with the cleaned text plus new
Father / Mother name columns
- app:npc:import resolves those optional columns in a second pass, so a
fresh install reproduces the parent links without app:npc:link-parents
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -95,6 +95,7 @@ class ImportNpcsCommand extends Command
|
||||
|
||||
$created = $updated = 0;
|
||||
$seen = [];
|
||||
$byName = [];
|
||||
foreach ($rows as $r) {
|
||||
$key = $this->key($r['Family'], $r['Name']);
|
||||
$seen[$key] = true;
|
||||
@@ -109,6 +110,7 @@ class ImportNpcsCommand extends Command
|
||||
} else {
|
||||
++$updated;
|
||||
}
|
||||
$byName[mb_strtolower(trim($r['Name']))] = $npc;
|
||||
|
||||
// NOTE: gameNotes is deliberately never written here.
|
||||
$npc->setAge('' === $r['Age'] ? null : (int) $r['Age'])
|
||||
@@ -120,6 +122,35 @@ class ImportNpcsCommand extends Command
|
||||
->setMysteryImportance(MysteryImportance::from($r['Mystery Importance']));
|
||||
}
|
||||
|
||||
// Second pass: resolve the optional Father / Mother name columns now that
|
||||
// every row exists. Unknown names are reported, not fatal.
|
||||
$badParents = [];
|
||||
foreach ($rows as $line => $r) {
|
||||
if (!\array_key_exists('Father', $r) && !\array_key_exists('Mother', $r)) {
|
||||
break;
|
||||
}
|
||||
$npc = $byName[mb_strtolower(trim($r['Name']))];
|
||||
foreach (['Father' => 'setFather', 'Mother' => 'setMother'] as $col => $setter) {
|
||||
$val = trim($r[$col] ?? '');
|
||||
if ('' === $val) {
|
||||
$npc->$setter(null);
|
||||
|
||||
continue;
|
||||
}
|
||||
$parent = $byName[mb_strtolower($val)] ?? null;
|
||||
if (null === $parent) {
|
||||
$badParents[] = sprintf('row %d: %s "%s" not found', $line + 2, $col, $val);
|
||||
|
||||
continue;
|
||||
}
|
||||
$npc->$setter($parent);
|
||||
}
|
||||
}
|
||||
if ($badParents) {
|
||||
$io->warning('Some Father/Mother values could not be resolved:');
|
||||
$io->listing($badParents);
|
||||
}
|
||||
|
||||
$purged = 0;
|
||||
if ($input->getOption('purge')) {
|
||||
foreach ($existing as $key => $npc) {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Repository\NpcRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:npc:strip-parent-text',
|
||||
description: 'Remove "son/daughter/child of X [and Y]" from the Relationships / Notes text (parents now live in their own father/mother fields).',
|
||||
)]
|
||||
class StripParentTextCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Matches "Son of X", "Daughter of X and Y", "Youngest child of X and Y".
|
||||
* Keeps "grandson of ...", "father of ...", "daughter-in-law of ..." intact.
|
||||
*/
|
||||
private const RE = "/(?:youngest\\s+)?(?<!grand)(?:son|daughter|child)\\s+of\\s+\\p{Lu}[\\p{L}'-]+(?:\\s+and\\s+\\p{Lu}[\\p{L}'-]+)?/iu";
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly NpcRepository $npcs,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show changes without writing');
|
||||
}
|
||||
|
||||
/** Strip the parent clause and tidy up leftover punctuation. Returns null if nothing meaningful remains. */
|
||||
public static function clean(?string $text): ?string
|
||||
{
|
||||
if (null === $text || '' === trim($text)) {
|
||||
return $text;
|
||||
}
|
||||
$out = preg_replace(self::RE, '', $text);
|
||||
$out = preg_replace('/\s*[;,]\s*[;,]\s*/', ', ', (string) $out); // ", ," -> ", "
|
||||
$out = preg_replace('/^\s*[;,]\s*/', '', (string) $out); // leading punctuation
|
||||
$out = preg_replace('/\s*[;,]\s*$/', '', (string) $out); // trailing punctuation
|
||||
$out = trim(preg_replace('/\s{2,}/', ' ', (string) $out));
|
||||
|
||||
if ('' === $out) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mb_strtoupper(mb_substr($out, 0, 1)).mb_substr($out, 1);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$dryRun = (bool) $input->getOption('dry-run');
|
||||
|
||||
$rows = [];
|
||||
$changed = 0;
|
||||
foreach ($this->npcs->findAll() as $npc) {
|
||||
$before = $npc->getRelationships();
|
||||
$after = self::clean($before);
|
||||
if ($before === $after) {
|
||||
continue;
|
||||
}
|
||||
++$changed;
|
||||
$rows[] = [$npc->getName(), (string) $before, $after ?? '(cleared)'];
|
||||
if (!$dryRun) {
|
||||
$npc->setRelationships($after);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$dryRun) {
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
$io->table(['NPC', 'Before', 'After'], $rows);
|
||||
$io->success(sprintf('%s: %d of %d NPCs updated.', $dryRun ? 'Dry run' : 'Done', $changed, \count($this->npcs->findAll())));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user