module MOO.Database (
Database
, Connected
, ServerOptions(..)
, Persistence(..)
, serverOptions
, initDatabase
, dbObjectRef
, dbObject
, maxObject
, resetMaxObject
, renumber
, setObjects
, addObject
, deleteObject
, modifyObject
, allPlayers
, setPlayer
, getServerOption
, getServerOption'
, loadServerOptions
, getServerMessage
, loadPersistence
, syncPersistence
, saveDatabase
) where
import Control.Applicative ((<$>), (<*>))
import Control.Monad (forM, forM_, when, (>=>))
import Data.IntSet (IntSet)
import Data.Maybe (isJust)
import Data.Monoid ((<>))
import Data.Text (Text)
import Data.Time (getCurrentTime)
import Data.Typeable (Typeable)
import Data.Vector (Vector)
import Data.Version (showVersion)
import Database.VCache (VCache, VSpace, VTx, VCacheable(put, get), vcache_space,
PVar, loadRootPVarIO, newPVarsIO, newPVarIO, newPVar,
readPVarIO, readPVar, writePVar, runVTx, markDurable)
import qualified Data.HashMap.Lazy as HM
import qualified Data.HashSet as HS
import qualified Data.IntSet as IS
import qualified Data.Vector as V
import MOO.Builtins (builtinFunctions)
import MOO.Object
import MOO.Task
import MOO.Types
import MOO.Util
import MOO.Version
import qualified MOO.List as Lst
import qualified MOO.String as Str
data Database = Database {
objects :: Vector (PVar (Maybe Object))
, players :: IntSet
, serverOptions :: ServerOptions
} deriving Typeable
instance VCacheable Database where
put db = do
put $ VVector (objects db)
put $ VIntSet (players db)
get = do
objects <- unVVector <$> get
players <- unVIntSet <$> get
return initDatabase { objects = objects, players = players }
initDatabase = Database {
objects = V.empty
, players = IS.empty
, serverOptions = undefined
}
dbObjectRef :: ObjId -> Database -> Maybe (PVar (Maybe Object))
dbObjectRef oid = (V.!? oid) . objects
dbObject :: ObjId -> Database -> VTx (Maybe Object)
dbObject oid = maybe (return Nothing) readPVar . dbObjectRef oid
maxObject :: Database -> ObjId
maxObject = pred . V.length . objects
resetMaxObject :: Database -> VTx Database
resetMaxObject db = do
newMaxObject <- findLastValid (maxObject db)
return db { objects = V.take (succ newMaxObject) $ objects db }
where findLastValid :: ObjId -> VTx ObjId
findLastValid oid
| oid >= 0 = dbObject oid db >>=
maybe (findLastValid $ pred oid) (return . const oid)
| otherwise = return nothing
renumber :: ObjId -> Database -> VTx (ObjId, Database)
renumber old db = do
maybeNew <- findLeastUnused 0
case maybeNew of
Nothing -> return (old, db)
Just new -> do
let Just oldRef = dbObjectRef old db
Just newRef = dbObjectRef new db
Just obj <- readPVar oldRef
writePVar oldRef Nothing
writePVar newRef (Just obj)
renumberObject obj old new db
forM_ [0..maxObject db] $ \oid -> case dbObjectRef oid db of
Just ref -> do
maybeObj <- readPVar ref
case maybeObj of
Just obj -> do
maybeNew <- renumberOwnership old new obj
when (isJust maybeNew) $ writePVar ref maybeNew
Nothing -> return ()
Nothing -> return ()
let db' = setPlayer (objectIsPlayer obj) new $ setPlayer False old db
return (new, db')
where findLeastUnused :: ObjId -> VTx (Maybe ObjId)
findLeastUnused new
| new < old = dbObject new db >>= maybe (return $ Just new)
(const $ findLeastUnused $ succ new)
| otherwise = return Nothing
setObjects :: VSpace -> [Maybe Object] -> Database -> IO Database
setObjects vspace objs db = do
refs <- newPVarsIO vspace objs
return db { objects = V.fromList refs }
addObject :: Object -> Database -> VTx Database
addObject obj db = do
ref <- newPVar (Just obj)
return db { objects = V.snoc (objects db) ref }
deleteObject :: ObjId -> Database -> VTx ()
deleteObject oid db =
case dbObjectRef oid db of
Just ref -> writePVar ref Nothing
Nothing -> return ()
modifyObject :: ObjId -> Database -> (Object -> VTx Object) -> VTx ()
modifyObject oid db f =
case dbObjectRef oid db of
Just ref -> readPVar ref >>= maybe (return ()) (f >=> writePVar ref . Just)
Nothing -> return ()
allPlayers :: Database -> [ObjId]
allPlayers = IS.toList . players
setPlayer :: Bool -> ObjId -> Database -> Database
setPlayer isPlayer oid db = db { players = change oid (players db) }
where change | isPlayer = IS.insert
| otherwise = IS.delete
data ServerOptions = Options {
bgSeconds :: Int
, bgTicks :: Int
, connectTimeout :: IntT
, defaultFlushCommand :: Text
, fgSeconds :: Int
, fgTicks :: Int
, maxStackDepth :: Int
, queuedTaskLimit :: Maybe Int
, nameLookupTimeout :: IntT
, outboundConnectTimeout :: IntT
, protectProperty :: Id -> Bool
, protectFunction :: Id -> Bool
, supportNumericVerbnameStrings :: Bool
}
getServerOption :: Id -> MOO (Maybe Value)
getServerOption = getServerOption' systemObject
getServerOption' :: ObjId -> Id -> MOO (Maybe Value)
getServerOption' oid option = getServerOptions oid >>= ($ option)
getServerOptions :: ObjId -> MOO (Id -> MOO (Maybe Value))
getServerOptions oid = do
serverOptions <- readProperty oid "server_options"
return $ case serverOptions of
Just (Obj oid) -> readProperty oid . fromId
_ -> const (return Nothing)
getProtected :: (Id -> MOO (Maybe Value)) -> [Id] -> MOO (Id -> Bool)
getProtected getOption ids = do
maybes <- forM ids $ fmap (fmap truthOf) . getOption . ("protect_" <>)
let protectedSet = HS.fromList [ id | (id, Just True) <- zip ids maybes ]
return (`HS.member` protectedSet)
loadServerOptions :: MOO ()
loadServerOptions = do
option <- getServerOptions systemObject
bgSeconds <- option "bg_seconds"
bgTicks <- option "bg_ticks"
fgSeconds <- option "fg_seconds"
fgTicks <- option "fg_ticks"
maxStackDepth <- option "max_stack_depth"
queuedTaskLimit <- option "queued_task_limit"
connectTimeout <- option "connect_timeout"
outboundConnectTimeout <- option "outbound_connect_timeout"
nameLookupTimeout <- option "name_lookup_timeout"
defaultFlushCommand <- option "default_flush_command"
supportNumericVerbnameStrings <- option "support_numeric_verbname_strings"
protectProperty <- getProtected option builtinProperties
protectFunction <- getProtected option (HM.keys builtinFunctions)
let options = Options {
bgSeconds = case bgSeconds of
Just (Int secs) | secs >= 1 -> fromIntegral secs
_ -> defaultBgSeconds
, bgTicks = case bgTicks of
Just (Int ticks) | ticks >= 100 -> fromIntegral ticks
_ -> defaultBgTicks
, fgSeconds = case fgSeconds of
Just (Int secs) | secs >= 1 -> fromIntegral secs
_ -> defaultFgSeconds
, fgTicks = case fgTicks of
Just (Int ticks) | ticks >= 100 -> fromIntegral ticks
_ -> defaultFgTicks
, maxStackDepth = case maxStackDepth of
Just (Int depth) | depth > 50 -> fromIntegral depth
_ -> defaultMaxStackDepth
, queuedTaskLimit = case queuedTaskLimit of
Just (Int limit) | limit >= 0 -> Just (fromIntegral limit)
_ -> Nothing
, connectTimeout = case connectTimeout of
Just (Int secs) | secs > 0 -> secs
_ -> 300
, outboundConnectTimeout = case outboundConnectTimeout of
Just (Int secs) | secs > 0 -> secs
_ -> 5
, nameLookupTimeout = case nameLookupTimeout of
Just (Int secs) | secs >= 0 -> secs
_ -> 5
, defaultFlushCommand = case defaultFlushCommand of
Just (Str cmd) -> Str.toText cmd
Just _ -> ""
Nothing -> ".flush"
, supportNumericVerbnameStrings =
maybe False truthOf supportNumericVerbnameStrings
, protectProperty = protectProperty
, protectFunction = protectFunction
}
db <- getDatabase
putDatabase db { serverOptions = options }
getServerMessage :: ObjId -> Id -> MOO [Text] -> MOO [Text]
getServerMessage oid msg def = do
maybeValue <- getServerOption' oid msg
case maybeValue of
Just (Str s) -> return [Str.toText s]
Just (Lst v) -> maybe (return []) return $ strings (Lst.toList v)
Just _ -> return []
Nothing -> def
where strings :: [Value] -> Maybe [Text]
strings (v:vs) = case v of
Str s -> (Str.toText s :) <$> strings vs
_ -> Nothing
strings [] = Just []
type Connected = [(ObjId, ObjId)]
data Persistence = Persistence {
persistenceVSpace :: VSpace
, persistenceVersion :: PVar VVersion
, persistenceDatabase :: PVar Database
, persistenceConnected :: PVar Connected
, persistenceCheckpoint :: PVar VUTCTime
} deriving Typeable
instance VCacheable Persistence where
put p = do
put $ persistenceVSpace p
put $ persistenceVersion p
put $ persistenceDatabase p
put $ persistenceConnected p
put $ persistenceCheckpoint p
get = Persistence <$> get <*> get <*> get <*> get <*> get
rootPVar :: VCache -> IO (PVar (Maybe Persistence))
rootPVar vcache = loadRootPVarIO vcache "EtaMOO database" Nothing
saveDatabase :: VCache -> (Database, Connected) -> IO ()
saveDatabase vcache (db, connected) = do
let vspace = vcache_space vcache
versionPVar <- newPVarIO vspace (VVersion version)
databasePVar <- newPVarIO vspace db
connectedPVar <- newPVarIO vspace connected
checkpointPVar <- newPVarIO vspace . VUTCTime =<< getCurrentTime
let p = Persistence {
persistenceVSpace = vspace
, persistenceVersion = versionPVar
, persistenceDatabase = databasePVar
, persistenceConnected = connectedPVar
, persistenceCheckpoint = checkpointPVar
}
root <- rootPVar vcache
runVTx vspace $ do
writePVar root (Just p)
markDurable
loadPersistence :: VCache -> IO Persistence
loadPersistence vcache = rootPVar vcache >>= readPVarIO >>=
maybe (error "invalid database") checkVersion
where checkVersion :: Persistence -> IO Persistence
checkVersion p = do
dbVersion <- unVVersion <$> readPVarIO (persistenceVersion p)
when (dbVersion /= version) $ error $
"database version " ++ showVersion dbVersion ++
" does not match current version " ++ showVersion version
return p
syncPersistence :: Persistence -> IO ()
syncPersistence p = do
now <- getCurrentTime
runVTx (persistenceVSpace p) $ do
writePVar (persistenceVersion p) (VVersion version)
writePVar (persistenceCheckpoint p) (VUTCTime now)
markDurable