packages feed

EtaMOO-0.1.0.0: src/MOO/Command.hs

{-# LANGUAGE OverloadedStrings #-}

-- | MOO command parsing and execution
module MOO.Command (

  -- * Data Structures
    Command(..)

  -- * Parsing Typed Commands
  , parseWords
  , parseCommand

  -- * Executing MOO Code
  , runCommand

  ) where

import Control.Monad (liftM, void, foldM, join)
import Data.Char (isSpace, isDigit)
import Data.Monoid
import Data.Text (Text)
import Text.Parsec
import Text.Parsec.Text (Parser)

import qualified Data.IntSet as IS
import qualified Data.Text as T
import qualified Data.Vector as V

import MOO.Types
import {-# SOURCE #-} MOO.Task
import MOO.Object
import MOO.Verb
import {-# SOURCE #-} MOO.Network

commandWord :: Parser Text
commandWord = do
  word <- many1 wordChar
  spaces
  return (T.concat word)

  where wordChar = T.singleton `liftM` satisfy nonspecial <|>
                   T.pack      `liftM` quotedChars        <|>
                   T.singleton `liftM` backslashChar      <|>
                   trailingBackslash

        nonspecial '\"' = False
        nonspecial '\\' = False
        nonspecial  c   = not $ isSpace c

        quotedChars     = between (char '"') quoteEnd $
                          many (noneOf "\"\\" <|> backslashChar)
        quoteEnd        = void (char '"') <|> eof <|> void trailingBackslash

        backslashChar   = try (char '\\' >> anyChar)

        trailingBackslash = try (char '\\' >> eof) >> return ""

commandWords :: Parser [Text]
commandWords = between spaces eof $ many commandWord

builtinCommand :: Parser Text
builtinCommand = say <|> emote <|> eval
  where say   = char '\"' >> return "say"
        emote = char ':'  >> return "emote"
        eval  = char ';'  >> return "eval"

command :: Parser (Text, Text)
command = between spaces eof $ do
  verb <- builtinCommand <|> commandWord <|> return ""
  argstr <- T.pack `liftM` many anyChar
  return (verb, argstr)

matchPrep :: [Text] -> (Text, (PrepSpec, Text), Text)
matchPrep = matchPrep' id prepPhrases
  where matchPrep' dobj _ [] = (T.unwords $ dobj [], (PrepNone, ""), "")
        matchPrep' dobj ((spec,phrase):phrases) args
          | phrase == map T.toCaseFold argsPhrase =
            (T.unwords $ dobj [], (spec, T.unwords argsPhrase), T.unwords iobj)
          | otherwise = matchPrep' dobj phrases args
          where (argsPhrase, iobj) = splitAt (length phrase) args
        matchPrep' dobj [] (arg:args) =
          matchPrep' (dobj . (arg :)) prepPhrases args

-- | A structure describing a player's parsed command
data Command = Command {
    commandVerb     :: Text
  , commandArgs     :: [Text]
  , commandArgStr   :: Text
  , commandDObjStr  :: Text
  , commandPrepSpec :: PrepSpec
  , commandPrepStr  :: Text
  , commandIObjStr  :: Text
  } deriving Show

-- | Split a typed command into words according to the MOO rules for quoting
-- and escaping.
parseWords :: Text -> [Text]
parseWords argstr = args
  where Right args = parse commandWords "" argstr

-- | Return a 'Command' value describing the arguments of a typed command as
-- parsed into verb, preposition, direct and indirect object, etc.
parseCommand :: Text -> Command
parseCommand cmd = Command verb args argstr dobjstr prepSpec prepstr iobjstr
  where Right (verb, argstr) = parse command "" cmd
        args = parseWords argstr
        (dobjstr, (prepSpec, prepstr), iobjstr) = matchPrep args

objectNumber :: Parser ObjId
objectNumber = liftM read $ between (char '#') eof $ many1 (satisfy isDigit)

matchObject :: ObjId -> StrT -> MOO ObjId
matchObject player str
  | T.null str = return nothing
  | otherwise  = case parse objectNumber "" str of
    Right oid -> do
      obj <- getObject oid
      case obj of
        Just _  -> return oid
        Nothing -> matchObject' player str
    Left _ -> matchObject' player str

  where matchObject' :: ObjId -> StrT -> MOO ObjId
        matchObject' player str = case str' of
          "me"   -> return player
          "here" -> maybe nothing (objectForMaybe . objectLocation) `liftM`
                    getObject player
          _      -> do
            maybePlayer <- getObject player
            case maybePlayer of
              Nothing      -> return failedMatch
              Just player' -> do
                let holding   = objectContents player'
                    maybeRoom = objectLocation player'
                roomContents <-
                  maybe (return IS.empty)
                  (liftM (maybe IS.empty objectContents) . getObject)
                  maybeRoom
                matchName str' $ IS.toList (holding `IS.union` roomContents)
          where str' = T.toCaseFold str

        matchName :: StrT -> [ObjId] -> MOO ObjId
        matchName str = liftM (uncurry matchResult) .
                        foldM (matchName' str) ([], [])
        matchName' str matches@(exact, prefix) oid = do
          maybeAliases <- readProperty oid "aliases"
          maybeObj <- getObject oid
          let aliases = case maybeAliases of
                Just (Lst v) -> V.toList v
                _            -> []
              names = maybe id ((:) . Str . objectName) maybeObj aliases
          return $ case searchNames str names of
            ExactMatch  -> (oid : exact, prefix)
            PrefixMatch -> (exact, oid : prefix)
            NoMatch     -> matches

        matchResult :: [ObjId] -> [ObjId] -> ObjId
        matchResult [oid] _     = oid
        matchResult []    [oid] = oid
        matchResult []    []    = failedMatch
        matchResult _     _     = ambiguousMatch

        searchNames :: StrT -> [Value] -> Match
        searchNames str = mconcat . map (nameMatch str)

        nameMatch :: StrT -> Value -> Match
        nameMatch str (Str name)
          | name'               == str = ExactMatch
          | T.take strLen name' == str = PrefixMatch
          | otherwise                  = NoMatch
          where name'  = T.toCaseFold name
                strLen = T.length str
        nameMatch _ _ = NoMatch

        nothing        = -1
        ambiguousMatch = -2
        failedMatch    = -3

data Match = NoMatch | PrefixMatch | ExactMatch

instance Monoid Match where
  mempty = NoMatch

  NoMatch `mappend` match      = match
  _       `mappend` ExactMatch = ExactMatch
  match   `mappend` _          = match

-- | Execute a typed command by locating and calling an appropriate MOO verb
-- for the current player, matching @dobj@ and @iobj@ objects against the
-- strings in the typed command.
runCommand :: Command -> MOO Value
runCommand command = do
  player <- getPlayer
  dobj <- matchObject player (commandDObjStr command)
  iobj <- matchObject player (commandIObjStr command)

  room <- (objectForMaybe . join . fmap objectLocation) `liftM` getObject player
  maybeVerb <- (getFirst . mconcat . map First) `liftM`
               mapM (locateVerb dobj iobj) [player, room, dobj, iobj]
  case maybeVerb of
    Just (this, spec) -> callCommandVerb player spec this command (dobj, iobj)
    Nothing           -> do
      maybeVerb <- findVerb verbPermX "huh" room
      case maybeVerb of
        (Just oid, Just verb) ->
          callCommandVerb player (oid, verb) room command (dobj, iobj)
        _ -> notify player "I couldn't understand that." >> return nothing

  where locateVerb :: ObjId -> ObjId -> ObjId ->
                      MOO (Maybe (ObjId, (ObjId, Verb)))
        locateVerb dobj iobj this = do
          location <- findVerb acceptable (commandVerb command) this
          case location of
            (Just oid, Just verb) -> return $ Just (this, (oid, verb))
            _                     -> return Nothing
          where acceptable verb =
                  objMatch this (verbDirectObject   verb) dobj &&
                  objMatch this (verbIndirectObject verb) iobj &&
                  prepMatch (verbPreposition verb) (commandPrepSpec command)