packages feed

roguestar-engine-0.4.0.0: src/Protocol.hs

{-# LANGUAGE ExistentialQuantification, ScopedTypeVariables, PatternGuards, OverloadedStrings #-}

module Protocol
    (mainLoop)
    where

import Data.Char
import Data.List as List
import CreatureData
import Creature
import Character
import DB
import System.Exit
import System.IO
import BeginGame
import Data.Maybe
import Plane
import PlaneData
import Building
import BuildingData
import Tool
import FactionData
import PlaneVisibility
import Facing
import ToolData
import Control.Monad.Error
import Turns
import SpeciesData
import Species
import Data.Ord
import Combat
import Substances
import PlayerState
import Make
import Control.Concurrent
import Control.Monad.STM
import Control.Concurrent.STM.TVar
import Control.Exception
import WorkCluster
import qualified Data.ByteString.Char8 as B
import qualified Perception
-- Don't call dbBehave, use dbPerformPlayerTurn
import Behavior hiding (dbBehave)
-- We need to construct References based on UIDs, so we cheat a little:
import DBPrivate (Reference(ToolRef))

mainLoop :: DB_BaseType -> IO ()
mainLoop db_init = 
    do db_var <- newMVar db_init
       input_chan <- newChan
       output_chan <- newChan
       query_count <- newTVarIO (Just 0) -- Just (the number of running queries) or Nothing (a non-query action is in progress)
       wait_quit <- newEmptyMVar
       work_cluster <- newWorkCluster
       replaceWorkOperation work_cluster . evaluateGame =<< readMVar db_var
       let foreverLoopThenQuit = flip finally (putMVar wait_quit ()) . forever
       _ <- forkIO $ foreverLoopThenQuit $ writeChan input_chan =<< B.getLine
       _ <- forkIO $ foreverLoopThenQuit $
           do next_line <- liftM (B.map toLower . B.unlines . B.lines) (readChan output_chan)
              when (B.length next_line > 0) $
                  do B.putStrLn next_line
                     B.putStrLn "over"
              hFlush stdout
       _ <- forkIO $ foreverLoopThenQuit $
           do next_command <- readChan input_chan
              case (B.words $ B.map toLower next_command) of
                  ["quit"] -> exitWith ExitSuccess
                  ["reset"] -> stopping query_count $ modifyMVar_ db_var (const $ return initial_db)
                  ("game":"query":args) -> 
                      do querrying query_count $
                             do result <- workRequest work_cluster (Query, args)
                                complete Nothing output_chan result
                  ("game":"action":args) ->
                      do result <- workRequest work_cluster (Action, args)
                         stopping query_count $ complete (Just db_var) output_chan result
                         querrying query_count $ complete Nothing output_chan result -- print the result as a query, this will ensure errors get printed
                         replaceWorkOperation work_cluster . evaluateGame =<< readMVar db_var
                  ("noop":_) -> return ()
                  failed -> 
                      do _ <- forkIO $ complete Nothing output_chan $ Left $ DBError $ "protocol-error: unrecognized request: `" ++ B.unpack (B.unwords failed) ++ "`"
                         return ()
       takeMVar wait_quit -- "park" the main function

-- | Evaluate a 'GameDirective' and return it from a remote thread via an 'MVar'.
evaluateGame :: DB_BaseType -> WorkRequest -> IO WorkResult
evaluateGame db0 (Query, ["snapshot"]) = (runDB $ ro $ liftM (\b -> "answer: snapshot " `B.append` if b then "yes" else "no") dbHasSnapshot) db0
evaluateGame db0 (Query, args) = (runDB $ ro $ dbPeepOldestSnapshot $ dbDispatchQuery args) db0
evaluateGame db0 (Action, args) = runDB (liftM (const "") $ dbDispatchAction args) db0

-- | Wait for currently running queries to finish, and stop processing incomming queries while we mutate the database.
stopping :: TVar (Maybe Integer) -> IO () -> IO ()
stopping query_count actionM = bracket
    (atomically $ do maybe retry (\x -> when (x /= 0) retry) =<< readTVar query_count
                     writeTVar query_count $ Nothing)
    (const $ atomically $ writeTVar query_count (Just 0))
    (const actionM)

-- | Process a querry concurrently with other queries.
querrying :: TVar (Maybe Integer) -> IO () -> IO ()
querrying query_count actionM =
    do atomically $ writeTVar query_count =<< liftM Just . (maybe retry $ return . (+1)) =<< readTVar query_count
       _ <- forkIO $ finally (atomically $ do writeTVar query_count =<< liftM (fmap (subtract 1)) (readTVar query_count)) actionM
       return ()

-- | Complete a querry or action.  If a database variable is provided, it will be modified according to the result of the action.
-- The result of the action will be printed to the output_chan.
complete :: Maybe (MVar DB_BaseType) -> Chan B.ByteString -> Either DBError (B.ByteString,DB_BaseType) -> IO ()
complete m_db_var output_chan result =
    do case m_db_var of
           Just db_var -> 
               do modifyMVar_ db_var $ \db0 -> return $ case result of
                      Right (_,db1) -> db1
                      Left (DBErrorFlag errflag) -> db0 { db_error_flag = show errflag }
                      Left (DBError _) -> db0
                  writeChan output_chan "done"
           Nothing ->
               do case result of
                      Right (outstr,_) ->
                          do _ <- evaluate outstr
                             writeChan output_chan outstr
                      Left (DBErrorFlag _) -> return () -- client will query this explicitly (if it cares)
                      Left (DBError errstr) ->
                          do writeChan output_chan $ "error: " `B.append` B.pack errstr
                             B.hPutStrLn stderr $ "DBError: " `B.append` B.pack errstr

dbOldestSnapshotOnly :: (DBReadable db) => db ()
dbOldestSnapshotOnly = 
    do b <- dbHasSnapshot
       when b $ fail "protocol-error: pending snapshot"

-- |
-- Perform an action assuming the database is in the DBRaceSelectionState,
-- otherwise returns an error message.
--
dbRequiresRaceSelectionState :: (DBReadable db) => db a -> db a
dbRequiresRaceSelectionState action = 
    do dbOldestSnapshotOnly
       state <- playerState
       case state of
           RaceSelectionState -> action
           _ -> throwError $ DBError $ "protocol-error: not in race selection state (" ++ show state ++ ")"

-- |
-- Perform an action assuming the database is in the DBClassSelectionState,
-- otherwise returns an error message.
--
dbRequiresClassSelectionState :: (DBReadable db) => (Creature -> db a) -> db a
dbRequiresClassSelectionState action = 
    do dbOldestSnapshotOnly
       state <- playerState
       case state of
           ClassSelectionState creature -> action creature
           _ -> throwError $ DBError $ "protocol-error: not in class selection state (" ++ show state ++ ")"

-- |
-- Perform an action that operates on the player creature (not in any context).
-- The states that work for this are:
--
-- * ClassSelectionState
-- * PlayerCreatureTurn
--
dbRequiresPlayerCenteredState :: (DBReadable db) => (Creature -> db a) -> db a
dbRequiresPlayerCenteredState action =
    do dbOldestSnapshotOnly
       state <- playerState
       case state of
		  ClassSelectionState creature -> action creature
		  PlayerCreatureTurn creature_ref _ -> action =<< dbGetCreature creature_ref
		  _ -> throwError $ DBError $ "protocol-error: not in player-centered state (" ++ show state ++ ")"

-- |
-- Perform an action that works during any creature's turn in a planar environment.
-- The states that work for this are:
--
-- * PlayerCreatureTurn
-- * SnapshotEvent
--
dbRequiresPlanarTurnState :: (DBReadable db) => (CreatureRef -> db a) -> db a
dbRequiresPlanarTurnState action =
    do dbOldestSnapshotOnly
       state <- playerState
       maybe (throwError $ DBError $ "protocol-error: not in planar turn state (" ++ show state ++ ")") action $ creatureOf state

-- |
-- Perform an action that works only during a player-character's turn.
-- The states that work for this are:
--
-- PlayerCreatureTurn
--
dbRequiresPlayerTurnState :: (DBReadable db) => (CreatureRef -> db a) -> db a
dbRequiresPlayerTurnState action =
    do dbOldestSnapshotOnly
       state <- playerState
       case state of
                  PlayerCreatureTurn creature_ref _ -> action creature_ref
                  _ -> throwError $ DBError $ "protocol-error: not in player turn state (" ++ show state ++ ")"

-- |
-- For arbitrary-length menu selections, get the current index into the menu, if any.
--
menuState :: (DBReadable db) => db (Maybe Integer)
menuState = liftM menuIndex playerState

-- |
-- For arbitrary-length menu selections, modify the current index into the menu.  If there is no menu index
-- in the current state, this has no effect.
--
modifyMenuState :: (Integer -> Integer) -> DB ()
modifyMenuState f_ = 
    do number_of_tools <- liftM genericLength toolMenuElements
       let f = (\x -> if number_of_tools == 0 then 0 else x `mod` number_of_tools) . f_
       setPlayerState . modifyMenuIndex f =<< playerState

dbDispatchQuery :: (DBReadable db) => [B.ByteString] -> db B.ByteString
dbDispatchQuery ["state"] = 
    do state <- playerState
       return $ case state of
			   RaceSelectionState -> "answer: state race-selection"
			   ClassSelectionState {} -> "answer: state class-selection"
			   PlayerCreatureTurn _ NormalMode -> "answer: state player-turn"
                           PlayerCreatureTurn _ MoveMode -> "answer: state move"
                           PlayerCreatureTurn _ (PickupMode {}) -> "answer: state pickup"
                           PlayerCreatureTurn _ (DropMode {}) -> "answer: state drop"
                           PlayerCreatureTurn _ (WieldMode {}) -> "answer: state wield"
                           PlayerCreatureTurn _ AttackMode -> "answer: state attack"
                           PlayerCreatureTurn _ FireMode -> "answer: state fire"
                           PlayerCreatureTurn _ JumpMode -> "answer: state jump"
                           PlayerCreatureTurn _ TurnMode -> "answer: state turn"
                           PlayerCreatureTurn _ (MakeMode _ make_prep) | isFinished make_prep -> "answer: state make-finished"
                           PlayerCreatureTurn _ (MakeMode _ make_prep) | needsKind make_prep -> "answer: state make-what"
                           PlayerCreatureTurn _ (MakeMode {}) -> "answer: state make"
                           PlayerCreatureTurn _ ClearTerrainMode -> "answer: state clear-terrain"
                           SnapshotEvent (AttackEvent {}) -> "answer: state attack-event"
                           SnapshotEvent (MissEvent {}) -> "answer: state miss-event"
                           SnapshotEvent (KilledEvent {}) -> "answer: state killed-event"
                           SnapshotEvent (WeaponOverheatsEvent {}) -> "answer: state weapon-overheats-event"
                           SnapshotEvent (WeaponExplodesEvent {}) -> "answer: state weapon-explodes-event"
                           SnapshotEvent (DisarmEvent {}) -> "answer: state disarm-event"
                           SnapshotEvent (SunderEvent {}) -> "answer: state sunder-event"
                           SnapshotEvent (TeleportEvent {}) -> "answer: state teleport-event"
                           SnapshotEvent (HealEvent {}) -> "answer: state heal-event"
                           SnapshotEvent (ExpendToolEvent {}) -> "answer: state expend-tool-event"
                           GameOver -> "answer: state game-over"

dbDispatchQuery ["action-count"] =
    do n <- dbActionCount
       return $ "answer: action-count " `B.append` (B.pack $ show n)

dbDispatchQuery ["menu-state"] =
    do m_state <- menuState
       return $ case m_state of
           Nothing -> "answer: menu-state 0"
           Just state -> "answer: menu-state " `B.append` (B.pack $ show state)

dbDispatchQuery ["who-attacks"] =
    do state <- playerState
       return $ case state of
           SnapshotEvent (AttackEvent { attack_event_source_creature = attacker_ref }) -> "answer: who-attacks " `B.append` (B.pack $ show $ toUID attacker_ref)
	   SnapshotEvent (MissEvent { miss_event_creature = attacker_ref }) -> "answer: who-attacks " `B.append` (B.pack $ show $ toUID attacker_ref)
           SnapshotEvent (WeaponOverheatsEvent { weapon_overheats_event_creature = attacker_ref }) -> "answer: who-attacks " `B.append` (B.pack $ show $ toUID attacker_ref)
           SnapshotEvent (WeaponExplodesEvent { weapon_explodes_event_creature = attacker_ref }) -> "answer: who-attacks " `B.append` (B.pack $ show $ toUID attacker_ref)
           SnapshotEvent (DisarmEvent { disarm_event_source_creature = attacker_ref }) -> "answer: who-attacks " `B.append` (B.pack $ show $ toUID attacker_ref)
           SnapshotEvent (SunderEvent { sunder_event_source_creature = attacker_ref }) -> "answer: who-attacks " `B.append` (B.pack $ show $ toUID attacker_ref)
	   _ -> "answer: who-attacks 0"

dbDispatchQuery ["who-hit"] =
    do state <- playerState
       return $ case state of
           SnapshotEvent (AttackEvent { attack_event_target_creature = target_ref }) -> "answer: who-hit " `B.append` (B.pack $ show $ toUID target_ref)
           SnapshotEvent (DisarmEvent { disarm_event_target_creature = target_ref }) -> "answer: who-hit " `B.append` (B.pack $ show $ toUID target_ref)
           SnapshotEvent (SunderEvent { sunder_event_target_creature = target_ref }) -> "answer: who-hit " `B.append` (B.pack $ show $ toUID target_ref)
	   _ -> "answer: who-hit 0"

dbDispatchQuery ["tool-used"] =
    do state <- playerState
       return $ case state of
           SnapshotEvent (ExpendToolEvent { expend_tool_event_tool = tool_ref }) -> "answer: tool-used " `B.append` (B.pack $ show $ toUID tool_ref)
           _ -> "answer: tool-used 0"

dbDispatchQuery ["weapon-used"] =
    do state <- playerState
       return $ case state of
           SnapshotEvent (AttackEvent { attack_event_source_weapon = Just weapon_ref }) -> "answer: weapon-used " `B.append` (B.pack $ show $ toUID weapon_ref)
	   SnapshotEvent (MissEvent { miss_event_weapon = Just weapon_ref }) -> "answer: weapon-used " `B.append` (B.pack $ show $ toUID weapon_ref)
           SnapshotEvent (WeaponOverheatsEvent { weapon_overheats_event_weapon = weapon_ref }) -> "answer: weapon-used " `B.append` (B.pack $ show $ toUID weapon_ref)
           SnapshotEvent (WeaponExplodesEvent { weapon_explodes_event_weapon = weapon_ref }) -> "answer: weapon-used " `B.append` (B.pack $ show $ toUID weapon_ref)
           SnapshotEvent (SunderEvent { sunder_event_source_weapon = weapon_ref }) -> "answer: weapon-used " `B.append` (B.pack $ show $ toUID weapon_ref)
	   _ -> "answer: weapon-used 0"

dbDispatchQuery ["tool-hit"] =
    do state <- playerState
       return $ case state of
           SnapshotEvent (DisarmEvent { disarm_event_target_tool = tool_ref }) -> "answer: tool-hit " `B.append` (B.pack $ show $ toUID tool_ref)
           SnapshotEvent (SunderEvent { sunder_event_target_tool = tool_ref }) -> "answer: tool-hit " `B.append` (B.pack $ show $ toUID tool_ref)
           _ -> "answer: tool-hit 0"

dbDispatchQuery ["who-killed"] =
    do state <- playerState
       return $ case state of
           SnapshotEvent (KilledEvent killed_ref) -> "answer: who-killed " `B.append` (B.pack $ show $ toUID killed_ref)
	   _ -> "answer: who-killed 0"

dbDispatchQuery ["who-event"] =
    do state <- playerState
       return $ case state of
           SnapshotEvent event -> "answer: who-event " `B.append` fromMaybe "0" (fmap (B.pack . show . toUID) $ subjectOf event)
           _ -> "answer: who-event 0"

dbDispatchQuery ["player-races","0"] =
    return ("begin-table player-races 0 name\n" `B.append`
	    B.unlines (map B.pack player_race_names) `B.append`
	    "end-table")

dbDispatchQuery ["visible-terrain","0"] =
    do maybe_plane_ref <- dbGetCurrentPlane
       terrain_map <- maybe (return []) (dbGetVisibleTerrainForFaction Player) maybe_plane_ref 
       return ("begin-table visible-terrain 0 x y terrain-type\n" `B.append`
	       (B.unlines $ map (\(terrain_type,Position (x,y)) -> B.unwords $ map B.pack [show x, show y, show terrain_type]) terrain_map) `B.append`
	       "end-table")

dbDispatchQuery ["who-player"] = return "answer: who-player 2"

dbDispatchQuery ["visible-objects","0"] = 
    do maybe_plane_ref <- dbGetCurrentPlane
       (objects :: [Location S (Reference ()) ()]) <- maybe (return []) (dbGetVisibleObjectsForFaction (return . const True) Player) maybe_plane_ref
       table_rows <- mapM (dbObjectToTableRow . entity) objects
       return ("begin-table visible-objects 0 object-unique-id x y facing\n" `B.append`
               (B.unlines $ table_rows) `B.append`
               "end-table")
        where dbObjectToTableRow obj_ref = 
                do l <- dbWhere obj_ref
                   return $ case (extractLocation l,extractLocation l) of
                                 (Just (Position (x,y)),maybe_face) -> B.unwords $ map B.pack $ [show $ toUID obj_ref,show x,show y,show $ fromMaybe Here maybe_face]
                                 _ -> ""

dbDispatchQuery ["object-details",uid] = ro $
  do maybe_plane_ref <- dbGetCurrentPlane
     (visibles :: [Reference ()]) <- maybe
         (return [])
         (flip dbGetVisibleObjectsForFaction Player $ \ref ->
              do let f = (== uid) . B.pack . show . toUID
                 let m_wielder = coerceReference ref
                 m_wield <- maybe (return Nothing) dbGetWielded m_wielder
                 return $ maybe False f m_wield || f ref)
         maybe_plane_ref
     let creature_refs = mapMaybe (coerceReferenceTyped _creature) visibles
     wielded <- liftM catMaybes $ mapM dbGetWielded creature_refs
     let tool_refs = mapMaybe (coerceReferenceTyped _tool) visibles ++ wielded
     let building_refs = mapMaybe (coerceReferenceTyped _building) visibles
     creatures <- liftM (zip creature_refs) $ mapRO dbGetCreature creature_refs
     tools <- liftM (zip tool_refs) $ mapRO dbGetTool tool_refs
     buildings <- liftM (zip building_refs) $ mapRO dbGetBuilding building_refs
     liftM B.unlines $ liftM3 (\a b c -> concat [a,b,c])
                            (mapM creatureToTableData creatures)
                            (mapM toolToTableData tools)
                            (mapM buildingToTableData buildings)
   where objectTableWrapper :: (DBReadable db) =>
                               Reference a ->
                               db B.ByteString ->
                               db B.ByteString
         objectTableWrapper obj_ref tableDataF =
          do table_data <- tableDataF
             return $
                 "begin-table object-details " `B.append`
                 (B.pack $ show $ toUID obj_ref) `B.append`
                 " property value\n" `B.append`
                 table_data `B.append`
                 "end-table"
         creatureToTableData :: (DBReadable db) =>
                                (CreatureRef,Creature) ->
                                db B.ByteString
         creatureToTableData (ref,creature) = objectTableWrapper ref $
            do fac <- getCreatureFaction ref
               hp <- getCreatureAbsoluteHealth ref
               maxhp <- getCreatureMaxHealth ref
               return $
                   "object-type creature\n" `B.append`
                   "species " `B.append` (B.pack $ show $ creature_species creature) `B.append` "\n" `B.append`
                   "random-id " `B.append` (B.pack $ show $ creature_random_id creature) `B.append` "\n" `B.append`
                   "faction " `B.append` B.pack (show fac) `B.append` "\n" `B.append`
                       (if fac == Player then
                           "hp " `B.append` B.pack (show hp) `B.append` "\n" `B.append`
                           "maxhp " `B.append` B.pack (show maxhp) `B.append` "\n"
                        else "")
         toolToTableData :: (DBReadable db) => (ToolRef,Tool) -> db B.ByteString
         toolToTableData (ref,tool) = objectTableWrapper ref $ return $
               "object-type tool\n" `B.append`
               "tool-type " `B.append` toolType tool `B.append` "\n" `B.append`
               "tool " `B.append` toolName tool `B.append` "\n"
         buildingToTableData :: (DBReadable db) => (BuildingRef,Building) -> db B.ByteString
         buildingToTableData (ref,Building) = objectTableWrapper ref $
             do building_type <- buildingType ref
                return $ "object-type building\n" `B.append`
                         "building-type " `B.append` B.pack (show building_type) `B.append` "\n"

dbDispatchQuery ["player-stats","0"] = dbRequiresPlayerCenteredState dbQueryPlayerStats

dbDispatchQuery ["center-coordinates","0"] = dbRequiresPlanarTurnState dbQueryCenterCoordinates

dbDispatchQuery ["base-classes","0"] = dbRequiresClassSelectionState dbQueryBaseClasses

dbDispatchQuery ["pickups","0"] = dbRequiresPlayerTurnState $ \creature_ref -> 
    liftM (showToolMenuTable "pickups" "0") $ toolsToMenuTable =<< dbAvailablePickups creature_ref

dbDispatchQuery ["inventory","0"] = dbRequiresPlayerTurnState $ \creature_ref ->
    liftM (showToolMenuTable "inventory" "0") $ toolsToMenuTable =<< dbGetContents creature_ref

dbDispatchQuery ["menu","0"] =
    liftM (showToolMenuTable "menu" "0") $ toolsToMenuTable =<< toolMenuElements

dbDispatchQuery ["menu",s] | Just window_size <- readNumber s =
    do n <- liftM (fromMaybe 0) menuState
       let half_window = window_size `div` 2
       let windowF (x,_,_) = abs (x - (max half_window n)) <= half_window
       liftM (showToolMenuTable "menu" s . filter windowF) $ toolsToMenuTable =<< toolMenuElements

dbDispatchQuery ["wielded-objects","0"] =
    do m_plane_ref <- dbGetCurrentPlane
       creature_refs <- maybe (return []) (dbGetVisibleObjectsForFaction (return . const True) Player) m_plane_ref
       wielded_tool_refs <- mapM dbGetWielded creature_refs
       let wieldedPairToTable :: CreatureRef -> Maybe ToolRef -> Maybe B.ByteString
           wieldedPairToTable creature_ref = fmap (\tool_ref -> (B.pack $ show $ toUID tool_ref) `B.append` " " `B.append` (B.pack $ show $ toUID creature_ref))
       return $ "begin-table wielded-objects 0 uid creature\n" `B.append`
                B.unlines (catMaybes $ zipWith wieldedPairToTable creature_refs wielded_tool_refs) `B.append`
		"end-table"

dbDispatchQuery ["biome"] =
    do m_plane_ref <- dbGetCurrentPlane
       biome_name <- case m_plane_ref of
           Nothing -> return "nothing"
	   Just plane_ref -> liftM (show . plane_biome) $ dbGetPlane plane_ref
       return $ "answer: biome " `B.append` B.pack biome_name

dbDispatchQuery ["current-plane"] =
    do m_plane_ref <- dbGetCurrentPlane
       return $ case m_plane_ref of
           Nothing -> "answer: current-plane 0"
           Just plane_ref -> "answer: current-plane " `B.append` (B.pack $ show $ toUID plane_ref)

dbDispatchQuery ["plane-random-id"] =
    do m_plane_ref <- dbGetCurrentPlane
       case m_plane_ref of
           Nothing -> return "answer: plane-random-id 0"
           Just plane_ref -> liftM (("answer: plane-random-id " `B.append`) . B.pack . show . plane_random_id) $ dbGetPlane plane_ref

dbDispatchQuery ["planet-name"] =
    do m_plane_ref <- dbGetCurrentPlane
       case m_plane_ref of
         Nothing -> return "answer: planet-name nothing"
         Just plane_ref -> liftM ("answer: planet-name " `B.append`) $ planetName plane_ref

dbDispatchQuery ["compass"] =
    do m_player_ref <- getCurrentCreature Player
       case m_player_ref of
         Nothing -> return "answer: compass nothing"
         Just player_ref -> Perception.runPerception player_ref $ liftM (("answer: compass " `B.append`) . B.pack . show) Perception.compass

dbDispatchQuery unrecognized = return $ "protocol-error: unrecognized query `" `B.append` B.unwords unrecognized `B.append` "`"

dbDispatchAction :: [B.ByteString] -> DB ()
dbDispatchAction ["continue"] = dbPopOldestSnapshot

dbDispatchAction ["select-race",race_name] = 
    dbRequiresRaceSelectionState $ dbSelectPlayerRace race_name

dbDispatchAction ["reroll"] =
    dbRequiresClassSelectionState $ dbRerollRace

dbDispatchAction ["select-class",class_name] =
    dbRequiresClassSelectionState $ dbSelectPlayerClass class_name

dbDispatchAction [direction] | isJust $ stringToFacing direction =
    do state <- playerState
       case state of
           PlayerCreatureTurn _ player_mode -> case player_mode of
               JumpMode ->   dbDispatchAction ["jump",direction]
               TurnMode ->   dbDispatchAction ["turn",direction]
               AttackMode -> dbDispatchAction ["attack",direction]
               FireMode ->   dbDispatchAction ["fire",direction]
               MoveMode ->   dbDispatchAction ["move",direction]
               ClearTerrainMode ->  dbDispatchAction ["clear-terrain",direction]
               _ ->          dbDispatchAction ["normal",direction]
           _ -> throwError $ DBError $ "protocol-error: not in player turn state"

dbDispatchAction ["normal"] =
    dbRequiresPlayerTurnState $ \creature_ref -> (setPlayerState $ PlayerCreatureTurn creature_ref NormalMode)

dbDispatchAction ["normal",direction] | Just face <- stringToFacing direction =
    dbRequiresPlayerTurnState $ \creature_ref -> 
        do behavior <- facingBehavior creature_ref face
           dbPerformPlayerTurn behavior creature_ref

dbDispatchAction ["move"] =
    dbRequiresPlayerTurnState $ \creature_ref -> (setPlayerState $ PlayerCreatureTurn creature_ref MoveMode)

dbDispatchAction ["move",direction] | isJust $ stringToFacing direction =
    dbRequiresPlayerTurnState (\creature_ref -> dbPerformPlayerTurn (Step $ fromJust $ stringToFacing direction) creature_ref)

dbDispatchAction ["jump"] =
    dbRequiresPlayerTurnState $ \creature_ref -> (setPlayerState $ PlayerCreatureTurn creature_ref JumpMode)

dbDispatchAction ["jump",direction] | isJust $ stringToFacing direction =
    dbRequiresPlayerTurnState (\creature_ref -> dbPerformPlayerTurn (Behavior.Jump $ fromJust $ stringToFacing direction) creature_ref)

dbDispatchAction ["turn"] =
    dbRequiresPlayerTurnState $ \creature_ref -> (setPlayerState $ PlayerCreatureTurn creature_ref TurnMode)

dbDispatchAction ["turn",direction] | isJust $ stringToFacing direction =
    dbRequiresPlayerTurnState $ \creature_ref -> dbPerformPlayerTurn (TurnInPlace $ fromJust $ stringToFacing direction) creature_ref

dbDispatchAction ["clear-terrain"] =
    dbRequiresPlayerTurnState $ \creature_ref -> (setPlayerState $ PlayerCreatureTurn creature_ref ClearTerrainMode)

dbDispatchAction ["clear-terrain",direction] | isJust $ stringToFacing direction =
    dbRequiresPlayerTurnState $ \creature_ref -> dbPerformPlayerTurn (ClearTerrain $ fromJust $ stringToFacing direction) creature_ref

dbDispatchAction ["next"] = modifyMenuState (+1)

dbDispatchAction ["prev"] = modifyMenuState (subtract 1)

dbDispatchAction ["select-menu"] =
    do state <- playerState
       i <- menuState
       tool_table <- toolsToMenuTable =<< toolMenuElements
       let selection = maybe "0" (\(_,tool_ref,_) -> B.pack $ show $ toUID tool_ref) $ find (\(n,_,_) -> Just n == i) tool_table
       case state of
           PlayerCreatureTurn _ player_mode -> case player_mode of
               PickupMode {} -> dbDispatchAction ["pickup",selection]
               DropMode {}   -> dbDispatchAction ["drop",selection]
               WieldMode {}  -> dbDispatchAction ["wield",selection]
               MakeMode {}   -> dbDispatchAction ["make-with",selection]
               _ -> throwError $ DBError $ "protocol-error: not in menu selection state"
           _ -> throwError $ DBError $ "protocol-error: not in player turn state"

dbDispatchAction ["make-begin"] = dbRequiresPlayerTurnState $ \creature_ref ->
    setPlayerState (PlayerCreatureTurn creature_ref (MakeMode 0 prepare_make))

dbDispatchAction ["make-what",what] | (Just device_kind) <- readDeviceKind what =
    do state <- playerState
       case state of
           PlayerCreatureTurn c (MakeMode n make_prep) -> (setPlayerState $ PlayerCreatureTurn c $ MakeMode n (make_prep `makeWith` device_kind))
           _ -> throwError $ DBError $ "protocol-error: not in make or make-what state"

dbDispatchAction ["make-with",tool_uid] =
    do tool_ref <- readUID ToolRef tool_uid
       tool <- dbGetTool tool_ref
       state <- playerState
       case state of
           PlayerCreatureTurn c (MakeMode _ make_prep) -> case (hasChromalite tool, hasMaterial tool, hasGas tool) of
               (Just ch,_,_) | needsChromalite make_prep -> setPlayerState (PlayerCreatureTurn c $ MakeMode 0 $ make_prep `makeWith` (ch,tool_ref))
               (_,Just m,_) | needsMaterial make_prep -> setPlayerState (PlayerCreatureTurn c $ MakeMode 0 $ make_prep `makeWith` (m,tool_ref))
               (_,_,Just g) | needsGas make_prep -> setPlayerState (PlayerCreatureTurn c $ MakeMode 0 $ make_prep `makeWith` (g,tool_ref))
               _ | otherwise -> throwError $ DBError "error: tool doesn't have needed substance"
           _ -> throwError $ DBError "protocol-error: not in make or make-what state"

dbDispatchAction ["make-end"] =
    do state <- playerState
       case state of
           PlayerCreatureTurn c (MakeMode _ make_prep) | isFinished make_prep -> dbPerformPlayerTurn (Make make_prep) c
           PlayerCreatureTurn _ (MakeMode {}) -> throwError $ DBError "protocol-error: make isn't complete"
           _ -> throwError $ DBError "protocol-error: not in make or make-what state"

dbDispatchAction ["pickup"] = dbRequiresPlayerTurnState $ \creature_ref ->
    do pickups <- dbAvailablePickups creature_ref
       case pickups of
           [tool_ref] -> dbPerformPlayerTurn (Pickup tool_ref) creature_ref >> return ()
	   [] -> throwError $ DBErrorFlag NothingAtFeet
	   _ -> setPlayerState (PlayerCreatureTurn creature_ref (PickupMode 0))

dbDispatchAction ["pickup",tool_uid] = dbRequiresPlayerTurnState $ \creature_ref ->
    do tool_ref <- readUID ToolRef tool_uid
       dbPerformPlayerTurn (Pickup tool_ref) creature_ref

dbDispatchAction ["drop"] = dbRequiresPlayerTurnState $ \creature_ref ->
    do inventory <- dbGetContents creature_ref
       case inventory of
           [tool_ref] -> dbPerformPlayerTurn (Drop tool_ref) creature_ref >> return ()
	   [] -> throwError $ DBErrorFlag NothingInInventory
	   _ -> setPlayerState (PlayerCreatureTurn creature_ref (DropMode 0))

dbDispatchAction ["drop",tool_uid] = dbRequiresPlayerTurnState $ \creature_ref ->
    do tool_ref <- readUID ToolRef tool_uid
       dbPerformPlayerTurn (Drop tool_ref) creature_ref

dbDispatchAction ["wield"] = dbRequiresPlayerTurnState $ \creature_ref ->
    do available <- availableWields creature_ref
       case available of
           [tool_ref] -> dbPerformPlayerTurn (Wield tool_ref) creature_ref >> return ()
	   [] -> throwError $ DBErrorFlag NothingInInventory
	   _ -> setPlayerState (PlayerCreatureTurn creature_ref (WieldMode 0))

dbDispatchAction ["wield",tool_uid] = dbRequiresPlayerTurnState $ \creature_ref ->
    do tool_ref <- readUID ToolRef tool_uid
       dbPerformPlayerTurn (Wield tool_ref) creature_ref

dbDispatchAction ["unwield"] = dbRequiresPlayerTurnState $ \creature_ref -> dbPerformPlayerTurn Unwield creature_ref

dbDispatchAction ["fire"] =
    dbRequiresPlayerTurnState $ \creature_ref -> rangedAttackModel creature_ref >> setPlayerState (PlayerCreatureTurn creature_ref FireMode)

dbDispatchAction ["fire",direction] = dbRequiresPlayerTurnState $ \creature_ref -> dbPerformPlayerTurn (Fire $ fromJust $ stringToFacing direction) creature_ref

dbDispatchAction ["attack"] =
    dbRequiresPlayerTurnState $ \creature_ref -> meleeAttackModel creature_ref >> setPlayerState (PlayerCreatureTurn creature_ref AttackMode)

dbDispatchAction ["attack",direction] = dbRequiresPlayerTurnState $ \creature_ref -> dbPerformPlayerTurn (Attack $ fromJust $ stringToFacing direction) creature_ref

dbDispatchAction ["activate"] = dbRequiresPlayerTurnState $ \creature_ref -> dbPerformPlayerTurn Activate creature_ref

dbDispatchAction unrecognized = throwError $ DBError $ ("protocol-error: unrecognized action `" ++ (B.unpack $ B.unwords unrecognized) ++ "`")

dbSelectPlayerRace :: B.ByteString -> DB ()
dbSelectPlayerRace race_name = 
    case find (\s -> B.map toLower (B.pack $ show s) == race_name) player_species of
        Nothing -> throwError $ DBError $ "protocol-error: unrecognized race '" ++ B.unpack race_name ++ "'"
        Just species -> generateInitialPlayerCreature species

dbSelectPlayerClass :: B.ByteString -> Creature -> DB ()
dbSelectPlayerClass class_name creature = 
    let eligable_base_classes = getEligableBaseCharacterClasses creature
	in case find (\x -> (B.map toLower . B.pack . show) x == class_name) eligable_base_classes of
	       Nothing -> throwError $ DBError $ "protocol-error: unrecognized or invalid class '" ++ B.unpack class_name ++ "'"
	       Just the_class -> dbBeginGame creature the_class

dbRerollRace :: Creature -> DB ()
dbRerollRace _ = do starting_race <- dbGetStartingRace
		    generateInitialPlayerCreature $ fromJust starting_race

dbQueryPlayerStats :: (DBReadable db) => Creature -> db B.ByteString
dbQueryPlayerStats creature = return $ playerStatsTable creature

-- |
-- Generate a list of tools, e.g. for an inventory list or pickup list.
-- The data source is selected on a context-sensitive basis.
--
toolMenuElements :: (DBReadable db) => db [ToolRef]
toolMenuElements =
    do state <- playerState
       case state of
           PlayerCreatureTurn c (PickupMode {}) -> dbAvailablePickups c
           PlayerCreatureTurn c (WieldMode {}) -> availableWields c
           PlayerCreatureTurn c (MakeMode _ make_prep) | needsChromalite make_prep -> filterM (liftM (isJust . hasChromalite) . dbGetTool) =<< availableWields c
           PlayerCreatureTurn c (MakeMode _ make_prep) | needsMaterial make_prep -> filterM (liftM (isJust . hasMaterial) . dbGetTool) =<< availableWields c
           PlayerCreatureTurn c (MakeMode _ make_prep) | needsGas make_prep -> filterM (liftM (isJust . hasGas) . dbGetTool) =<< availableWields c
           PlayerCreatureTurn c _ -> dbGetContents c
           _ -> return []

-- |
-- Convert a list of tool menu elements into table row entries.
-- The result entries consist of an index incrementing from zero, ToolRef, and name of the tool.
--
toolsToMenuTable :: (DBReadable db) => [ToolRef] -> db [(Integer,ToolRef,B.ByteString)]
toolsToMenuTable raw_uids =
    do let uids = sortBy (comparing toUID) raw_uids
       tool_names <- mapM (liftM toolName . dbGetTool) uids
       return $ zip3 [0..] uids tool_names

-- |
-- Generate a tool menu table in text form, with the specified name and element list.
--
showToolMenuTable :: B.ByteString -> B.ByteString -> [(Integer,ToolRef,B.ByteString)] -> B.ByteString
showToolMenuTable table_name table_id tool_table = 
    "begin-table " `B.append` table_name `B.append` " " `B.append` table_id `B.append` " n uid name" `B.append` "\n" `B.append`
    B.unlines (map (\(n,uid,tool_name) -> B.unwords [B.pack $ show n,B.pack $ show $ toUID uid,tool_name]) tool_table) `B.append`
    "end-table"

-- |
-- Information about player creatures (for which the player should have almost all available information.)
--
playerStatsTable :: Creature -> B.ByteString
playerStatsTable c =
    "begin-table player-stats 0 property value\n" `B.append`
               "str " `B.append` (B.pack $ show $ rawScore Strength c) `B.append` "\n" `B.append`
	       "spd " `B.append` (B.pack $ show $ rawScore Speed c) `B.append` "\n" `B.append`
	       "con " `B.append` (B.pack $ show $ rawScore Constitution c) `B.append` "\n" `B.append`
	       "int " `B.append` (B.pack $ show $ rawScore Intellect c) `B.append` "\n" `B.append`
	       "per " `B.append` (B.pack $ show $ rawScore Perception c) `B.append` "\n" `B.append`
	       "cha " `B.append` (B.pack $ show $ rawScore Charisma c) `B.append` "\n" `B.append`
	       "mind " `B.append` (B.pack $ show $ rawScore Mindfulness c) `B.append` "\n" `B.append`
	       "maxhp " `B.append` (B.pack $ show $ creatureAbilityScore ToughnessTrait c) `B.append` "\n" `B.append`
	       "species " `B.append` (B.pack $ show $ creature_species c) `B.append` "\n" `B.append`
	       "random-id " `B.append` (B.pack $ show $ creature_random_id c) `B.append` "\n" `B.append`
	       "gender " `B.append` (B.pack $ show $ creatureGender c) `B.append` "\n" `B.append`
	       "end-table"

toolName :: Tool -> B.ByteString
toolName (DeviceTool _ d) = deviceName d
toolName (Sphere s) = prettySubstance s

toolType :: Tool -> B.ByteString
toolType (DeviceTool Gun _) = "gun"
toolType (DeviceTool Sword _) = "sword"
toolType (Sphere (GasSubstance _)) = "sphere-gas"
toolType (Sphere (MaterialSubstance _)) = "sphere-material"
toolType (Sphere (ChromaliteSubstance _)) = "sphere-chromalite"

dbQueryBaseClasses :: (DBReadable db) => Creature -> db B.ByteString
dbQueryBaseClasses creature = return $ baseClassesTable creature

baseClassesTable :: Creature -> B.ByteString
baseClassesTable creature = 
    "begin-table base-classes 0 class\n" `B.append`
    (B.unlines $ map (B.pack . show) $ getEligableBaseCharacterClasses creature) `B.append`
    "end-table"

dbQueryCenterCoordinates :: (DBReadable db) => CreatureRef -> db B.ByteString
dbQueryCenterCoordinates creature_ref =
    do l <- dbWhere creature_ref
       case (extractLocation l,extractLocation l :: Maybe Facing) of
		(Just (Position (x,y)),Nothing) -> 
                    return (begin_table `B.append`
			    "x " `B.append` B.pack (show x) `B.append` "\n" `B.append`
			    "y " `B.append` B.pack (show y) `B.append` "\n" `B.append`
			    "end-table")
                (Just (Position (x,y)),Just face) -> 
                    return (begin_table `B.append`
                           "x " `B.append` B.pack (show x) `B.append` "\n" `B.append`
                           "y " `B.append` B.pack (show y) `B.append` "\n" `B.append`
                           "facing " `B.append` B.pack (show face) `B.append` "\n" `B.append`
                           "end-table")
		_ -> return (begin_table `B.append` "end-table")
	   where begin_table = "begin-table center-coordinates 0 axis coordinate\n"

readUID :: (Integer -> Reference a) -> B.ByteString -> DB (Reference a)
readUID f x = 
    do let m_uid = readNumber x
       ok <- maybe (return False) (dbVerify . f) m_uid
       when (not ok) $ throwError $ DBError $ "protocol-error: " ++ B.unpack x ++ " is not a valid uid."
       return $ f $ fromJust m_uid

readNumber :: B.ByteString -> Maybe Integer
readNumber = fmap fst . B.readInteger

readDeviceKind :: B.ByteString -> Maybe DeviceKind
readDeviceKind "pistol" = Just Pistol
readDeviceKind "carbine" = Just Carbine
readDeviceKind "rifle" = Just Rifle
readDeviceKind "fleuret" = Just Fleuret
readDeviceKind "sabre" = Just Sabre
readDeviceKind _ = Nothing