packages feed

EtaMOO-0.1.0.0: src/MOO/Builtins/Objects.hs

{-# LANGUAGE OverloadedStrings #-}

module MOO.Builtins.Objects ( builtins ) where

import Control.Concurrent.STM
import Control.Monad (when, unless, liftM, void, join)
import Data.Maybe
import Data.Set (Set)

import qualified Data.HashMap.Strict as HM
import qualified Data.IntSet as IS
import qualified Data.Set as S
import qualified Data.Text as T
import qualified Data.Vector as V

import MOO.Builtins.Common
import MOO.Database
import MOO.Types
import MOO.Task
import MOO.Object
import MOO.Verb
import MOO.Network
import MOO.Unparser
import MOO.Parser
import {-# SOURCE #-} MOO.Compiler
import MOO.AST

{-# ANN module ("HLint: ignore Use camelCase" :: String) #-}

-- | § 4.4.3 Manipulating Objects
builtins :: [BuiltinSpec]
builtins = [
    ("create"        , (bf_create        , Info 1 (Just 2) [TObj, TObj] TObj))
  , ("chparent"      , (bf_chparent      , Info 2 (Just 2) [TObj, TObj] TAny))
  , ("valid"         , (bf_valid         , Info 1 (Just 1) [TObj]       TInt))
  , ("parent"        , (bf_parent        , Info 1 (Just 1) [TObj]       TObj))
  , ("children"      , (bf_children      , Info 1 (Just 1) [TObj]       TLst))
  , ("recycle"       , (bf_recycle       , Info 1 (Just 1) [TObj]       TAny))
  , ("object_bytes"  , (bf_object_bytes  , Info 1 (Just 1) [TObj]       TInt))
  , ("max_object"    , (bf_max_object    , Info 0 (Just 0) []           TObj))

  , ("move"          , (bf_move          , Info 2 (Just 2) [TObj, TObj] TAny))
  , ("properties"    , (bf_properties    , Info 1 (Just 1) [TObj]       TLst))
  , ("property_info" , (bf_property_info , Info 2 (Just 2) [TObj, TStr] TLst))
  , ("set_property_info",
                    (bf_set_property_info, Info 3 (Just 3) [TObj, TStr,
                                                            TLst]       TAny))
  , ("add_property"  , (bf_add_property  , Info 4 (Just 4) [TObj, TStr,
                                                            TAny, TLst] TAny))
  , ("delete_property",
                      (bf_delete_property, Info 2 (Just 2) [TObj, TStr] TAny))
  , ("is_clear_property",
                    (bf_is_clear_property, Info 2 (Just 2) [TObj, TStr] TInt))
  , ("clear_property", (bf_clear_property, Info 2 (Just 2) [TObj, TStr] TAny))

  , ("verbs"         , (bf_verbs         , Info 1 (Just 1) [TObj]       TLst))
  , ("verb_info"     , (bf_verb_info     , Info 2 (Just 2) [TObj, TAny] TLst))
  , ("set_verb_info" , (bf_set_verb_info , Info 3 (Just 3) [TObj, TAny,
                                                            TLst]       TAny))
  , ("verb_args"     , (bf_verb_args     , Info 2 (Just 2) [TObj, TAny] TLst))
  , ("set_verb_args" , (bf_set_verb_args , Info 3 (Just 3) [TObj, TAny,
                                                            TLst]       TAny))
  , ("add_verb"      , (bf_add_verb      , Info 3 (Just 3) [TObj, TLst,
                                                            TLst]       TInt))
  , ("delete_verb"   , (bf_delete_verb   , Info 2 (Just 2) [TObj, TAny] TAny))
  , ("verb_code"     , (bf_verb_code     , Info 2 (Just 4) [TObj, TAny,
                                                            TAny, TAny] TLst))
  , ("set_verb_code" , (bf_set_verb_code , Info 3 (Just 3) [TObj, TAny,
                                                            TLst]       TLst))
  , ("disassemble"   , (bf_disassemble   , Info 2 (Just 2) [TObj, TAny] TLst))

  , ("players"       , (bf_players       , Info 0 (Just 0) []           TLst))
  , ("is_player"     , (bf_is_player     , Info 1 (Just 1) [TObj]       TInt))
  , ("set_player_flag",
                      (bf_set_player_flag, Info 2 (Just 2) [TObj, TAny] TAny))
  ]

-- § 4.4.3.1 Fundamental Operations on Objects

bf_create (Obj parent : optional) = do
  maybeParent <- case parent of
    -1  -> return Nothing
    oid -> checkFertile oid >> return (Just oid)

  db <- getDatabase
  let newOid = maxObject db + 1

  ownerOid <- case maybeOwner of
    Nothing         -> frame permissions
    Just (Obj (-1)) -> checkWizard         >> return newOid
    Just (Obj oid)  -> checkPermission oid >> return oid

  maybeQuota <- readProperty ownerOid "ownership_quota"
  case maybeQuota of
    Just (Int quota)
      | quota <= 0 -> raise E_QUOTA
      | otherwise  -> writeProperty ownerOid "ownership_quota" (Int $ quota - 1)
    _ -> return ()

  properties <- case maybeParent of
    Nothing  -> return $ objectProperties initObject
    Just oid -> do
      -- add to parent's set of children
      liftSTM $ modifyObject oid db $ \obj -> return $ addChild obj newOid

      -- properties inherited from parent
      Just parent <- getObject oid
      HM.fromList `liftM` mapM mkProperty (HM.toList $ objectProperties parent)

        where mkProperty (name, propTVar) = liftSTM $ do
                prop <- readTVar propTVar
                let prop' = prop {
                        propertyValue     = Nothing
                      , propertyInherited = True
                      , propertyOwner     = if propertyPermC prop
                                            then ownerOid
                                            else propertyOwner prop
                      }
                propTVar' <- newTVar prop'
                return (name, propTVar')

  let newObj = initObject {
          objectParent     = maybeParent
        , objectOwner      = ownerOid
        , objectProperties = properties
        }

  putDatabase =<< liftSTM (addObject newObj db)

  callFromFunc "create" 0 (newOid, "initialize") []
  return (Obj newOid)

  where (maybeOwner : _) = maybeDefaults optional

bf_chparent [Obj object, Obj new_parent] = notyet "chparent"

bf_valid [Obj object] = (truthValue . isJust) `liftM` getObject object

bf_parent [Obj object] = (Obj . getParent) `liftM` checkValid object

bf_children [Obj object] = (objectList . getChildren) `liftM` checkValid object

bf_recycle [Obj object] = notyet "recycle"

bf_object_bytes [Obj object] = do
  checkWizard
  obj <- checkValid object

  propertyBytes <- liftM storageBytes $ liftSTM $
                   mapM readTVar $ HM.elems (objectProperties obj)
  verbBytes     <- liftM storageBytes $ liftSTM $
                   mapM (readTVar . snd) $ objectVerbs obj

  return $ Int $ fromIntegral $ storageBytes obj + propertyBytes + verbBytes

bf_max_object [] = (Obj . maxObject) `liftM` getDatabase

-- § 4.4.3.2 Object Movement

bf_move [Obj what, Obj where_] = do
  what' <- checkValid what
  where' <- case where_ of
    -1  -> return Nothing
    oid -> Just `liftM` checkValid oid
  checkPermission (objectOwner what')

  when (isJust where') $ do
    accepted <- maybe False truthOf `liftM`
                callFromFunc "move" 0 (where_, "accept") [Obj what]
    unless accepted $ do
      wizard <- isWizard =<< frame permissions
      unless wizard $ raise E_NACC

  let newWhere = case where_ of
        -1  -> Nothing
        oid -> Just oid

  maybeWhat <- getObject what
  case maybeWhat of
    Nothing      -> return ()
    Just whatObj -> unless (objectLocation whatObj == newWhere) $ do
      maybeWhere <- getObject where_
      when (isNothing newWhere || isJust maybeWhere) $ do
        checkRecurse what where_

        let oldWhere = objectLocation whatObj
        db <- getDatabase

        liftSTM $ modifyObject what db $ \obj ->
          return obj { objectLocation = newWhere }
        case oldWhere of
          Nothing        -> return ()
          Just oldWhere' -> liftSTM $ modifyObject oldWhere' db $ \obj ->
            return obj { objectContents = IS.delete what (objectContents obj) }
        case newWhere of
          Nothing        -> return ()
          Just newWhere' -> liftSTM $ modifyObject newWhere' db $ \obj ->
            return obj { objectContents = IS.insert what (objectContents obj) }

        case oldWhere of
          Nothing        -> return ()
          Just oldWhere' ->
            void $ callFromFunc "move" 1 (oldWhere', "exitfunc") [Obj what]

        maybeWhat <- getObject what
        case maybeWhat of
          Nothing      -> return ()
          Just whatObj ->
            when (objectLocation whatObj == newWhere) $
            void $ callFromFunc "move" 2 (where_, "enterfunc") [Obj what]

  return nothing

  where checkRecurse what loc = do
          when (loc == what) $ raise E_RECMOVE
          maybeLoc <- getObject loc
          case join $ objectLocation `fmap` maybeLoc of
            Just oid -> checkRecurse what oid
            Nothing  -> return ()

-- § 4.4.3.3 Operations on Properties

bf_properties [Obj object] = do
  obj <- checkValid object
  unless (objectPermR obj) $ checkPermission (objectOwner obj)

  stringList `liftM` liftSTM (definedProperties obj)

bf_property_info [Obj object, Str prop_name] = do
  obj <- checkValid object
  prop <- getProperty obj prop_name
  unless (propertyPermR prop) $ checkPermission (propertyOwner prop)

  return $ fromList [Obj $ propertyOwner prop, Str $ perms prop]

  where perms prop = T.pack $ concat [['r' | propertyPermR prop],
                                      ['w' | propertyPermW prop],
                                      ['c' | propertyPermC prop]]

traverseDescendants :: (Object -> MOO a) -> ObjId -> MOO ()
traverseDescendants f oid = do
  Just obj <- getObject oid
  f obj
  mapM_ (traverseDescendants f) $ getChildren obj

modifyDescendants :: Database -> (Object -> STM Object) -> ObjId -> MOO ()
modifyDescendants db f oid = do
  liftSTM $ modifyObject oid db f
  Just obj <- getObject oid
  mapM_ (modifyDescendants db f) $ getChildren obj

{-# ANN module ("HLint: ignore Use String" :: String) #-}

checkPerms :: [Char] -> StrT -> MOO (Set Char)
checkPerms valid perms = do
  let permSet = S.fromList (T.unpack $ T.toCaseFold perms)
  unless (S.null $ permSet `S.difference` S.fromList valid) $ raise E_INVARG
  return permSet

bf_set_property_info [Obj object, Str prop_name, Lst info] = do
  (owner, perms, new_name) <- case V.toList info of
    [Obj owner, Str perms]               -> return (owner, perms, Nothing)
    [_        , _        ]               -> raise E_TYPE
    [Obj owner, Str perms, Str new_name] -> return (owner, perms, Just new_name)
    [_        , _        , _           ] -> raise E_TYPE
    _                                    -> raise E_INVARG
  permSet <- checkPerms "rwc" perms
  checkValid owner

  obj <- checkValid object
  prop <- getProperty obj prop_name
  unless (propertyPermW prop) $ checkPermission (propertyOwner prop)
  checkPermission owner

  let setInfo = modifyProperty obj prop_name $ \prop ->
        return prop {
            propertyOwner = owner
          , propertyPermR = 'r' `S.member` permSet
          , propertyPermW = 'w' `S.member` permSet
          , propertyPermC = 'c' `S.member` permSet
        }

  case new_name of
    Nothing      -> setInfo
    Just newName -> do
      let newName' = T.toCaseFold newName
          oldName' = T.toCaseFold prop_name

      unless (objectPermW obj) $ checkPermission (objectOwner obj)

      when (propertyInherited prop) $ raise E_INVARG
      unless (newName' == oldName') $ flip traverseDescendants object $ \obj ->
        when (isJust $ lookupPropertyRef obj newName') $ raise E_INVARG

      setInfo

      db <- getDatabase
      flip (modifyDescendants db) object $ \obj -> do
        let Just propTVar = lookupPropertyRef obj oldName'
        prop <- readTVar propTVar
        writeTVar propTVar $ prop { propertyName = newName }

        return obj { objectProperties =
                        HM.insert newName' propTVar $
                        HM.delete oldName' (objectProperties obj) }

  return nothing

bf_add_property [Obj object, Str prop_name, value, Lst info] = do
  (owner, perms) <- case V.toList info of
    [Obj owner, Str perms] -> return (owner, perms)
    [_        , _        ] -> raise E_TYPE
    _                      -> raise E_INVARG
  permSet <- checkPerms "rwc" perms
  checkValid owner

  obj <- checkValid object
  unless (objectPermW obj) $ checkPermission (objectOwner obj)
  checkPermission owner

  when (isBuiltinProperty name) $ raise E_INVARG
  flip traverseDescendants object $ \obj ->
    when (isJust $ lookupPropertyRef obj name) $ raise E_INVARG

  let definedProp = initProperty {
          propertyName      = prop_name
        , propertyValue     = Just value
        , propertyInherited = False
        , propertyOwner     = owner
        , propertyPermR     = 'r' `S.member` permSet
        , propertyPermW     = 'w' `S.member` permSet
        , propertyPermC     = 'c' `S.member` permSet
      }
      inheritedProp = definedProp {
          propertyInherited = True
        , propertyValue     = Nothing
       }
      addProperty prop obj = do
        propTVar <- newTVar prop
        return obj { objectProperties =
                        HM.insert name propTVar $ objectProperties obj }
      addInheritedProperty prop obj =
        flip addProperty obj $ if propertyPermC prop
                               then prop { propertyOwner = objectOwner obj }
                               else prop
  db <- getDatabase
  liftSTM $ modifyObject object db (addProperty definedProp)
  mapM_ (modifyDescendants db $
         addInheritedProperty inheritedProp) $ getChildren obj

  return nothing

  where name = T.toCaseFold prop_name

bf_delete_property [Obj object, Str prop_name] = do
  obj <- checkValid object
  unless (objectPermW obj) $ checkPermission (objectOwner obj)
  prop <- getProperty obj prop_name
  when (propertyInherited prop) $ raise E_PROPNF

  db <- getDatabase
  flip (modifyDescendants db) object $ \obj ->
    return obj { objectProperties = HM.delete name (objectProperties obj) }

  return nothing

  where name = T.toCaseFold prop_name

bf_is_clear_property [Obj object, Str prop_name] = do
  obj <- checkValid object
  if isBuiltinProperty prop_name
    then return $ truthValue False
    else do
      prop <- getProperty obj prop_name
      unless (propertyPermR prop) $ checkPermission (propertyOwner prop)

      return (truthValue $ isNothing $ propertyValue prop)

bf_clear_property [Obj object, Str prop_name] = do
  obj <- checkValid object
  if isBuiltinProperty prop_name
    then raise E_PERM
    else do
      modifyProperty obj prop_name $ \prop -> do
        unless (propertyPermW prop) $ checkPermission (propertyOwner prop)
        unless (propertyInherited prop) $ raise E_INVARG
        return prop { propertyValue = Nothing }

      return nothing

-- § 4.4.3.4 Operations on Verbs

bf_verbs [Obj object] = do
  obj <- checkValid object
  unless (objectPermR obj) $ checkPermission (objectOwner obj)

  stringList `liftM` liftSTM (definedVerbs obj)

bf_verb_info [Obj object, verb_desc] = do
  obj <- checkValid object
  verb <- getVerb obj verb_desc
  unless (verbPermR verb) $ checkPermission (verbOwner verb)

  return $ fromList
    [Obj $ verbOwner verb, Str $ perms verb, Str $ verbNames verb]

  where perms verb = T.pack $ concat [['r' | verbPermR verb],
                                      ['w' | verbPermW verb],
                                      ['x' | verbPermX verb],
                                      ['d' | verbPermD verb]]

verbInfo :: LstT -> MOO (ObjId, Set Char, StrT)
verbInfo info = do
  (owner, perms, names) <- case V.toList info of
    [Obj owner, Str perms, Str names] -> return (owner, perms, names)
    [_        , _        , _        ] -> raise E_TYPE
    _                                 -> raise E_INVARG
  permSet <- checkPerms "rwxd" perms
  checkValid owner
  when (null $ T.words names) $ raise E_INVARG

  return (owner, permSet, names)

bf_set_verb_info [Obj object, verb_desc, Lst info] = do
  (owner, permSet, names) <- verbInfo info

  obj <- checkValid object
  verb <- getVerb obj verb_desc
  unless (verbPermW verb) $ checkPermission (verbOwner verb)
  checkPermission owner

  let newNames = T.toCaseFold names
      oldNames = T.toCaseFold (verbNames verb)
  unless (newNames == oldNames || objectPermW obj) $
    checkPermission (objectOwner obj)

  modifyVerb (object, obj) verb_desc $ \verb ->
    return verb {
        verbNames = names
      , verbOwner = owner
      , verbPermR = 'r' `S.member` permSet
      , verbPermW = 'w' `S.member` permSet
      , verbPermX = 'x' `S.member` permSet
      , verbPermD = 'd' `S.member` permSet
    }

  return nothing

bf_verb_args [Obj object, verb_desc] = do
  obj <- checkValid object
  verb <- getVerb obj verb_desc
  unless (verbPermR verb) $ checkPermission (verbOwner verb)

  return $ stringList [dobj verb, prep verb, iobj verb]

  where dobj = obj2text  . verbDirectObject
        iobj = obj2text  . verbIndirectObject
        prep = prep2text . verbPreposition

verbArgs :: LstT -> MOO (ObjSpec, PrepSpec, ObjSpec)
verbArgs args = do
  (dobj, prep, iobj) <- case V.toList args of
    [Str dobj, Str prep, Str iobj] -> return (dobj, breakSlash prep, iobj)
      where breakSlash = fst . T.breakOn "/"
    [_       , _       , _       ] -> raise E_TYPE
    _                              -> raise E_INVARG
  dobj' <- maybe (raise E_INVARG) return $ text2obj  (T.toCaseFold dobj)
  prep' <- maybe (raise E_INVARG) return $ text2prep (T.toCaseFold prep)
  iobj' <- maybe (raise E_INVARG) return $ text2obj  (T.toCaseFold iobj)

  return (dobj', prep', iobj')

bf_set_verb_args [Obj object, verb_desc, Lst args] = do
  (dobj, prep, iobj) <- verbArgs args

  obj <- checkValid object
  verb <- getVerb obj verb_desc
  unless (verbPermW verb) $ checkPermission (verbOwner verb)

  modifyVerb (object, obj) verb_desc $ \verb ->
    return verb {
        verbDirectObject   = dobj
      , verbPreposition    = prep
      , verbIndirectObject = iobj
    }

  return nothing

bf_add_verb [Obj object, Lst info, Lst args] = do
  (owner, permSet, names) <- verbInfo info
  (dobj, prep, iobj)      <- verbArgs args

  obj <- checkValid object
  unless (objectPermW obj) $ checkPermission (objectOwner obj)
  checkPermission owner

  let definedVerb = initVerb {
          verbNames          = names
        , verbOwner          = owner
        , verbPermR          = 'r' `S.member` permSet
        , verbPermW          = 'w' `S.member` permSet
        , verbPermX          = 'x' `S.member` permSet
        , verbPermD          = 'd' `S.member` permSet
        , verbDirectObject   = dobj
        , verbPreposition    = prep
        , verbIndirectObject = iobj
      }

  db <- getDatabase
  liftSTM $ modifyObject object db $ addVerb definedVerb

  return $ Int $ fromIntegral $ length (objectVerbs obj) + 1

bf_delete_verb [Obj object, verb_desc] = do
  obj <- checkValid object
  getVerb obj verb_desc
  unless (objectPermW obj) $ checkPermission (objectOwner obj)

  case lookupVerbRef obj verb_desc of
    Nothing         -> raise E_VERBNF
    Just (index, _) -> do
      db <- getDatabase
      liftSTM $ modifyObject object db $ deleteVerb index

  return nothing

bf_verb_code (Obj object : verb_desc : optional) = do
  obj <- checkValid object
  verb <- getVerb obj verb_desc
  unless (verbPermR verb) $ checkPermission (verbOwner verb)
  checkProgrammer

  let code = init $ T.splitOn "\n" $
             unparse fully_paren indent (verbProgram verb)
  return (stringList code)

  where [fully_paren, indent] = booleanDefaults optional [False, True]

bf_set_verb_code [Obj object, verb_desc, Lst code] = do
  obj <- checkValid object
  verb <- getVerb obj verb_desc
  text <- (T.concat . ($ [])) `liftM` V.foldM addLine id code
  unless (verbPermW verb) $ checkPermission (verbOwner verb)
  checkProgrammer

  case parse text of
    Left errors   -> return $ fromListBy (Str . T.pack) errors
    Right program -> do
      modifyVerb (object, obj) verb_desc $ \verb ->
        return verb {
            verbProgram = program
          , verbCode    = compile program
        }
      return $ Lst V.empty

  where addLine :: ([StrT] -> [StrT]) -> Value -> MOO ([StrT] -> [StrT])
        addLine add (Str line) = return (add [line, "\n"] ++)
        addLine _    _         = raise E_INVARG

bf_disassemble [Obj object, verb_desc] = do
  obj <- checkValid object
  verb <- getVerb obj verb_desc
  unless (verbPermR verb) $ checkPermission (verbOwner verb)

  let Program statements = verbProgram verb
  return $ fromListBy (Str . T.pack . show) statements

-- § 4.4.3.5 Operations on Player Objects

bf_players [] = (objectList . allPlayers) `liftM` getDatabase

bf_is_player [Obj object] =
  (truthValue . objectIsPlayer) `liftM` checkValid object

bf_set_player_flag [Obj object, value] = do
  checkValid object
  checkWizard

  db <- getDatabase
  liftSTM $ modifyObject object db $
    \obj -> return obj { objectIsPlayer = isPlayer }
  putDatabase $ setPlayer isPlayer object db

  unless isPlayer $ bootPlayer object

  return nothing

  where isPlayer = truthOf value