view www/common.inc.php @ 2763:78ad0e51b7b5

Improve wizards.txt parser, add functionality for specifying alternative / additional names as some wizards have used more than one. Also other improvements in wizard data handling.
author Matti Hamalainen <ccr@tnsp.org>
date Tue, 12 Mar 2024 15:47:58 +0200
parents 06ce4399639c
children
line wrap: on
line source

<?php
// Globals and definitions
$errorSet = FALSE;
$errorMsgs = [];
$statusSet = 0;
$statusMsg = "";


// Location flags (translated to PHP from liblocfile.h)
define("LOCF_NONE"              , 0x000000);

// Marker types
define("LOCF_M_SCENIC1"         , 0x000001);   // '?' Scenic marker
define("LOCF_M_SCENIC2"         , 0x000002);   // '%' Shrine marker/etc
define("LOCF_M_PCITY"           , 0x000004);   // 'C' Player city
define("LOCF_M_CITY"            , 0x000008);   // 'c' City
define("LOCF_M_MASK"            , 0x00000f);

// Location types
define("LOCF_T_SHRINE"          , 0x000010);   // 'S' Raceshrine
define("LOCF_T_GUILD"           , 0x000020);   // 'G' Guild
define("LOCF_T_SS"              , 0x000040);   // 'P' Player guild/Secret Society
define("LOCF_T_MONSTER"         , 0x000080);   // 'M' Special monster
define("LOCF_T_TRAINER"         , 0x000100);   // 'T' Guild trainer
define("LOCF_T_FORT"            , 0x000200);   // 'F' Regions fort
define("LOCF_T_MASK"            , 0x00fff0);
define("LOCF_MASK"              , (LOCF_M_PCITY | LOCF_M_CITY | LOCF_T_MASK));

// Extra flags
define("LOCF_INVIS"             , 0x010000);   // '-' Invisible marker / Don't show label
define("LOCF_CLOSED"            , 0x020000);   // '!' Area is CLOSED
define("LOCF_INSTANCED"         , 0x040000);   // 'I' Location is "instanced" for each player
define("LOCF_INVALID"           , 0x400000);   // Possibly invalid location
define("LOCF_NOMARKER"          , 0x800000);   // Location has no marker in mapdata or explicitly defined
define("LOCF_Q_MASK"            , 0xff0000);


// Creator roles
define("AUTHOR_ORIG"            , 0x000001);   // '@' Original area creator
define("AUTHOR_RECODER"         , 0x000002);   // '!' Converter or recoder of area
define("AUTHOR_MAINTAINER"      , 0x000004);   // '%' Maintainer
define("AUTHOR_EXPANDER"        , 0x000008);   // '&' Expander, adding new things


// Area name flags
define("NAME_ORIG"              , 0x000001);   // '@' Original area name


// Timestamp accuracy
define("TS_ACC_DEFAULT"         , 0);
define("TS_ACC_KNOWN"           , '!');
define("TS_ACC_GUESSTIMATE"     , '?');
define("TS_ACC_APPROXIMATE"     , '#');


// Location types table
define("LTI_PAGE_DESC"    , 0);
define("LTI_MENU_TITLE"   , 1);
define("LTI_INVERT_FLAGS" , 2);
define("LTI_FLAGS"        , 3);

$locationTypes =
[
//ID  =>  Page description            , Menu title   , InvFlags, Mask flags
  ""  => ["ALL"                       , "EVERYTHING" , TRUE  , 0 ],
  "A" => ["Areas"                     , "Areas"      , TRUE  , LOCF_MASK ],
  "S" => ["Race and other shrines"    , "Shrines"    , FALSE , LOCF_T_SHRINE ],
  "G" => ["Guilds"                    , "Guilds"     , FALSE , LOCF_T_GUILD ],
  "P" => ["Outworld secret societies" , "SS"         , FALSE , LOCF_T_SS ],
  "M" => ["Special outworld monsters" , "Monsters"   , FALSE , LOCF_T_MONSTER ],
  "c" => ["Major cities"              , "Cities"     , FALSE , LOCF_M_CITY ],
  "T" => ["Guild trainers"            , "Trainers"   , FALSE , LOCF_T_TRAINER ],
  "C" => ["Player cities"             , "PCities"    , FALSE , LOCF_M_PCITY ],
//"F" => ["Regional Forts"            , "Forts"      , FALSE , LOCF_T_FORT ],
];



function mpGetLocationTypePrefix($flags)
{
  switch ($flags & LOCF_M_MASK)
  {
    case LOCF_M_PCITY: return "PCITY ";
    case LOCF_M_CITY: return "CITY ";
    default:
      switch ($flags & LOCF_T_MASK)
      {
        case LOCF_T_SHRINE: return "SHRINE ";
        case LOCF_T_GUILD: return "GUILD ";
        case LOCF_T_SS: return "SS ";
        case LOCF_T_MONSTER: return "MONSTER ";
        case LOCF_T_TRAINER: return "TRAINER ";
        case LOCF_T_FORT: return "FORT ";
        default: return "";
      }
  }
}


function mpGetLocationTypeStr($flags)
{
  switch ($flags & LOCF_M_MASK)
  {
    case LOCF_M_PCITY: return "Player City";
    case LOCF_M_CITY: return "Major City";
    default:
      switch ($flags & LOCF_T_MASK)
      {
        case LOCF_T_SHRINE: return "Race Shrine";
        case LOCF_T_GUILD: return "Guild";
        case LOCF_T_SS: return "SS";
        case LOCF_T_MONSTER: return "Monster";
        case LOCF_T_TRAINER: return "Trainer";
        case LOCF_T_FORT: return "Fort";
        default: return "Area";
      }
  }
}


function mpError($msg)
{
  global $errorSet, $errorMsgs;
  $errorSet = TRUE;
  $errorMsgs[] = $msg;
  return FALSE;
}


function mpTrimIfString($val)
{
  if (is_string($val))
    return trim($val);
  else
    return $val;
}


function mpGetRequestItem($name, $default = "", $allowGet = FALSE)
{
  if ($allowGet)
    return isset($_REQUEST[$name]) ? mpTrimIfString($_REQUEST[$name]) : $default;
  else
    return isset($_POST[$name]) ? mpTrimIfString($_POST[$name]) : $default;
}


function chentities($str)
{
  return htmlentities($str, ENT_NOQUOTES, "UTF-8");
}


function mpLocFormatTime($loc)
{
  switch ($loc["added_accuracy"])
  {
    case TS_ACC_GUESSTIMATE: return strftime("%b %Y", $loc["added"]);
    case TS_ACC_APPROXIMATE: return strftime("%b %Y", $loc["added"]);
    case TS_ACC_DEFAULT: return strftime("%d %b %Y*", $loc["added"]);
    default: return strftime("%d %b %Y", $loc["added"]);
  }
}


function mpPrintPageHeader($title, $extra = "", $bodyExtra = "", $useMenu = TRUE)
{
  global $pageTitle, $pageMapURL, $pageCSS, $pageCharset, $pageLang;

  echo
    "<!DOCTYPE html>\n".
    "<html".(isset($pageLang) ? " lang=\"".$pageLang."\"" : "").">\n".
    "<head>\n".
    "  <meta charset=\"".$pageCharset."\">\n".
    "  <title>".strip_tags($pageTitle)."</title>\n".
    "  <meta name=\"keywords\" content=\"batmud,map,laenor,lucentium,rothikgen,desolathya,furnachia,mud,mush,mmorpg,mmo,massively,multiplayer,online,network,game,ggr,pupunen\"/>\n".
    "  <link rel=\"shortcut icon\" href=\"/maps/favicon.ico\" type=\"image/x-icon\" />\n".
    $extra.
    "  <link rel=\"stylesheet\" href=\"".$pageCSS."\" type=\"text/css\" />\n".
    "</head>\n".
    "<body".$bodyExtra.">\n";

  if ($useMenu)
    require "menu.inc.php";

  echo "<div id=\"contents\">\n";

/*
  // CETA banner expires on 27.8.2017
  if (time() < mktime(0, 0, 0, 8, 27, 2017))
  {
    echo "<div style=\"text-align: center;\"><a href=\"https://suomiceta.wordpress.com/\"><img src=\"/cropped-suomiceta_nettisivubanneri.png\" alt=\"Suomi-CETA\" /></a></div>\n";
  }
*/
}


function mpPrintPageFooter($useContents = TRUE)
{
  if ($useContents)
    echo "</div>\n";

  echo "</body>\n</html>\n";
}


function mpGetMTimeStr($mtime)
{
//  return date("D d.m.Y H:i:s T", $mtime);
  return date("d.m.Y H:i", $mtime);
}


function mpFingerURL($name)
{
  return "https://www.bat.org/char/".$name;
}


function burl($name)
{
//  return "<a href=\"".mpFingerURL($name)."\">".$name."</a>";
  return "<b>".$name."</b>";
}


function mpGetMapURLLink($data, $link, $name, $gmap)
{
  global $pageGMapURL, $continentList;
  if ($link)
  {
    $c = $continentList[$data["continent"]];

    $str = "<a class=\"mapLink\" href=\"".
      $data["continent"].".html#loc".$data["x"]."_".$data["y"].
      "\">".chentities($name)."</a>";

    if (isset($pageGMapURL) && $gmap &&
        $c[CTI_HAS_MAP] == TRUE && $c[CTI_REG_CONT] == TRUE) // hasmap & read regular cont must be true
    {
      $str .= " <a class=\"gmapLink\" title=\"GMap link\" ".
      "href=\"".$pageGMapURL."?x=".$data["globalx"].
      "&amp;y=".$data["globaly"]."&amp;zoom=10\">&#x2605;</a>";
    }
    return $str;
  }
  else
    return "<b>".chentities($name)."</b>";
}


function mpGetMapLink($data, $link, $gmap, $ltype)
{
  return mpGetMapURLLink($data, $link,
    ($ltype ? mpGetLocationTypePrefix($data["flags"]) : "").$data["name"], $gmap);
}


function mpChConv($str)
{
  global $pageCharset;
  return iconv("UTF-8", $pageCharset, $str);
}


function mpAddLocNames(&$list, $input)
{
  $list = [];
  foreach ($input as $name)
  {
    $nflags = 0;
    $npos = 0;

    switch (substr($name, 0, 1))
    {
      case "@": $npos++; $nflags = AUTHOR_ORIG; break;
      case "!": $npos++; $nflags = AUTHOR_RECODER; break;
      case "%": $npos++; $nflags = AUTHOR_MAINTAINER; break;
      case "&": $npos++; $nflags = AUTHOR_EXPANDER; break;
    }

    $list[] = [
      "name" => substr($name, $npos),
      "flags" => $nflags
    ];
  }
}


function mpGetGlobalCoords($continent, $xc, $yc, &$gx, &$gy)
{
  global $continentList, $worldMap;
  if (isset($continentList[$continent]))
  {
    $gx = $worldMap["ox"] + $continentList[$continent][CTI_XOFFS] + $xc - 1;
    $gy = $worldMap["oy"] + $continentList[$continent][CTI_YOFFS] + $yc - 1;
    return TRUE;
  }
  else
    return FALSE;
}


function mpParseLocFile($filename, &$locations, $applyFilter, $invertFilter, $filterMask)
{
  global $locationTypes;

  $file = @fopen(basename($filename.".loc"), "r");
  if (!$file) return FALSE;

  while (!feof($file))
  {
    $inLine = mpChConv(trim(fgets($file)));

    // Ignore comments and empty lines
    if (strlen($inLine) == 0 || $inLine[0] == "#")
      continue;

    if (preg_match("/^(\d+)\s*;\s*(\d+)\s*;\s*([0-3][A-Za-z%!\?\-]*)\s*;\s*([^;]+)\s*;\s*(.*?)\s*;\s*(.*?)\s*;\s*(.*?)\s*;\s*(.*?)\s*$/", $inLine, $record))
    {
      // Get location names
      mpAddLocNames($names, preg_split("/\s*\|\s*/", $record[4]));

      // Parse the flags, first skip the label orientation
      $rawflags = $record[3];
      for ($fi = 0; $fi < strlen($rawflags) &&
        (ctype_digit($rawflags[$fi]) || $rawflags[$fi] == ' '); $fi++);

      $rawflags = substr($rawflags, $fi);
      $id = $rawflags.$names[0]["name"];

      $flags = LOCF_NONE;
      for ($fi = 0; $fi < strlen($rawflags); $fi++)
      {
        switch ($rawflags[$fi])
        {
          case "?": $flags |= LOCF_M_SCENIC1; break;
          case "%": $flags |= LOCF_M_SCENIC2; break;
          case "C": $flags |= LOCF_M_PCITY; break;
          case "c": $flags |= LOCF_M_CITY; break;

          case "S": $flags |= LOCF_T_SHRINE; break;
          case "G": $flags |= LOCF_T_GUILD; break;
          case "P": $flags |= LOCF_T_SS; break;
          case "M": $flags |= LOCF_T_MONSTER; break;
          case "T": $flags |= LOCF_T_TRAINER; break;
          case "F": $flags |= LOCF_T_FORT; break;

          case "-": $flags |= LOCF_INVIS; break;
          case "!": $flags |= LOCF_CLOSED; break;
          case "I": $flags |= LOCF_INVALID; break;

          default:
            mpError("Location '<b>".$id."</b>' has invalid flag '<b>".$rawflags[$fi]."</b>' <=> ".$filename.")");
        }
      }

      // Is the location visible? Does it match the filter, if any?
      if (($flags & LOCF_INVIS) == 0 &&
          (!$applyFilter || (($flags & $filterMask) ? TRUE : FALSE) != $invertFilter))
      {
        // Check for duplicate locations
        if (isset($locations[$id]))
        {
          mpError("Location '<b>".$id."</b>' already defined (".$locations[$id]["continent"]." <=> ".$filename.")");
          continue;
        }

        // Create primary location name
        $name = $names[0]["name"];

        if ($flags & LOCF_CLOSED)
        {
          $name .= " (CLOSED)";
          $ffreeform = $record[8]." This location is closed.";
        }
        else
        {
          $ffreeform = $record[8];
        }

        mpGetGlobalCoords($filename, $record[1], $record[2], $glx, $gly);

        // Split author names
        if (strlen($record[5]) > 0)
          mpAddLocNames($authors, preg_split("/\s*,\s*/", $record[5]));
        else
          $authors = [];

        // Get timestamp
        $stamp = $record[6];
        $added = -1;
        $added_accuracy = TS_ACC_DEFAULT;
        if (strlen($stamp) > 0)
        {
          switch ($stamp[0])
          {
            case TS_ACC_KNOWN:
            case TS_ACC_GUESSTIMATE:
            case TS_ACC_APPROXIMATE:
              $added_accuracy = $stamp[0];
              $stamp = substr($stamp, 1);
              break;
          }

          if (sscanf($stamp, "%02d.%02d.%04d", $aday, $amonth, $ayear) == 3)
          {
            if (($added = mktime(0, 0, 0, $amonth, $aday, $ayear)) === FALSE)
              $added = -1;
          }
          else
          {
            mpError("Location <b>".$record[4]."</b> has invalid timestamp '".$record[6]."'");
          }
        }

        // Add location to array of locations
        $locations[$id] = [
          "name" => $name, // NOTE! Name field should be the first one in this list due to how we are abusing asort() etc
          "names" => $names,
          "continent" => $filename,
          "x" => $record[1],
          "y" => $record[2],
          "globalx" => $glx,
          "globaly" => $gly,
          "flags" => $flags,
          "authors" => $authors,
          "added" => $added,
          "added_accuracy" => $added_accuracy,
          "url" => strlen($record[7]) ? $record[7] : NULL,
          "freeform" => strlen($ffreeform) ? $ffreeform : NULL,
        ];
      }
    } else
    {
      mpError($inLine);
      //return FALSE;
    }
  }

  fclose($file);
  return TRUE;
}


function mpParseLocFiles($applyFilter,
  $invertFilter = TRUE,
  $filterMask = LOCF_M_PCITY | LOCF_T_FORT)
{
  global $continentList;
  $locations = [];

  foreach ($continentList as $id => $data)
  {
    if ($data[CTI_REG_CONT])
      mpParseLocFile($id, $locations, $applyFilter, $invertFilter, $filterMask);
  }

  return $locations;
}


function mpStoreWizInfoField(&$wizInfo, $field, $data)
{
  $str = trim($data);
  if (strlen($str) > 0)
    $wizInfo[$field] = $str;
}


function mpStoreWizInfoRecordInfo($nline, &$wizInfo, $record)
{
  // Parse and check names
  $allnames = preg_split("/\s*\|\s*/", trim($record[1]));
  foreach ($allnames as $name)
  {
    if ($name == "" ||
        $name != strtoupper(substr($name, 0, 1)).strtolower(substr($name, 1)))
    {
      mpError("Invalid name in wizard record (line #".$nline."): '".$name."'.");
    }
  }

  // Use first name as primary "key"
  $keyname = $allnames[0];
  if (isset($wizInfo[$keyname]))
  {
    mpError("Wizard record '".$keyname."' is already set on line #".
      $wizInfo[$keyname]["nline"].", vs line #".$nline);
    return FALSE;
  }
  else
  {
    $data = [
      "name" => $keyname,
      "names" => array_slice($allnames, 1),
      "areas" => 0,
      "nline" => $nline,
    ];

    $countries = array_map(
      function ($item)
      {
        return strtolower($item);
      },
      array_filter(preg_split("/\s*\|\s*/", $record[2]),
      function ($item)
      {
        return $item != "";
      }));

    if (count($countries) > 0)
      $data["countries"] = $countries;

    mpStoreWizInfoField($data, "homeURL", $record[3]);
    mpStoreWizInfoField($data, "imageURL", $record[4]);
    mpStoreWizInfoField($data, "desc", $record[5]);

    $wizInfo[$keyname] = $data;

    return TRUE;
  }
}


function mpParseWizInfoFile($filename, &$wizInfo)
{
  $startLine = $nline = 0;
  if (($file = @fopen($filename, "r")) === false)
    return FALSE;

  $contMode = FALSE;
  while (!feof($file))
  {
    $line = mpChConv(trim(fgets($file)));
    $nline++;

    if (strlen($line) == 0 || $line[0] == "#")
      continue;

    if ($contMode)
    {
      if (substr($line, -1, 1) == '$')
      {
        $record[5] .= " ".trim(substr($line, 0, -1));
        $contMode = FALSE;
        mpStoreWizInfoRecordInfo($startLine, $wizInfo, $record);
      }
      else
      {
        $record[5] .= " ".trim($line);
      }
    }
    else
    if (preg_match("/^([A-Za-z\|]+);([a-z\|]+)?;(https?:\/\/[^;]+|bat)?;([^;]+)?;([^\$]*)\\\$$/", $line, $record))
      mpStoreWizInfoRecordInfo($nline, $wizInfo, $record);
    else
    if (preg_match("/^([A-Za-z\|]+);([a-z\|]+)?;(https?:\/\/[^;]+|bat)?;([^;]+)?;(.*)$/", $line, $record))
    {
      $startLine = $nline;
      $contMode = TRUE;
    }
    else
      mpError("Wizard record line #".$nline.": ".$line);
  }

  fclose($file);
  return TRUE;
}


function mpMakeWizInfoAliases($wizInfo)
{
  $aliases = [];
  foreach ($wizInfo as $name => $data)
  {
    foreach ($data["names"] as $nname)
      $aliases[$nname] = $name;
  }
  return $aliases;
}


function mpReadWizInfoFiles()
{
  $wizInfo = [];
  mpParseWizInfoFile("wizards.txt", $wizInfo);
  return $wizInfo;
}


function mpPrintExtraBoxAlphaList($prefix, $table, $class = FALSE)
{
  echo "<div id=\"extraBox\"".($class !== FALSE ? " class=\"".$class."\"" : "").">\n";

  foreach ($table as $alpha => $locs)
  {
    $letter = strtoupper($alpha);
    echo "  <a href=\"#".$prefix.$letter."\">".$letter."</a>&nbsp;\n";
  }

  echo "</div>\n";
}


function mpSpecialDate($where, $index)
{
  global $specialDate;
  $str = "";

  switch ($specialDate)
  {
    case "aprilli":
      for ($ni = 0; $ni < 4; $ni++)
      {
        $str .= "<img src=\"img/".
          (($ni % 2 == 0 && $where == "index") ? "skull.gif" : "uc.gif").
          "\" alt=\"ZOMG\" />";
      }
      break;
  }

  return $str;
}

?>