view log2template.pl @ 374:10d030b85117 misc tip

Switch Convent map to SVG version, add link to PNG render to the caption.
author Matti Hamalainen <ccr@tnsp.org>
date Fri, 01 Mar 2024 10:02:38 +0200
parents 8e456ddb0324
children
line wrap: on
line source

#!/usr/bin/perl -w
#
# log2template - Log parser for BatWiki NPC/Armour/Item/Weapon templates
# Developed by Matti Hämäläinen (Ggr Pupunen) <ccr@tnsp.org>
# (C) Copyright 2010-2012 Tecnic Software productions (TNSP)
#
# Requires Perl 5.8.
#
# Usage
# =====
# 1) command qla say -- $* --;prod $*;whereami;say -??-;blook at $*;say -==-;look at $*;say ====
# 2) rip_action set party kills (or spam party kills every once in a while to collect expvalue data)
# 3) qla monster
# 4) Cast detect race, detect alignment, whatnot at monster (if you wish/can/have those).
# 5) Fight and kill monster.
# 6) run log2template.pl < logfile.log > output.txt
#
# 7) command id say -ID-;blook at $*;say -%-;weigh $*;say -#-;look at $*;say -!-;cast identify at $*
#
# Notes
# =====
# - You must have spellname translator, preferably one with format:
#   Xxxx claps its hands and says (cure critical wounds)
#
# - There is some flexibility in the parser, it can filter out ANSI from the
#   input log, etc. but you may have to customize it. And that requires Perl
#   knowledge.
#
use strict;
use warnings;

# Options
my $opt_ansi = 1;	# 1 = strip ANSI, 0 = don't (is faster)
my $opt_debug = 0;	# 1 = enable debug output
my $opt_pkills_crop = 60;
my $opt_outfile;
my $opt_getlocs = 0;
my $opt_locpath;
my $opt_identify = 1;
my $opt_npc = 1;
my $opt_dist = 1;

# Define the regular expression to match any outputs of "say"
# as this is somewhat race-dependant.
my $sayregex = "(quack|say|howl)";


# Define the various spell chant prefixes here, we need them
# for matching the spell cast lines themselves.
my @spellChants = (
"claps its hands and says",
"claps his hands and whispers",
"fills up his cheeks with air and exhales",
"booms in sinister voice",
"throws a pinch of magic dust in air and chants",
"rolls her eyes wildly and exclaims",
"rolls its eyes frantically and states",
"utters the magic words",
"spreads her fingers and whispers",
"waves its index finger while uttering",
"points an accusing finger and cries",
"gets an evil gleam in its eyes and chants",
"flaps arms and utters the magic words",
"flaps his arms and utters the magic words",
"taps its foot three times and utters the words",
"is surrounded by blue waves as you hear the words:",
"closes his eyes and with a dry, dark voice entones",
"shakes with firey rage and yells",
"traces fiery demonic night runes in the air",
"weaves a mystic matrix with glowing red lines in arcane ways and chants",
"mutters the holy words",
"mutters the magic words",
"makes shooing gestures with his hands and whispers",
"frantically waves his hands and yells",
"grabs a herb and chants",
"consults her guide and chants",
"utters the magic words",
);


my %areaMappings = (
  "Ocp west" => { "name" => "Old Central Park in Arelium", "continent" => "Laenor" },
);


my %genderMappings = (
  "it" => "n",
  "she" => "f",
  "he" => "m",
);

# Stat name translations
my @statTransTable = (
  sub { return $_[0] =~ s/^ability at the (skill|spell) (.+)$/$2/ },
  sub { return $_[0] =~ s/^mental regeneration$/spr/g },
  sub { return $_[0] =~ s/^physical regeneration$/hpr/g },
  sub { return $_[0] =~ s/^agility in combat$/avoid/g },
  sub { return $_[0] =~ s/^damage you do in combat$/dam/g },
  sub { return $_[0] =~ s/^resistance you have against physical damage/physical resistance/g },
#  sub { return $_[0] =~ s///g },
);


###
### Parse commandline arguments
###
while (defined(my $arg = shift)) {
  if ($arg eq "-d") {
    $opt_debug++;
  }
  elsif ($arg eq "-i") {
    $opt_identify = 0;
  }
  elsif ($arg eq "-n") {
    $opt_npc = 0;
  }
  elsif ($arg eq "-o") {
    $opt_outfile = shift or die("Output file option -o requires an argument.\n");
  }
  elsif ($arg eq "-L") {
    $opt_locpath = shift or die("Locfile path option -L requires an argument.\n");
  }
  elsif ($arg eq "-f") {
    $opt_dist = shift or die("Distance factor option -f requires an argument.\n");
    if ($opt_dist < 1 || $opt_dist > 50) {
      die("-f value must be 1 .. 50.\n");
    }
  }
  else {
    die("Invalid option '$arg'.
Usage: $0 [options] < input.log

    -o <filename>         Output filename.
    -d                    Enable debug mode.
    -i                    Disable identify parser.
    -n                    Disable NPC parser.
    -L <locfile_path>     Path to <continent_name>.loc files.
    -f <value>            Set spell/skill detection line distance factor.

");
  }
}

print STDERR "Debug mode enabled, level=".$opt_debug.".\n" if ($opt_debug);

if (defined($opt_outfile)) {
  open(STDOUT, '>', $opt_outfile) or die("Could not open output file '$opt_outfile'.\n");
}

if (defined($opt_locpath)) {
  $opt_getlocs = 1;
  print STDERR "Locfile path '".$opt_locpath."' set.\n";
}


### Translate stats
sub trans_stat($)
{
  my ($s) = @_;
  foreach my $pat (@statTransTable) {
    return $s if ($pat->($s));
  }  
  return $s;
}


my %locations = ();
sub parse_locfile($)
{
  my ($continent) = @_;

  return if defined($locations{$continent}{"OK"});

  my $nline = 0;
  my $filename = $opt_locpath."/".$continent.".loc";

  open(INFILE, '<', $filename) or print(STDERR "Could not open locfile '$filename'.\n");
  while (defined(my $line = <INFILE>)) {
    $nline++;
    chomp($line);
    if ($line =~ /^(#|\s*$)/) {
    } elsif ($line =~ /^\s*(\d+)\s*;\s*(\d+)\s*;\s*(\d\S*)\s*;\s*([^;]+);(.*)/) {
      my ($xc, $yc, $flags, $namestr, $rest) = ($1,$2,$3,$4,$5);
      my @names = split(/\s*\|\s*/, $namestr);
      $locations{$continent}{$xc}{$yc} = \@names;
    } else {
      print STDERR "Error on LOCfile '$filename' #$nline.\n";
    }
  }
  
  close(INFILE);
  $locations{$continent}{"OK"} = 1;
}


### Globals
my %monsters = ();
my %aligns = ();
my %spells = ();
my %skills = ();
my %races = ();
my %ids = ();

my $line = 0;

my $mob_sdesc;
my $mob = { };
my $state = 0;

my $id_tale;
my $id_tale2;
my $id = {};


### Convert spellchants array to regexp
my $spellregexps = join("|", map { s/ (his|its|her) / ... /g; $_ } @spellChants);


### Change parsing state
sub set_state($$)
{
  return unless ($_[0] != $state);
  print STDERR "STATE = $_[0], old = $state\n" if ($opt_debug > 1);
  $state = $_[0];
  if ($state < 0) {
    print STDERR "ERROR($line): $_[1]\n";
    $state = 0;
  }
}


sub check_gender($$)
{
  my ($mob, $s) = @_;

  if ($s =~ /^(He|She|It) +\[[a-z]+\] \([0-9-]+\%\)$/o ||
      $s =~ /^(He|She|It) is (in (a )?)?(excellent shape|good shape|slightly hurt|noticeably hurt|not in a good shape|bad shape|very bad shape|near death)/o) {
    my $gender = lc($1);
    if (defined($genderMappings{$gender})) {
      $$mob{"gender"} = $genderMappings{$gender};
    } else {
      print STDERR "ERROR($line): Invalid gender '$gender'\n";
    }
    return 1;
  }
  return 0;
}

sub parse_npc($)
{
  my ($s) = @_;
  
  if ($state == 1) {
    if ($s =~ /^You prod (.+?) like a stray cow\.$/o) {
      # Get 'short name'
      $$mob{"sname"} = $1;
    } elsif ($s =~ /^You are in '.*?' in (.+?) on the continent of ([A-Z][a-z]+). \(Coordinates: (\d+)x, (\d+)y(; Global: \d+x, \d+y)?\)$/o) {
      # Area, continent
      $$mob{"area"} = $1;
      $$mob{"continent"} = $2;
      $$mob{"xc"} = $3;
      $$mob{"yc"} = $4;
    } elsif ($s =~ /^You are in '.*?', which is on the continent of ([A-Z][a-z]+). \(Coordinates: (\d+)x, (\d+)y(; Global: \d+x, \d+y)?\)$/o) {
      # Area, continent
      $$mob{"area"} = $1;
      $$mob{"continent"} = $1;
      $$mob{"xc"} = $2;
      $$mob{"yc"} = $3;
    } elsif ($s =~ /^You are in '.*?' in (.+?)\.$/o) {
      my $area = $1;
      # Area, continent
      if (defined($areaMappings{$area})) {
        $$mob{"area"} = $areaMappings{$area}{"name"};
      }
      $$mob{"continent"} = $areaMappings{$area}{"continent"};
      $$mob{"xc"} = 0;
      $$mob{"yc"} = 0;
    } elsif ($s =~ /^You $sayregex '(-\?\?-|--\?\?--\?\?--\?\?--\?\?--)\.'$/o) {
      set_state(2, $s);
    } else {
      set_state(-1, $s);
    }
  }
  elsif ($state == 2) {
    # Mostly just grab shortdesc here
    if ($s =~ /^You $sayregex '(-==-|-=-=-=-=-=-=-=-=-)\.'$/o) {
      set_state(3, $s);
    } elsif (check_gender($mob, $s)) {
      set_state(-1, $s);
    } elsif ($s =~ /^You $sayregex /o) {
      set_state(-1, $s);
    } else {
      $mob_sdesc = $s;
    }
  }
  elsif ($state == 3) {
    if ($s =~ /^ +([a-zA-Z ,-]+?)'s equipment:$/o) {
      # End of long desc
      set_state(4, $s);
    } elsif (check_gender($mob, $s)) {
      # End of long desc
      set_state(4, $s);
    } elsif ($s =~ /^You $sayregex /o) {
      set_state(-1, $s);
    } else {
      # Collect long desc
      $$mob{"ldesc"} .= $s." ";
    }
  }
  elsif ($state == 4) {
    if ($s =~ /^You $sayregex '====*'?\.'$/o) {
      # Finished, submit information to hash
      $monsters{$mob_sdesc} = $mob;
      set_state(0, $s);
    } elsif ($s =~ /^([A-Z][a-z, ]+): (.*)$/o) {
      # Collect EQ and slot information
      $$mob{"eqs"}{$1} = $2;
    } elsif ($s =~ /^([A-Z][a-z, ]+)\s+\(partially hidden\)\s*: (.*)$/o) {
      # Collect EQ and slot information
      $$mob{"eqs"}{$1} = $2;
    } elsif ($s =~ /^ +([a-zA-Z -]+?)'s equipment:$/o) {
      # Ignore this line
    } elsif ($s =~ /^((It|She|He) has a blueish black kiss on ... cheek\.|(It|She|He) suffers from scourge\.|Some glittering red mist is surrounding this creature.|Nothing\.|(It|He|She) has a faint ring of .+ magic mist around ... neck\.)$/o) {
      # Ignore various useless lines such as holz markings
    } else {
      # Error.
      set_state(-1, $s);
    }
  }
}


### Parse the big data blob
sub parse_id_blob($)
{
  my $info = $_[0];

  # Weight & size
  if ($info =~ /It is ([A-Za-z -]+), ([A-Za-z -]+),/) {
    $$id{"weight2"} = $1;
    $$id{"size"} = $2;
  }

  # Quality      
  if ($info =~ /of ([A-Za-z ]+) quality/) {
    $$id{"quality"} = $1;
  }

  # Emits darkness or light
  if ($info =~ /(emits darkness|emits light)/) {
    push(@{$$id{"stats"}}, $1);
    $$id{"cat"}{$1} = 1;
  }

  # Materials, figure out primary material
  if ($info =~ /made of,? ?(.+?), (feather|worth)/) {
    @{$$id{"materials"}} = split(/\s*,\s*/, $1);
    my $qmat = $$id{"materials"}[0];
    my $qmax = -1;
    foreach my $mat (@{$$id{"materials"}}) {
      if ($mat =~ /^(\d+)% (.+)$/) {
        if ($1 >= $qmax) {
          $qmax = $1;
          $qmat = $2;
        }
      }
    }
    $$id{"material"} = $qmat;
    $$id{"cat"}{$qmat} = 1;
  }

  # Check for nun relics
  if (defined($$id{"full"}) && $$id{"full"} =~ / \(holy\)/) {
    $$id{"cat"}{"relic"} = 1;
  }

  # Set item class
  $$id{"class"} = "Item";
  if ($info =~ /The weapon skill to best use this in combat/) {
    $$id{"class"} = "Weapon";
  } elsif (defined($$id{"type"}) && $$id{"type"} ne "") {
    $$id{"class"} = "Armour";
  }

  # Check if the weight data is for FW'd item
  if (defined($$id{"full"})) {
    if ($$id{"full"} =~ / <(yellow|green|white) glow>$/) {
      $$id{"kg"} = "";
      $$id{"weight"} = "";
    } else {
      if (defined($$id{"kg"}{$$id{"full"}})) {
        $$id{"kg"} = $$id{"kg"}{$$id{"full"}};
      } else {
        $$id{"kg"} = "";
      }
    }
  }
}

sub parse_identify($)
{
  my ($s) = @_;
  
  if ($state == 10) {
    if ($s =~ /^You $sayregex '-%-\.'$/o) {
      set_state(11, $s);
    } elsif (!defined($$id{"full"})) {
      $$id{"full"} = $s;
    } else {
      set_state(11, $s);
    }
  }
  elsif ($state == 11) {
    if ($s =~ /^:\s*(\d+\.\d+)\s+(.+)$/) {
      # Weigh in kgs, and sdesc. Will have to match these later.
      $$id{"kg"}{$2} = $1;
      $$id{"full"} = $2;
    }
    elsif ($s =~ /^You $sayregex '-#-\.'$/o) {
      set_state(12, $s);
    }
  }
  elsif ($state == 12) {
    if ($s =~ /^It is surrounded by .+? glow\.$|^It is labeled as|^This item is a present from santa to /) {
      # Just skip these
    }
    elsif ($s =~ /You $sayregex '-!-\.'$/o) {
      set_state(13, $s);
    }
    elsif ($s =~ /^It contains a tale;$/) {
      $id_tale = 1;
    }
    elsif ($s =~ /^This (.+?) is in .+? condition\.$/) {
      $$id{"type"} = $1;
    }
    elsif ($s =~ /^These (.+?) are in .+? condition\.$/) {
      $$id{"type"} = $1;
    }
    elsif ($s =~ /^It looks (light weight|a bit heavy|heavy|very very heavy|very light weight|very heavy|ridiculously heavy)\.$/) {
      $$id{"weight"} = $1;
    }
    else {
      if ($id_tale) {
        $$id{"tale"} .= $s." ";
      } else {
        my $len = length($s);
        if (defined($$id{"len"}) && $len + $$id{"len"} < 79) {
          $$id{"ldesc"} .= "\n";
        } else  {
          $$id{"ldesc"} .= " ";
        }
        $$id{"ldesc"} .= $s;
        $$id{"len"} = $len;
      }
    }
  }
  elsif ($state == 13) {
    if ($s =~ /^The following messages seem to vibrate from (.+?):$/o) {
      $$id{"sdesc"} = $1;
      set_state(14, $s);
    }
  }
  elsif ($state == 14) {
    if ($s =~ /^It is called (.+)\.$/) {
      $$id{"handles"} = $1;
      $id_tale2 = 0;
    }
    elsif ($s =~ /^It takes the following slots: (.+)\.$/) {
      $$id{"slots"} = $1;
      $id_tale2 = 0;
    }
    elsif ($s =~ /^A halo of purity surrounds it\.$/) {
      push(@{$$id{"stats"}}, "[[:category:halo of purity|good wear only]]");
      $$id{"cat"}{"halo of purity"} = 1;
      $id_tale2 = 0;
    }
    elsif ($s =~ /^An aura of blackness surrounds it\.$/) {
      push(@{$$id{"stats"}}, "[[:category:aura of blackness|evil wear only]]");
      $$id{"cat"}{"aura of blackness"} = 1;
      $id_tale2 = 0;
    }
    elsif ($s =~ /^It will (.+?) (improve|reduce) (your|the) (.+?)\.$/) {
      my ($amt, $imp, $stat) = ($1, $2, $4);
      push(@{$$id{"stats"}}, ($imp eq "improve" ? "+" : "-").$amt." ".trans_stat($stat));
      $$id{"cat"}{trans_stat($stat)} = 1 if ($imp eq "improve");
      $id_tale2 = 0;
    }
    elsif ($s =~ /^It will (raise|lower) your (.+?)\.$/) {
      my $imp = $1;
      my $stat = substr($2, 0, 3);
      push(@{$$id{"stats"}}, ($imp eq "raise" ? "+" : "-").$stat);
      $$id{"cat"}{$stat} = 1 if ($imp eq "raise");
      $id_tale2 = 0;
    }
    elsif ($s =~ /^It contains a tale;$/) {
      $id_tale2 = 1;
    }
    elsif ($s =~ /^(.+?) did the heroic deed to bring this piece of equipment before you (.+)$/) {
      parse_id_blob($2);
      if (defined($$id{"full"})) {
        $ids{$$id{"full"}} = $id;
      }
      set_state(0, $s);
    }
    elsif ($s =~ /^It has been in the game for (.+)$/) {
      parse_id_blob($1);
      $ids{$$id{"full"}} = $id;
      set_state(0, $s);
    }
    else {
      if ($id_tale2) {
        $$id{"tale"} .= $s." ";
      } else {
        push(@{$$id{"other"}}, $s);
      }
    }
  }
}


###
### Main parsing loop
###
while (defined(my $s = <STDIN>)) {
  $line++;
  chomp($s);

  # Strip ANSI colours and escapes
  $s =~ s/\e\[?.*?[\@-~]//g if ($opt_ansi);

  if
  ($s =~ /^([A-Za-z][A-Za-z\'\ -]+?)\ ($spellregexps)\ \(([A-Za-z][A-Za-z\ ]+)\)/o)
  {
    # Grab cast spells
    $spells{$1}{$line} = lc($3);
  }
  elsif ($s =~ /\| \d\d:\d\d +(\d+): (.+?) +\|$/) {
    # Grab exp worth
    my $exp = $1;
    my $tmp = $2;
    my $sdesc = substr($tmp, 0, $opt_pkills_crop);
    if (defined($monsters{$sdesc}) && $line > $monsters{$sdesc}{"line"}) {
      $monsters{$sdesc}{"exp"} = $exp;
    } else {
      $sdesc = $tmp . " (undead)";
      if (defined($monsters{$sdesc}) && $line > $monsters{$sdesc}{"line"}) {
        $monsters{$sdesc}{"exp"} = $exp;
      }
    }
  }
  elsif ($s =~ /^\[\d\d:\d\d:\d\d\] hp|^You laugh out loud\.$|^Dunk dunk$/) {
    # Ignore these lines
  }
  elsif ($s =~ /^You see nothing special\.$/o) {
    # Reset parsing state if the target obviously didn't exist
    set_state(0, $s);
  }
  elsif ($state == 0 && $s =~ /^You $sayregex '-+ .+? -+\.'$/o) {
    next unless ($opt_npc);
    # Beginning of NPC info block
    set_state(1, $s);
    undef($mob);
    $mob_sdesc = "";
    $$mob{"line"} = $line;
  }
  elsif ($state == 0 && $s =~ /^You $sayregex '-ID-\.'$/o) {
    next unless ($opt_identify);
    # Beginning of identify info block
    set_state(10, $s);
    $id = {};
    undef($id);
    $id_tale = 0;
    $id_tale2 = 0;
    $$id{"line"} = $line;
  }
  elsif ($state > 0 && $state < 10) {
    # States for NPC parsing
    parse_npc($s);
  }
  elsif ($state >= 10) {
    parse_identify($s);
  }
  elsif ($s =~ /^(.+?) is (a bit good|a bit evil|evil|good|neutral)\.$/o) {
    # Grab detect alignment
    $aligns{$1}{$line} = $2;
  }
  elsif ($s =~ /^([A-Za-z][A-Za-z\'\ .-]+?)\ is an? ([a-z-]+)\.$/o) {
    # Grab detect race
    $races{$1}{$line} = $2;
  }
  elsif ($s =~ /^You $sayregex /o) {
    # Error, reset parsing state
    set_state(0, $s);
  }
  elsif ($s =~ /^([A-Za-z][A-Za-z\'\ -]+?) starts grappling [A-Z][a-z]+\.$/) {
    $skills{$1}{$line} = "grapple";
  }
  elsif ($s =~ /^([A-Za-z][A-Za-z\'\ -]+?) pushes [A-Z][a-z]+ extremely hard\.$/) {
    $skills{$1}{$line} = "push";
  }
  elsif ($s =~ /^([A-Za-z][A-Za-z\'\ -]+?)\'s (powerful bash sends|tries to bash)/) {
    $skills{$1}{$line} = "bash";
  }
  elsif ($s =~ /^([A-Za-z][A-Za-z\'\ -]+?) (aims a fast kick at|kicks [A-Z][a-z]+ in the groin very hard|swings a foot in a long arc|performs a fast kick)/) {
    $skills{$1}{$line} = "kick";
  }
}


###
### Print out item/identify information
###
foreach my $sdesc (sort { $a cmp $b } keys %ids) {
  my $item = $ids{$sdesc};
  my $name = $$item{"sdesc"};
  $name =~ s/ labeled as .*$//;
  my $qdesc = $name;
  if ($$item{"full"} =~ / <(red|purple|white) glow>$/) {
    $name .= " <red glow>";
  }

  print "---------------------------\n".
  $qdesc."\n\n".
  "{{ Infobox ".$$item{"class"}."\n".
  "| name = ".$name."\n".
  "| description = ".$$item{"ldesc"}."\n";

  if (defined($$item{"tale"}) && $$item{"tale"} ne "") {
    print "| tale = ".$$item{"tale"}."\n";
  }

  if (defined($$item{"type"})) {
    print "| type = ".$$item{"type"}."\n";
  }

  if (defined($$item{"stats"})) {
    print "| stats = ".join(", ", @{$$item{"stats"}})."\n";
  } else {
    print "| stats = \n";
  }

  print
  "| weight = ".$$item{"weight"}."\n".
  "| kg = ".$$item{"kg"}."\n".
  "| sacvalue = \n".
  "| handles = ".$$item{"handles"}."\n";
  
  if (defined($$item{"materials"})) {
    print "| material = ".join(", ", @{$$item{"materials"}})."\n";
  }

  print
  "| size = ".$$item{"size"}."\n".
  "| quality = ".$$item{"quality"}."\n".
  "| from = \n";

  if ($$item{"class"} eq "Weapon") {
    print
    "| wep1 = \n".
    "| wep2 = \n";
  }

  if (defined($$item{"other"})) {
    print "| other = ".join("\n", @{$$item{"other"}})."\n";
  }

  print
  "}}\n";

  foreach my $cat (keys %{$$item{"cat"}}) {
    print "[[Category:$cat]]\n";
  }

  print "---------------------\n";
}


###
### Print out the NPC information
###
foreach my $sdesc (sort { $a cmp $b } keys %monsters) {
  my $mob = $monsters{$sdesc};

  print "---------------------------
$sdesc

{{ NPC
| name = ".$$mob{"sname"}."
| description = ".$$mob{"ldesc"}."
| eqs = ";

  foreach my $slot (sort { $a cmp $b } keys %{$$mob{"eqs"}}) {
    my $s = $$mob{"eqs"}{$slot};
    $s =~ s/ \(glowing\)//;
    my $k = $s;
    $k =~ s/ <[a-z]+ glow>$//;
    if ($k eq $s) {
      print "$slot: [[$s]]\n\n";
    } else {
      print "$slot: [[$k|$s]]\n\n";
    }
  }
  
  print "none" unless (scalar keys %{$$mob{"eqs"}} > 0);
  print "\n";
        
  if ($sdesc =~ / \(undead\)$/) {
    print "| undead = x\n";
  }

  # Check if we find a matching alignment: find a cast of 'detect alignment'
  # with matching short name and withing appropriate distance from the mob look.
  my $mname = $$mob{"sname"};
  my $mline = $$mob{"line"};

  print "| alignment = ";
  if (defined($aligns{$mname})) {
    my $qoffs = 1000;
    my $qline = -1;
    foreach my $aline (sort { $a <=> $b } keys %{$aligns{$mname}}) {
      my $offs = ($aline - $mline);
      if ($offs < $qoffs) {
        $qoffs = $offs;
        $qline = $aline;
      }
    }
    if ($qline >= 0) {
      print STDERR "ALIGN: $mname ($sdesc) \@ #$qline ($qoffs)\n" if ($opt_debug);
      print $aligns{$mname}{$qline};
    }
  }
  print "\n".
  "| race = ";
  
  # Check for race
  if (defined($races{$mname})) {
    my $qoffs = 1000;
    my $qline = -1;
    foreach my $aline (sort { $a <=> $b } keys %{$races{$mname}}) {
      my $offs = ($aline - $mline);
      if ($offs < $qoffs) {
        $qoffs = $offs;
        $qline = $aline;
      }
    }
    if ($qline >= 0) {
      print STDERR "RACE: $mname ($sdesc) \@ #$qline ($qoffs)\n" if ($opt_debug);
      print $races{$mname}{$qline};
    }
  }
  print "\n";

  
  # Spells
  my $hasspells = 0;
  if (defined($spells{$mname}) && length($mname) >= 4) {
    my $maxdist = length($mname) * 150 * $opt_dist * length($mname);
    my $mindist = length($mname) * -5 * $opt_dist * length($mname);
    my %mspells = ();
    foreach my $sline (sort { $a <=> $b } keys %{$spells{$mname}}) {
      my $offs = $sline - $mline;
#      print STDERR "TEST $mname @ $offs vs $mindist...$maxdist : '".$spells{$mname}{$sline}."'\n";
      if ($offs < $maxdist && $offs > $mindist) {
        print STDERR "SPELL $mname \@ $offs / $mindist...$maxdist : '".$spells{$mname}{$sline}."'\n" if ($opt_debug);
        $mspells{$spells{$mname}{$sline}} = 1;
        $hasspells++;
      }
    }
    my $n = 1;
    foreach my $spell (sort { $a cmp $b } keys %mspells) {
      print "| spell".($n < 2 ? "" : $n)." = $spell\n";
      $n++;
    }
  }

unless ($hasspells > 0) {
print "| spell = 
| spell# = 
";
}

  print "
| area = ";

  if ($opt_getlocs) {
    my $cont = lc($$mob{"continent"});
    my $xc = $$mob{"xc"};
    my $yc = $$mob{"yc"};
    parse_locfile($cont);
    if (defined($locations{$cont}{$xc}{$yc})) {
      my $nm = $locations{$cont}{$xc}{$yc}[0];
      $nm =~ s/^[\@]//;
      print $nm;
    } else {
      print $$mob{"area"};
    }
  } else {
    print $$mob{"area"};
  }


print "\n";

  # Skills
  my $hasskills = 0;
  if (defined($skills{$mname}) && length($mname) >= 4) {
    my $maxdist = length($mname) * 150 * $opt_dist * length($mname);
    my $mindist = length($mname) * -5 * $opt_dist * length($mname);
    my %mskills = ();
    foreach my $sline (sort { $a <=> $b } keys %{$skills{$mname}}) {
      my $offs = $sline - $mline;
#      print STDERR "TEST $mname @ $offs vs $mindist...$maxdist : '".$skills{$mname}{$sline}."'\n";
      if ($offs < $maxdist && $offs > $mindist) {
        print STDERR "SPELL $mname \@ $offs / $mindist...$maxdist : '".$skills{$mname}{$sline}."'\n" if ($opt_debug);
        $mskills{$skills{$mname}{$sline}} = 1;
        $hasskills++;
      }
    }
    my $n = 1;
    foreach my $skill (sort { $a cmp $b } keys %mskills) {
      print "| skill".($n < 2 ? "" : $n)." = $skill\n";
      $n++;
    }
  }

unless ($hasskills > 0) {
print "| skill = 
| skill# = 
";
}


  print "
| exp = ";

  if (defined($$mob{"exp"})) {
    if ($$mob{"exp"} > 1000000) {
      printf "%1.2fM", $$mob{"exp"} / 1000000.0;
    } elsif ($$mob{"exp"} > 1000) {
      printf "%dk", $$mob{"exp"} / 1000;
    } else {
      printf "%d", $$mob{"exp"};
    }
  }

print "
| GAP = 
| PAP = 
| kerbholz = 
| gender = ";

  if (defined($$mob{"gender"})) {
    print $$mob{"gender"};
  }

print "
| other = 
}}
---------------------
";
}