Use new PlayerKey type in lib and Message module from the separate APILanguage

This commit is contained in:
Tissevert 2019-08-17 23:08:52 +02:00
parent d54d97b84d
commit 9d15a80ff5
9 changed files with 71 additions and 81 deletions

View file

@ -24,7 +24,7 @@ executable hanafudapi
other-modules: App other-modules: App
, Automaton , Automaton
, Config , Config
, Message , Messaging
, Game , Game
, JSON , JSON
, Data , Data
@ -37,6 +37,7 @@ executable hanafudapi
, containers , containers
, unordered-containers , unordered-containers
, hanafuda >= 0.3.0 , hanafuda >= 0.3.0
, hanafuda-APILanguage
, http-types , http-types
, aeson , aeson
, mtl , mtl

View file

@ -16,13 +16,13 @@ import Data.Map ((!))
import Control.Concurrent (MVar, modifyMVar, putMVar, readMVar, takeMVar) import Control.Concurrent (MVar, modifyMVar, putMVar, readMVar, takeMVar)
import Control.Monad.Reader (ReaderT(..), ask, asks, lift) import Control.Monad.Reader (ReaderT(..), ask, asks, lift)
import Network.WebSockets (Connection) import Network.WebSockets (Connection)
import qualified Player (Key) import Hanafuda.KoiKoi (PlayerKey)
import qualified Session (T(..)) import qualified Session (T(..))
import qualified Server (T(..)) import qualified Server (T(..))
data Context = Context { data Context = Context {
mServer :: MVar Server.T mServer :: MVar Server.T
, key :: Player.Key , key :: PlayerKey
} }
type T a = ReaderT Context IO a type T a = ReaderT Context IO a
@ -30,7 +30,7 @@ type T a = ReaderT Context IO a
server :: T Server.T server :: T Server.T
server = asks mServer >>= lift . readMVar server = asks mServer >>= lift . readMVar
get :: Player.Key -> T Session.T get :: PlayerKey -> T Session.T
get key = get key =
(! key) . Server.sessions <$> server (! key) . Server.sessions <$> server

View file

@ -11,20 +11,21 @@ import Data.Map (Map, (!?))
import qualified Game (Key, T, new, play) import qualified Game (Key, T, new, play)
import qualified Hanafuda.KoiKoi as KoiKoi (Game(..), Step(..)) import qualified Hanafuda.KoiKoi as KoiKoi (Game(..), Step(..))
import qualified Session (Status(..), T(..), Update) import qualified Session (Status(..), T(..), Update)
import qualified Server (endGame, get, logIn, logOut, update, register) import qualified Server (endGame, get, logIn, logOut, update, register, room)
import qualified App (Context(..), T, current, debug, get, server, try, update, update_) import qualified App (Context(..), T, current, debug, get, server, try, update, update_)
import qualified Message (FromClient(..), T(..), broadcast, get, notifyPlayers, relay, send, sendTo, update) import qualified Hanafuda.Message as Message (FromClient(..), T(..))
import qualified Messaging (broadcast, get, notifyPlayers, relay, send, sendTo, update)
receive :: Session.Status -> Message.FromClient -> App.T () receive :: Session.Status -> Message.FromClient -> App.T ()
receive (Session.LoggedIn False) logIn@(Message.LogIn login) = receive (Session.LoggedIn False) logIn@(Message.LogIn login) =
asks App.key >>= App.try . (Server.logIn login) asks App.key >>= App.try . (Server.logIn login)
>>= maybe >>= maybe
(Message.relay logIn Message.broadcast >> setSessionStatus (Session.LoggedIn True)) (Messaging.relay logIn Messaging.broadcast >> setSessionStatus (Session.LoggedIn True))
sendError sendError
receive (Session.LoggedIn True) logOut@Message.LogOut = do receive (Session.LoggedIn True) logOut@Message.LogOut = do
Message.relay logOut Message.broadcast Messaging.relay logOut Messaging.broadcast
asks App.key >>= App.update_ . Server.logOut asks App.key >>= App.update_ . Server.logOut
setSessionStatus (Session.LoggedIn False) setSessionStatus (Session.LoggedIn False)
@ -34,8 +35,8 @@ receive (Session.LoggedIn True) invitation@(Message.Invitation {Message.to}) = d
Session.LoggedIn True -> do Session.LoggedIn True -> do
key <- asks App.key key <- asks App.key
App.update_ (Server.update to (Data.set $ Session.Answering key :: Session.Update)) App.update_ (Server.update to (Data.set $ Session.Answering key :: Session.Update))
Message.broadcast $ Message.update {Message.paired = [key, to]} Messaging.broadcast $ Messaging.update {Message.paired = [key, to]}
(Message.relay invitation $ Message.sendTo [to]) (Messaging.relay invitation $ Messaging.sendTo [to])
setSessionStatus (Session.Waiting to) setSessionStatus (Session.Waiting to)
_ -> sendError "They just left" _ -> sendError "They just left"
@ -44,16 +45,16 @@ receive (Session.Answering to) message@(Message.Answer {Message.accept}) = do
key <- asks App.key key <- asks App.key
case Session.status session of case Session.status session of
Session.Waiting for | for == key -> do Session.Waiting for | for == key -> do
Message.relay message $ Message.sendTo [to] Messaging.relay message $ Messaging.sendTo [to]
newStatus <- newStatus <-
if accept if accept
then do then do
gameKey <- Server.register <$> (lift $ Game.new for to) >>= App.update gameKey <- Server.register <$> (lift $ Game.new for to) >>= App.update
game <- Server.get gameKey <$> App.server game <- Server.get gameKey <$> App.server
Message.notifyPlayers game [] Messaging.notifyPlayers game []
return $ Session.Playing gameKey return $ Session.Playing gameKey
else do else do
Message.broadcast $ Message.update {Message.alone = [key, to]} Messaging.broadcast $ Messaging.update {Message.alone = [key, to]}
return $ Session.LoggedIn True return $ Session.LoggedIn True
App.update_ $ Server.update to (Data.set newStatus :: Session.Update) App.update_ $ Server.update to (Data.set newStatus :: Session.Update)
setSessionStatus newStatus setSessionStatus newStatus
@ -66,7 +67,7 @@ receive (Session.Playing gameKey) played@(Message.Play {}) = do
case result of case result of
Left message -> sendError message Left message -> sendError message
Right newGame -> do Right newGame -> do
Message.notifyPlayers newGame logs Messaging.notifyPlayers newGame logs
case KoiKoi.step newGame of case KoiKoi.step newGame of
KoiKoi.Over -> do KoiKoi.Over -> do
App.debug $ "Game " ++ show gameKey ++ " ended" App.debug $ "Game " ++ show gameKey ++ " ended"
@ -78,14 +79,14 @@ receive (Session.Playing gameKey) Message.Quit = do
case games !? gameKey of case games !? gameKey of
Nothing -> do Nothing -> do
key <- asks App.key key <- asks App.key
Message.broadcast $ Message.update {Message.alone = [key]} Messaging.broadcast $ Messaging.update {Message.alone = [key]}
setSessionStatus (Session.LoggedIn True) setSessionStatus (Session.LoggedIn True)
_ -> sendError "Game is still running" _ -> sendError "Game is still running"
receive state _ = sendError $ "Invalid message in state " ++ show state receive state _ = sendError $ "Invalid message in state " ++ show state
sendError :: String -> App.T () sendError :: String -> App.T ()
sendError = Message.send . Message.Error sendError = Messaging.send . Message.Error
setSessionStatus :: Session.Status -> App.T () setSessionStatus :: Session.Status -> App.T ()
setSessionStatus newStatus = do setSessionStatus newStatus = do
@ -95,7 +96,7 @@ setSessionStatus newStatus = do
loop :: App.T () loop :: App.T ()
loop = do loop = do
message <- Message.get message <- Messaging.get
status <- Session.status <$> App.current status <- Session.status <$> App.current
status `receive` message status `receive` message
loop loop
@ -103,5 +104,5 @@ loop = do
start :: App.T () start :: App.T ()
start = do start = do
App.debug "Initial state" App.debug "Initial state"
Message.Welcome <$> App.server <*> asks App.key >>= Message.send Message.Welcome . Server.room <$> App.server <*> asks App.key >>= Messaging.send
loop loop

View file

@ -18,29 +18,26 @@ import Control.Monad.Except (throwError)
import Data.Text (pack) import Data.Text (pack)
import Data.Map (mapWithKey) import Data.Map (mapWithKey)
import Data.HashMap.Strict (insert) import Data.HashMap.Strict (insert)
import Data.Aeson (FromJSON(..), ToJSON(..), ToJSON1(..), ToJSONKey(..), Value(..), genericParseJSON, genericToEncoding, genericLiftToEncoding, toEncoding1, toJSON1) import Data.Aeson (ToJSON(..), ToJSONKey(..), Value(..), defaultOptions, genericToEncoding)
import Data.Aeson.Types (toJSONKeyText) import Data.Aeson.Types (toJSONKeyText)
import qualified JSON (defaultOptions, singleLCField)
import qualified Data (Key) import qualified Data (Key)
import qualified Player (Key) import qualified Hanafuda (Flower(..), Pack, cardsOfPack, empty)
import qualified Hanafuda (Flower(..), Card(..), Pack, cardsOfPack, empty) import qualified Hanafuda.KoiKoi (Game(..), Environment, Mode(..), Move(..), PlayerKey, Score, Step(..), Yaku(..), new, play)
import qualified Hanafuda.Player (Player(..), Players(..)) import qualified Hanafuda.Player (Player(..), Players(..))
import qualified Hanafuda.KoiKoi (Action(..), Game(..), Environment, Mode(..), Move(..), Score, Source(..), Step(..), Yaku(..), new, play) import Hanafuda.Message()
import GHC.Generics import GHC.Generics
deriving instance Generic Hanafuda.Flower deriving instance Generic Hanafuda.Flower
deriving instance Generic Hanafuda.KoiKoi.Mode deriving instance Generic Hanafuda.KoiKoi.Mode
deriving instance Generic Hanafuda.KoiKoi.Yaku deriving instance Generic Hanafuda.KoiKoi.Yaku
deriving instance Generic Hanafuda.KoiKoi.Step deriving instance Generic Hanafuda.KoiKoi.Step
deriving instance Generic1 (Hanafuda.Player.Player Player.Key)
deriving instance Generic1 (Hanafuda.Player.Players Player.Key)
type T = Hanafuda.KoiKoi.Game Player.Key type T = Hanafuda.KoiKoi.Game
deriving instance Generic T deriving instance Generic T
instance ToJSON T where instance ToJSON T where
toEncoding = genericToEncoding JSON.defaultOptions toEncoding = genericToEncoding defaultOptions
instance ToJSON Hanafuda.Flower instance ToJSON Hanafuda.Flower
@ -51,34 +48,28 @@ instance ToJSON Hanafuda.Pack where
instance ToJSON Hanafuda.KoiKoi.Mode instance ToJSON Hanafuda.KoiKoi.Mode
instance ToJSON Hanafuda.KoiKoi.Step where instance ToJSON Hanafuda.KoiKoi.Step where
toEncoding = genericToEncoding JSON.defaultOptions toEncoding = genericToEncoding defaultOptions
instance ToJSON1 (Hanafuda.Player.Player Player.Key) where instance ToJSON (Hanafuda.Player.Player Hanafuda.KoiKoi.Score) where
liftToEncoding = genericLiftToEncoding JSON.defaultOptions toJSON = toJSON
toEncoding = toEncoding
instance ToJSON (Hanafuda.Player.Player Player.Key Hanafuda.KoiKoi.Score) where
toJSON = toJSON1
toEncoding = toEncoding1
instance ToJSON Hanafuda.KoiKoi.Yaku where instance ToJSON Hanafuda.KoiKoi.Yaku where
toEncoding = genericToEncoding JSON.defaultOptions toEncoding = genericToEncoding defaultOptions
instance ToJSONKey Hanafuda.KoiKoi.Yaku where instance ToJSONKey Hanafuda.KoiKoi.Yaku where
toJSONKey = toJSONKeyText (pack . show) toJSONKey = toJSONKeyText (pack . show)
instance ToJSON1 (Hanafuda.Player.Players Player.Key) where instance ToJSON (Hanafuda.Player.Players Hanafuda.KoiKoi.Score) where
liftToEncoding = genericLiftToEncoding JSON.defaultOptions toJSON = toJSON
toEncoding = toEncoding
instance ToJSON (Hanafuda.Player.Players Player.Key Hanafuda.KoiKoi.Score) where
toJSON = toJSON1
toEncoding = toEncoding1
type Key = Data.Key T type Key = Data.Key T
new :: Player.Key -> Player.Key -> IO T new :: Hanafuda.KoiKoi.PlayerKey -> Hanafuda.KoiKoi.PlayerKey -> IO T
new p1 p2 = do new p1 p2 = do
Hanafuda.KoiKoi.new [p1, p2] $ Hanafuda.KoiKoi.WholeYear Hanafuda.KoiKoi.new [p1, p2] $ Hanafuda.KoiKoi.WholeYear
export :: Player.Key -> T -> Value export :: Hanafuda.KoiKoi.PlayerKey -> T -> Value
export key game = Object $ insert "deck" (toJSON $ length $ Hanafuda.KoiKoi.deck game) $ ast export key game = Object $ insert "deck" (toJSON $ length $ Hanafuda.KoiKoi.deck game) $ ast
where where
Hanafuda.Player.Players unfiltered = Hanafuda.KoiKoi.players game Hanafuda.Player.Players unfiltered = Hanafuda.KoiKoi.players game
@ -89,7 +80,7 @@ export key game = Object $ insert "deck" (toJSON $ length $ Hanafuda.KoiKoi.deck
Hanafuda.KoiKoi.players = Hanafuda.Player.Players $ mapWithKey maskOpponentsHand unfiltered Hanafuda.KoiKoi.players = Hanafuda.Player.Players $ mapWithKey maskOpponentsHand unfiltered
} }
play :: Hanafuda.KoiKoi.Environment m => Player.Key -> Hanafuda.KoiKoi.Move -> T -> m (Hanafuda.KoiKoi.Game Player.Key) play :: Hanafuda.KoiKoi.Environment m => Hanafuda.KoiKoi.PlayerKey -> Hanafuda.KoiKoi.Move -> T -> m T
play key move game play key move game
| Hanafuda.KoiKoi.playing game == key = | Hanafuda.KoiKoi.playing game == key =
Hanafuda.KoiKoi.play move game Hanafuda.KoiKoi.play move game

View file

@ -14,13 +14,14 @@ import qualified Config (listenPort)
import qualified Session (open) import qualified Session (open)
import qualified Server (disconnect, new, register) import qualified Server (disconnect, new, register)
import qualified App (Context(..), T, update_) import qualified App (Context(..), T, update_)
import qualified Message (FromClient(..), broadcast, relay) import qualified Hanafuda.Message as Message (FromClient(..))
import Messaging (broadcast, relay)
import qualified Automaton (start) import qualified Automaton (start)
exit :: App.T () exit :: App.T ()
exit = do exit = do
asks App.key >>= App.update_ . Server.disconnect asks App.key >>= App.update_ . Server.disconnect
Message.relay Message.LogOut Message.broadcast relay Message.LogOut broadcast
serverApp :: App.T () -> App.T () -> IO ServerApp serverApp :: App.T () -> App.T () -> IO ServerApp
serverApp onEnter onExit = do serverApp onEnter onExit = do

View file

@ -1,7 +1,6 @@
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
module Message ( module Messaging (
FromClient(..) FromClient(..)
, T(..) , T(..)
, broadcast , broadcast
@ -17,20 +16,19 @@ module Message (
import Data.List (intercalate) import Data.List (intercalate)
import Data.Foldable (forM_) import Data.Foldable (forM_)
import Data.Map (keys) import Data.Map (keys)
import Data.Aeson (FromJSON(..), ToJSON(..), Value, eitherDecode', encode, genericParseJSON, genericToEncoding, defaultOptions) import Data.Aeson (eitherDecode', encode)
import Network.WebSockets (receiveData, sendTextData) import Network.WebSockets (receiveData, sendTextData)
import Data.ByteString.Lazy.Char8 (unpack) import Data.ByteString.Lazy.Char8 (unpack)
import Data.Text (Text)
import Control.Monad.Reader (asks, lift) import Control.Monad.Reader (asks, lift)
import qualified Player (Key)
import qualified Game (T, export) import qualified Game (T, export)
import qualified Session (T(..)) import qualified Session (T(..))
import qualified Server (T(..), get) import qualified Server (T(..), get)
import qualified App (Context(..), T, connection, debug, server) import qualified App (Context(..), T, connection, debug, server)
import qualified Hanafuda.KoiKoi as KoiKoi (Action, Game(..), Move(..)) import qualified Hanafuda.KoiKoi as KoiKoi (Action, Game(..), PlayerKey)
import GHC.Generics (Generic) import qualified Hanafuda.Message as Message (T)
import Hanafuda.Message (FromClient(..), T(..))
sendTo :: [Player.Key] -> T -> App.T () sendTo :: [KoiKoi.PlayerKey] -> Message.T -> App.T ()
sendTo playerKeys obj = do sendTo playerKeys obj = do
sessions <- getSessions <$> App.server sessions <- getSessions <$> App.server
App.debug $ '(' : intercalate ", " recipients ++ ") <" ++ (unpack encoded) App.debug $ '(' : intercalate ", " recipients ++ ") <" ++ (unpack encoded)
@ -40,16 +38,16 @@ sendTo playerKeys obj = do
getSessions server = (\key -> Server.get key server) <$> playerKeys getSessions server = (\key -> Server.get key server) <$> playerKeys
recipients = show <$> playerKeys recipients = show <$> playerKeys
send :: T -> App.T () send :: Message.T -> App.T ()
send obj = do send obj = do
key <- asks App.key key <- asks App.key
sendTo [key] obj sendTo [key] obj
broadcast :: T -> App.T () broadcast :: Message.T -> App.T ()
broadcast obj = broadcast obj =
App.server >>= flip sendTo obj . keys . Server.sessions App.server >>= flip sendTo obj . keys . Server.sessions
relay :: FromClient -> (T -> App.T ()) -> App.T () relay :: FromClient -> (Message.T -> App.T ()) -> App.T ()
relay message f = do relay message f = do
App.debug "Relaying" App.debug "Relaying"
(\from -> f $ Relay {from, message}) =<< asks App.key (\from -> f $ Relay {from, message}) =<< asks App.key
@ -59,10 +57,10 @@ receive = do
received <- ((lift . receiveData) =<< App.connection) received <- ((lift . receiveData) =<< App.connection)
App.debug $ '>':(unpack received) App.debug $ '>':(unpack received)
case eitherDecode' received of case eitherDecode' received of
Left errorMessage -> send (Message.Error errorMessage) >> receive Left errorMessage -> send (Error errorMessage) >> receive
Right clientMessage -> return clientMessage Right clientMessage -> return clientMessage
get :: App.T Message.FromClient get :: App.T FromClient
get = get =
receive >>= pong receive >>= pong
where where

View file

@ -3,18 +3,15 @@
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Player ( module Player (
Key T(..)
, T(..)
) where ) where
import Data.Text (Text) import Data.Text (Text)
import qualified Data (Key)
import GHC.Generics import GHC.Generics
data T = T { data T = T {
name :: Text name :: Text
} deriving (Eq, Ord, Generic) } deriving (Eq, Ord, Generic)
type Key = Data.Key T
{- {-
instance FromJSON Key instance FromJSON Key

View file

@ -14,24 +14,25 @@ module Server (
, logOut , logOut
, new , new
, register , register
, room
, update , update
) where ) where
import Data.Aeson (ToJSON(..), (.=), object, pairs)
import Data.Map (Map, (!), (!?), adjust, delete, insert, lookupMax, mapWithKey) import Data.Map (Map, (!), (!?), adjust, delete, insert, lookupMax, mapWithKey)
import qualified Data.Map as Map (empty) import qualified Data.Map as Map (empty)
import Data.Monoid ((<>))
import Data.Set (Set, member) import Data.Set (Set, member)
import qualified Data.Set as Set (delete, empty, insert) import qualified Data.Set as Set (delete, empty, insert)
import Data.Text (Text) import Data.Text (Text)
import Hanafuda.KoiKoi (PlayerKey)
import Hanafuda.Message (PlayerStatus(..), Room)
import qualified Data (RW(..)) import qualified Data (RW(..))
import qualified Game (Key, T) import qualified Game (Key, T)
import qualified Player (Key, T(..)) import qualified Player (T(..))
import qualified Session (Status(..), T(..), Update) import qualified Session (Status(..), T(..), Update)
type Names = Set Text type Names = Set Text
type Players = Map Player.Key Player.T type Players = Map PlayerKey Player.T
type Sessions = Map Player.Key Session.T type Sessions = Map PlayerKey Session.T
type Games = Map Game.Key Game.T type Games = Map Game.Key Game.T
data T = T { data T = T {
names :: Names names :: Names
@ -56,22 +57,22 @@ instance Data.RW Games T where
get = games get = games
set games server = server {games} set games server = server {games}
newtype Player = Player (Text, Bool) export :: Sessions -> PlayerKey -> Player.T -> PlayerStatus
instance ToJSON Player where export sessions key player = PlayerStatus (Player.name player, alone)
toJSON (Player (name, alone)) = object ["name" .= name, "alone" .= alone]
toEncoding (Player (name, alone)) = pairs ("name" .= name <> "alone" .= alone)
export :: Sessions -> Player.Key -> Player.T -> Player
export sessions key player = Player (Player.name player, alone)
where where
alone = alone =
case Session.status (sessions ! key) of case Session.status (sessions ! key) of
Session.LoggedIn True -> True Session.LoggedIn True -> True
_ -> False _ -> False
room :: T -> Room
room (T {players, sessions}) = mapWithKey (export sessions) players
{-
instance ToJSON T where instance ToJSON T where
toJSON (T {players, sessions}) = toJSON $ mapWithKey (export sessions) players toJSON (T {players, sessions}) = toJSON $ mapWithKey (export sessions) players
toEncoding (T {players, sessions}) = toEncoding $ mapWithKey (export sessions) players toEncoding (T {players, sessions}) = toEncoding $ mapWithKey (export sessions) players
-}
new :: T new :: T
new = T { new = T {
@ -93,7 +94,7 @@ update :: forall a b. (Ord a, Data.RW (Map a b) T) => a -> (b -> b) -> T -> T
update key updator = update key updator =
Data.update (adjust updator key :: Map a b -> Map a b) Data.update (adjust updator key :: Map a b -> Map a b)
disconnect :: Player.Key -> T -> T disconnect :: PlayerKey -> T -> T
disconnect key = disconnect key =
Data.update (delete key :: Sessions -> Sessions) . logOut key Data.update (delete key :: Sessions -> Sessions) . logOut key
@ -101,7 +102,7 @@ endGame :: Game.Key -> T -> T
endGame key = endGame key =
Data.update (delete key :: Games -> Games) Data.update (delete key :: Games -> Games)
logIn :: Text -> Player.Key -> T -> Either String T logIn :: Text -> PlayerKey -> T -> Either String T
logIn name key server = logIn name key server =
Data.update (Set.insert name) . Data.update (Set.insert name) .
Data.update (insert key $ Player.T {Player.name}) . Data.update (insert key $ Player.T {Player.name}) .
@ -110,7 +111,7 @@ logIn name key server =
then Left "This name is already registered" then Left "This name is already registered"
else Right server else Right server
logOut :: Player.Key -> T -> T logOut :: PlayerKey -> T -> T
logOut key server = logOut key server =
maybe maybe
server server

View file

@ -11,15 +11,15 @@ module Session (
import Network.WebSockets (Connection) import Network.WebSockets (Connection)
import Data.Aeson (ToJSON(..), genericToEncoding) import Data.Aeson (ToJSON(..), genericToEncoding)
import GHC.Generics (Generic) import GHC.Generics (Generic)
import Hanafuda.KoiKoi (PlayerKey)
import qualified JSON (singleLCField) import qualified JSON (singleLCField)
import qualified Data (RW(..)) import qualified Data (RW(..))
import qualified Player (Key)
import qualified Game (Key) import qualified Game (Key)
data Status = data Status =
LoggedIn Bool LoggedIn Bool
| Answering Player.Key | Answering PlayerKey
| Waiting Player.Key | Waiting PlayerKey
| Playing Game.Key | Playing Game.Key
deriving (Show, Generic) deriving (Show, Generic)