Add a built-in deck source and the CaH base deck.

This does a lot of the work needed for #120.
This commit is contained in:
Gareth Latty
2020-05-01 01:11:50 +01:00
parent 3024b9430a
commit 17331589f0
52 changed files with 2450 additions and 301 deletions
+1 -1
View File
@@ -13,6 +13,7 @@
"elm/http": "2.0.0",
"elm/json": "1.1.3",
"elm/random": "1.0.0",
"elm/regex": "1.0.0",
"elm/svg": "1.0.1",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
@@ -29,7 +30,6 @@
"indirect": {
"elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/regex": "1.0.0",
"elm/virtual-dom": "1.0.2",
"elm-community/list-extra": "8.2.3",
"owanturist/elm-union-find": "1.0.0"
+3 -3
View File
@@ -4868,9 +4868,9 @@
}
},
"html-webpack-plugin": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.2.1.tgz",
"integrity": "sha512-zTTPxKJ8bgRe4RVDzT1MZW8ysW5wwDfJmD3AN+7mw2MKMWZJibZzBgHaDqnL6FJg1kvk38sQPMJNmI8Q1Ntr9A==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.3.0.tgz",
"integrity": "sha512-C0fzKN8yQoVLTelcJxZfJCE+aAvQiY2VUf3UuKrR4a9k5UMWYOtpDLsaXwATbcVCnI05hUS7L9ULQHWLZhyi3w==",
"dev": true,
"requires": {
"@types/html-minifier-terser": "^5.0.0",
+1 -1
View File
@@ -43,7 +43,7 @@
"extract-loader": "^5.0.1",
"file-loader": "^6.0.0",
"html-loader": "^1.1.0",
"html-webpack-plugin": "^4.2.1",
"html-webpack-plugin": "^4.3.0",
"node-sass": "^4.14.0",
"postcss-import": "^12.0.1",
"postcss-loader": "^3.0.0",
+16 -2
View File
@@ -4,6 +4,7 @@ import Browser
import Browser.Navigation as Navigation
import Html exposing (Html)
import Html.Attributes as HtmlA
import Http
import Json.Decode as Json
import MassiveDecks.Cast.Client as Cast
import MassiveDecks.Cast.Model as Cast
@@ -28,6 +29,8 @@ import MassiveDecks.Pages.Route as Route exposing (Route)
import MassiveDecks.Pages.Start as Start
import MassiveDecks.Pages.Start.Route as Start
import MassiveDecks.Pages.Unknown as Unknown
import MassiveDecks.Requests.Api as Api
import MassiveDecks.Requests.Request as Request
import MassiveDecks.ServerConnection as ServerConnection
import MassiveDecks.Settings as Settings
import MassiveDecks.Settings.Messages as Settings
@@ -85,6 +88,7 @@ init flags url key =
, speech = speech
, notifications = Notifications.init
, remoteMode = remoteMode
, sources = { builtIn = Nothing, cardcast = False }
}
( page, pageCmd ) =
@@ -93,9 +97,12 @@ init flags url key =
else
( Pages.Loading, Cmd.none )
sourceCmd =
Request.map (Error.Add >> ErrorMsg) never UpdateSources |> Api.sourceInfo |> Http.request
in
( { page = page, shared = shared, errorOverlay = Overlay.init }
, Cmd.batch [ pageCmd, settingsCmd, speechCmd ]
, Cmd.batch [ sourceCmd, pageCmd, settingsCmd, speechCmd ]
)
@@ -264,6 +271,13 @@ update msg model =
in
( { model | shared = { oldShared | notifications = notifications } }, notificationsCmd )
UpdateSources info ->
let
oldShared =
model.shared
in
( { model | shared = { oldShared | sources = info } }, Cmd.none )
Refresh ->
( model, Navigation.reload )
@@ -284,7 +298,7 @@ update msg model =
Settings.update model.shared (Settings.ChangeLang (Just language))
( lobby, lobbyCmd ) =
Lobby.initWithAuth { gameCode = auth.claims.gc, section = Just Lobby.Spectate } auth
Lobby.initWithAuth shared { gameCode = auth.claims.gc, section = Just Lobby.Spectate } auth
in
( { model
| page = Pages.Lobby lobby
+38 -18
View File
@@ -1,6 +1,7 @@
module MassiveDecks.Card.Parts exposing
( Part(..)
, Parts
, Style(..)
, Transform(..)
, fromList
, map
@@ -19,16 +20,24 @@ import MassiveDecks.Util.String as String
{-| A transform to apply to the value in a slot.
-}
type Transform
= UpperCase
= NoTransform
| UpperCase
| Capitalize
| Stay
{-| A style to be applied to some text.
-}
type Style
= NoStyle
| Em
| Strong
{-| A part of a call's text. This is either just text or a position for a call to be inserted in-game.
-}
type Part
= Text String
| Slot Transform
= Text String Style
| Slot Transform Style
{-| Represents a line as a part of a part. Between each one the text will be forced to line break.
@@ -48,10 +57,10 @@ type Parts
isSlot : Part -> Bool
isSlot part =
case part of
Text _ ->
Text _ _ ->
False
Slot _ ->
Slot _ _ ->
True
@@ -136,18 +145,18 @@ viewLinesString blankPhrase =
viewLines (\s -> \p -> viewPartsString blankPhrase s p |> String.join "")
viewParts : (Bool -> String -> List a) -> a -> List String -> List Part -> List a
viewParts : (Bool -> String -> Style -> List a) -> a -> List String -> List Part -> List a
viewParts viewText emptySlot play parts =
case parts of
firstPart :: restParts ->
case firstPart of
Text string ->
viewText False string ++ viewParts viewText emptySlot play restParts
Text string style ->
viewText False string style ++ viewParts viewText emptySlot play restParts
Slot transform ->
Slot transform style ->
case play of
firstPlay :: restPlay ->
viewText True (applyTransform transform firstPlay) ++ viewParts viewText emptySlot restPlay restParts
viewText True (applyTransform transform firstPlay) style ++ viewParts viewText emptySlot restPlay restParts
[] ->
emptySlot :: viewParts viewText emptySlot [] restParts
@@ -163,27 +172,38 @@ viewPartsHtml =
viewPartsString : String -> List String -> List Part -> List String
viewPartsString blankPhrase =
viewParts (\_ -> \s -> [ s ]) blankPhrase
viewParts (\_ -> \s -> \_ -> [ s ]) blankPhrase
applyTransform : Transform -> String -> String
applyTransform transform value =
case transform of
NoTransform ->
value
UpperCase ->
String.toUpper value
Capitalize ->
String.capitalise value
Stay ->
value
viewTextHtml : Bool -> String -> List (Html msg)
viewTextHtml slot string =
viewTextHtml : Bool -> String -> Style -> List (Html msg)
viewTextHtml slot string style =
let
element =
case style of
NoStyle ->
Html.span
Em ->
Html.em
Strong ->
Html.strong
words =
string |> splitWords |> List.map (\word -> Html.span [] [ Html.text word ])
string |> splitWords |> List.map (\word -> element [] [ Html.text word ])
in
if slot then
[ Html.span [ HtmlA.class "slot" ] words ]
+16 -1
View File
@@ -16,6 +16,7 @@ import MassiveDecks.Model exposing (Shared)
import MassiveDecks.Pages.Lobby.Configure.Decks as Decks
import MassiveDecks.Pages.Lobby.Configure.Model exposing (Config)
import MassiveDecks.Util.String as String
import Regex exposing (Regex)
{-| Render the response to HTML.
@@ -71,9 +72,23 @@ viewCustom shared config side update canonicalize attributes response fill =
{- Private -}
punctuation : Regex
punctuation =
-- TODO: This should probably get localized.
Regex.fromString "[.?!]$" |> Maybe.withDefault Regex.never
viewBody : Response -> ViewBody msg
viewBody response =
ViewBody (\() -> [ Html.p [] [ Html.span [] [ response.body |> String.capitalise |> Html.text ] ] ])
let
end =
if response.body |> Regex.contains punctuation then
[]
else
[ Html.text "." ]
in
ViewBody (\() -> [ Html.p [] [ Html.span [] ((response.body |> String.capitalise |> Html.text) :: end) ] ])
viewCustomBody : String -> (String -> msg) -> (String -> msg) -> String -> Maybe String -> ViewBody msg
+40 -17
View File
@@ -17,6 +17,7 @@ module MassiveDecks.Card.Source exposing
import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import MassiveDecks.Card.Source.BuiltIn as BuiltIn
import MassiveDecks.Card.Source.Cardcast as Cardcast
import MassiveDecks.Card.Source.Custom as Player
import MassiveDecks.Card.Source.Fake as Fake
@@ -27,15 +28,16 @@ import MassiveDecks.Model exposing (..)
import MassiveDecks.Pages.Lobby.Configure.Decks.Model exposing (DeckOrError)
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.Util.Maybe as Maybe
import Weightless as Wl
import Weightless.Attributes as WlA
{-| The default source for an editor.
-}
default : External
default : Shared -> External
default =
Cardcast.generalMethods.empty ()
BuiltIn.generalMethods.empty
{-| Check if two sources are equal.
@@ -57,23 +59,33 @@ externalAndEquals a b =
False
{-| Get an empty source of the given type.
{-| Get an general methods of the given type.
-}
empty : String -> Maybe External
empty n =
generalMethods : String -> Maybe (ExternalGeneralMethods msg)
generalMethods n =
case n of
"BuiltIn" ->
BuiltIn.generalMethods |> Just
"Cardcast" ->
() |> Cardcast.generalMethods.empty |> Just
Cardcast.generalMethods |> Just
_ ->
Nothing
{-| Get an empty source of the given type.
-}
empty : Shared -> String -> Maybe External
empty shared n =
generalMethods n |> Maybe.map (\m -> m.empty shared)
{-| An empty source of the same general type as the given one.
-}
emptyMatching : External -> External
emptyMatching source =
() |> (externalMethods source |> .empty)
emptyMatching : Shared -> External -> External
emptyMatching shared source =
shared |> (externalMethods source |> .empty)
{-| The name of a source.
@@ -99,9 +111,9 @@ defaultDetails shared source =
{-| A tooltip for a source.
-}
tooltip : Source -> Maybe ( String, Html msg )
tooltip source =
case () |> (methods source |> .tooltip) of
tooltip : Shared -> Source -> Maybe ( String, Html msg )
tooltip shared source =
case shared |> (methods source |> .tooltip) of
Just ( id, rendered ) ->
Just
( id
@@ -134,15 +146,23 @@ logo source =
-}
generalEditor : Shared -> List DeckOrError -> External -> (External -> msg) -> List (Html msg)
generalEditor shared existing currentValue update =
let
enabledSources =
[ shared.sources.builtIn |> Maybe.map (\_ -> BuiltIn.generalMethods)
, Cardcast.generalMethods |> Maybe.justIf shared.sources.cardcast
]
toOption source =
Html.option [ HtmlA.value (source.id ()) ]
[ () |> source.name |> Lang.html shared
]
in
[ Wl.select
[ HtmlA.id "source-selector"
, WlA.outlined
, HtmlE.onInput (empty >> Maybe.withDefault default >> update)
]
[ Html.option [ HtmlA.value "Cardcast" ]
[ Strings.Cardcast |> Lang.html shared
]
, HtmlE.onInput (empty shared >> Maybe.withDefault (default shared) >> update)
]
(enabledSources |> List.filterMap (Maybe.map toOption))
, editor shared existing currentValue update
]
@@ -197,3 +217,6 @@ externalMethods external =
case external of
Cardcast playCode ->
Cardcast.methods playCode
BuiltIn id ->
BuiltIn.methods id
@@ -0,0 +1,132 @@
module MassiveDecks.Card.Source.BuiltIn exposing
( generalMethods
, methods
)
import Html as Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Events as HtmlE
import MassiveDecks.Card.Source.BuiltIn.Model exposing (..)
import MassiveDecks.Card.Source.Methods as Source
import MassiveDecks.Card.Source.Model as Source exposing (Source)
import MassiveDecks.Components.Form.Message exposing (Message)
import MassiveDecks.Model exposing (..)
import MassiveDecks.Pages.Lobby.Configure.Decks.Model as Decks exposing (DeckOrError)
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Util.Html as Html
import Weightless as Wl
import Weightless.Attributes as WlA
methods : Id -> Source.ExternalMethods msg
methods given =
{ name = name
, logo = logo
, empty = empty
, id = id
, problems = problems given
, defaultDetails = details given
, tooltip = tooltip given
, editor = editor given
, equals = equals given
}
generalMethods : Source.ExternalGeneralMethods msg
generalMethods =
{ name = name
, logo = logo
, empty = empty
, id = id
}
{- Private -}
id : () -> String
id _ =
"BuiltIn"
name : () -> MdString
name () =
Strings.BuiltIn
empty : Shared -> Source.External
empty shared =
shared.sources.builtIn
|> Maybe.andThen (.decks >> List.head)
|> Maybe.map .id
|> Maybe.withDefault (Id "")
|> Source.BuiltIn
equals : Id -> Source.External -> Bool
equals (Id given) source =
case source of
Source.BuiltIn (Id other) ->
given == other
_ ->
False
problems : Id -> () -> List (Message msg)
problems _ () =
[]
editor : Id -> Shared -> List DeckOrError -> (Source.External -> msg) -> Html msg
editor (Id selectedId) shared _ update =
case shared.sources.builtIn of
Just { decks } ->
let
deck deckInfo =
case deckInfo.id of
Id other ->
Html.option
[ HtmlA.selected (selectedId == other)
, HtmlA.value other
]
[ Html.text deckInfo.name ]
in
Html.div [ HtmlA.class "primary" ]
[ Wl.select
[ HtmlA.id "built-in-selector"
, WlA.outlined
, Id >> Source.BuiltIn >> update |> HtmlE.onInput
]
(decks |> List.map deck)
]
Nothing ->
Html.nothing
details : Id -> Shared -> Source.Details
details (Id given) shared =
let
isSame deckInfo =
case deckInfo.id of
Id other ->
given == other
in
{ name =
shared.sources.builtIn
|> Maybe.andThen (.decks >> List.filter isSame >> List.head >> Maybe.map .name)
|> Maybe.withDefault ""
, url = Nothing
}
tooltip : Id -> Shared -> Maybe ( String, Html msg )
tooltip (Id given) shared =
( "builtin-" ++ given, Html.span [] [ details (Id given) shared |> .name |> Html.text ] ) |> Just
logo : () -> Maybe (Html msg)
logo () =
Nothing
@@ -0,0 +1,17 @@
module MassiveDecks.Card.Source.BuiltIn.Model exposing (Id(..))
{-| Models for the built-in source.
-}
{-| The id for a built-in deck.
-}
type Id
= Id String
{-| Create an id from a string.
-}
playCode : String -> Id
playCode string =
string |> Id
@@ -25,6 +25,7 @@ methods playCode =
{ name = name
, logo = logo
, empty = empty
, id = id
, problems = problems playCode
, defaultDetails = details playCode
, tooltip = tooltip playCode
@@ -38,6 +39,7 @@ generalMethods =
{ name = name
, logo = logo
, empty = empty
, id = id
}
@@ -45,13 +47,18 @@ generalMethods =
{- Private -}
id : () -> String
id () =
"Cardcast"
name : () -> MdString
name () =
Strings.Cardcast
empty : () -> Source.External
empty () =
empty : Shared -> Source.External
empty shared =
"" |> playCode |> Source.Cardcast
@@ -61,6 +68,9 @@ equals (PlayCode pc) source =
Source.Cardcast (PlayCode other) ->
pc == other
_ ->
False
problems : PlayCode -> () -> List (Message msg)
problems (PlayCode pc) () =
@@ -92,6 +102,9 @@ editor (PlayCode pc) shared existing update =
in
playCode |> Maybe.justIf (existing |> List.all notSameDeck)
_ ->
Nothing
recentDeck (PlayCode recent) =
Html.option [ HtmlA.value recent ] []
in
@@ -116,8 +129,8 @@ details (PlayCode pc) shared =
}
tooltip : PlayCode -> () -> Maybe ( String, Html msg )
tooltip (PlayCode pc) () =
tooltip : PlayCode -> Shared -> Maybe ( String, Html msg )
tooltip (PlayCode pc) _ =
( "cardcast-" ++ pc, Html.span [] [ logoInternal, Html.text pc ] ) |> Just
@@ -15,7 +15,7 @@ methods =
{ name = name shared
, url = Nothing
}
, tooltip = \() -> Nothing
, tooltip = \_ -> Nothing
}
@@ -13,5 +13,5 @@ methods =
{ name = ""
, url = Nothing
}
, tooltip = \() -> Nothing
, tooltip = \_ -> Nothing
}
@@ -74,7 +74,7 @@ type alias ExternalGeneralMethods msg =
-}
type alias IsSpecific general msg =
{ general
| tooltip : () -> Maybe ( String, Html msg )
| tooltip : Shared -> Maybe ( String, Html msg )
, defaultDetails : Shared -> Details
}
@@ -93,5 +93,6 @@ type alias IsSpecificExternal general msg =
-}
type alias IsGeneralExternal rest =
{ rest
| empty : () -> External
| empty : Shared -> External
, id : () -> String
}
@@ -1,11 +1,15 @@
module MassiveDecks.Card.Source.Model exposing
( Details
( BuiltInDeck
, BuiltInInfo
, Details
, External(..)
, Info
, LoadFailureReason(..)
, Source(..)
, Summary
)
import MassiveDecks.Card.Source.BuiltIn.Model as BuiltIn
import MassiveDecks.Card.Source.Cardcast.Model as Cardcast
@@ -31,7 +35,8 @@ sources are more limited and specific.
-}
type External
= Cardcast Cardcast.PlayCode
= BuiltIn BuiltIn.Id
| Cardcast Cardcast.PlayCode
{-| A summary of the contents of the source deck.
@@ -56,3 +61,19 @@ type alias Details =
type LoadFailureReason
= SourceFailure
| NotFound
{-| Information about what sources are available from the server.
-}
type alias Info =
{ builtIn : Maybe BuiltInInfo
, cardcast : Bool
}
type alias BuiltInInfo =
{ decks : List BuiltInDeck }
type alias BuiltInDeck =
{ name : String, id : BuiltIn.Id }
+2
View File
@@ -1,5 +1,6 @@
module MassiveDecks.Messages exposing (Msg(..))
import MassiveDecks.Card.Source.Model as Source
import MassiveDecks.Cast.Model as Cast
import MassiveDecks.Error.Messages as Error
import MassiveDecks.Notifications.Model as Notifications
@@ -27,5 +28,6 @@ type Msg
| UpdateToken Lobby.Auth
| CastStatusUpdate Cast.Status
| RemoteCommand Cast.RemoteControlCommand
| UpdateSources Source.Info
| Refresh
| BlockedExternalUrl
+2
View File
@@ -4,6 +4,7 @@ module MassiveDecks.Model exposing
)
import Browser.Navigation as Navigation
import MassiveDecks.Card.Source.Model as Sources
import MassiveDecks.Cast.Model as Cast
import MassiveDecks.Notifications.Model as Notifications
import MassiveDecks.Settings.Model as Settings exposing (Settings)
@@ -23,6 +24,7 @@ type alias Shared =
, speech : Speech.Model
, notifications : Notifications.Model
, remoteMode : Bool
, sources : Sources.Info
}
+69 -16
View File
@@ -16,6 +16,7 @@ module MassiveDecks.Models.Decoders exposing
, remoteControlCommand
, revealingRound
, settings
, sourceInfo
, tokenValidity
, userId
, userSummary
@@ -28,6 +29,7 @@ import Json.Patch
import MassiveDecks.Card.Model as Card exposing (Call, Response)
import MassiveDecks.Card.Parts as Parts exposing (Part, Parts)
import MassiveDecks.Card.Play as Play exposing (Play)
import MassiveDecks.Card.Source.BuiltIn.Model as BuiltIn
import MassiveDecks.Card.Source.Cardcast.Model as Cardcast
import MassiveDecks.Card.Source.Model as Source exposing (Source)
import MassiveDecks.Cast.Model as Cast
@@ -54,6 +56,26 @@ import MassiveDecks.User as User exposing (User)
import Set exposing (Set)
sourceInfo : Json.Decoder Source.Info
sourceInfo =
Json.succeed Source.Info
|> Json.optional "builtIn" (builtInInfo |> Json.map Just) Nothing
|> Json.optional "cardcast" Json.bool False
builtInInfo : Json.Decoder Source.BuiltInInfo
builtInInfo =
Json.succeed Source.BuiltInInfo
|> Json.required "decks" (Json.list builtInDeck)
builtInDeck : Json.Decoder Source.BuiltInDeck
builtInDeck =
Json.succeed Source.BuiltInDeck
|> Json.required "name" Json.string
|> Json.required "id" (Json.string |> Json.map BuiltIn.Id)
unknownValue : String -> String -> Json.Decoder a
unknownValue name value =
("Unknown " ++ name ++ ": \"" ++ value ++ "\".") |> Json.fail
@@ -182,6 +204,9 @@ externalSource =
externalSourceByName : String -> Json.Decoder Source.External
externalSourceByName name =
case name of
"BuiltIn" ->
Json.field "id" Json.string |> Json.map (BuiltIn.Id >> Source.BuiltIn)
"Cardcast" ->
Json.field "playCode" Json.string |> Json.map (Cardcast.playCode >> Source.Cardcast)
@@ -869,32 +894,60 @@ parts =
part : Json.Decoder Part
part =
Json.oneOf
[ Json.string |> Json.map Parts.Text
, transform |> Json.map Parts.Slot
[ Json.string |> Json.map (\t -> Parts.Text t Parts.NoStyle)
, styled
, slot
]
slot : Json.Decoder Parts.Part
slot =
Json.succeed Parts.Slot
|> Json.optional "transform" transform Parts.NoTransform
|> Json.optional "style" style Parts.NoStyle
styled : Json.Decoder Parts.Part
styled =
Json.succeed Parts.Text
|> Json.required "text" Json.string
|> Json.optional "style" style Parts.NoStyle
transform : Json.Decoder Parts.Transform
transform =
Json.maybe (Json.field "transform" Json.string) |> Json.andThen transformByName
Json.string |> Json.andThen transformByName
transformByName : Maybe String -> Json.Decoder Parts.Transform
transformByName maybeName =
case maybeName of
Nothing ->
Json.succeed Parts.Stay
transformByName : String -> Json.Decoder Parts.Transform
transformByName name =
case name of
"UpperCase" ->
Json.succeed Parts.UpperCase
Just name ->
case name of
"UpperCase" ->
Json.succeed Parts.UpperCase
"Capitalize" ->
Json.succeed Parts.Capitalize
"Capitalize" ->
Json.succeed Parts.Capitalize
_ ->
unknownValue "transform" name
_ ->
unknownValue "transform" name
style : Json.Decoder Parts.Style
style =
Json.string |> Json.andThen styleByName
styleByName : String -> Json.Decoder Parts.Style
styleByName name =
case name of
"Em" ->
Json.succeed Parts.Em
"Strong" ->
Json.succeed Parts.Strong
_ ->
unknownValue "style" name
round : Json.Decoder Round
@@ -23,6 +23,7 @@ module MassiveDecks.Models.Encoders exposing
import Dict
import Json.Encode as Json
import MassiveDecks.Card.Source.BuiltIn.Model as BuiltIn
import MassiveDecks.Card.Source.Cardcast.Model as Cardcast
import MassiveDecks.Card.Source.Model as Source exposing (Source)
import MassiveDecks.Cast.Model as Cast
@@ -326,6 +327,9 @@ source s =
Source.Cardcast (Cardcast.PlayCode playCode) ->
Json.object [ ( "source", "Cardcast" |> Json.string ), ( "playCode", playCode |> Json.string ) ]
Source.BuiltIn (BuiltIn.Id id) ->
Json.object [ ( "source", "BuiltIn" |> Json.string ), ( "id", id |> Json.string ) ]
language : Language -> Json.Value
language l =
+4 -4
View File
@@ -82,16 +82,16 @@ init shared r auth =
]
in
fallbackAuth
|> Maybe.map (initWithAuth r >> Route.Continue)
|> Maybe.map (initWithAuth shared r >> Route.Continue)
|> Maybe.withDefault (Route.Redirect (Route.Start { section = Start.Join (Just r.gameCode) }))
initWithAuth : Route -> Auth -> ( Model, Cmd msg )
initWithAuth r auth =
initWithAuth : Shared -> Route -> Auth -> ( Model, Cmd msg )
initWithAuth shared r auth =
( { route = r
, auth = auth
, lobby = Nothing
, configure = Configure.init
, configure = Configure.init shared
, notificationId = 0
, notifications = []
, inviteDialogOpen = False
@@ -56,11 +56,11 @@ import Weightless as Wl
import Weightless.Attributes as WlA
init : Model
init =
init : Shared -> Model
init shared =
{ localConfig = default
, tab = Decks
, decks = Decks.init
, decks = Decks.init shared
, privacy = Privacy.init
, timeLimits = TimeLimits.init
, rules = Rules.init
@@ -43,9 +43,9 @@ componentById id =
all
init : Model
init =
{ toAdd = Source.default }
init : Shared -> Model
init shared =
{ toAdd = Source.default shared }
default : Config
@@ -66,7 +66,7 @@ update shared msg model =
( settings, settingsCmd ) =
Settings.onAddDeck source shared.settings
in
( { model | toAdd = Source.emptyMatching source }
( { model | toAdd = Source.emptyMatching shared source }
, { shared | settings = settings }
, Cmd.batch [ Actions.configure (addDeck source), settingsCmd ]
)
@@ -226,7 +226,7 @@ submitDeckAction wrap existing deckToAdd =
let
potentialProblems =
if List.any (getSource >> Source.equals deckToAdd) existing then
[ Strings.DeckAlreadyAdded |> Message.error ]
[ Strings.DeckAlreadyAdded |> Message.info ]
else
Source.problems deckToAdd
@@ -308,7 +308,7 @@ name wrap shared canEdit index source loading maybeError details =
|> Maybe.justIf canEdit
( maybeId, tooltip ) =
source |> Source.Ex |> Source.tooltip |> Maybe.decompose
source |> Source.Ex |> Source.tooltip shared |> Maybe.decompose
attrs =
maybeId |> Maybe.map (\id -> [ HtmlA.id id ]) |> Maybe.withDefault []
+7 -2
View File
@@ -585,8 +585,13 @@ examplePick2 : Card.Call
examplePick2 =
Card.call
(Parts.unsafeFromList
[ [ Parts.Slot Parts.Stay, Parts.Text " + ", Parts.Slot Parts.Stay ]
, [ Parts.Text " = ", Parts.Slot Parts.Stay ]
[ [ Parts.Slot Parts.NoTransform Parts.NoStyle
, Parts.Text " + " Parts.NoStyle
, Parts.Slot Parts.NoTransform Parts.NoStyle
]
, [ Parts.Text " = " Parts.NoStyle
, Parts.Slot Parts.NoTransform Parts.NoStyle
]
]
)
""
@@ -5,4 +5,4 @@ import MassiveDecks.Requests.HttpData.Messages as HttpData
type Msg
= SummaryUpdate (HttpData.Msg () (List Summary))
= SummaryUpdate (HttpData.Msg Never (List Summary))
@@ -12,7 +12,7 @@ import MassiveDecks.Requests.HttpData.Model exposing (HttpData)
{-| The model for the lobby browser.
-}
type alias Model =
HttpData () (List Summary)
HttpData Never (List Summary)
{-| An external summary of a lobby.
@@ -9,7 +9,7 @@ import MassiveDecks.Requests.HttpData.Messages as HttpData
type Msg
= GameCodeChanged String
| NameChanged String
| StartGame (HttpData.Msg () Lobby.Auth)
| StartGame (HttpData.Msg Never Lobby.Auth)
| JoinGame (HttpData.Msg MdError Lobby.Auth)
| LobbyBrowserMsg LobbyBrowser.Msg
| PasswordChanged String
@@ -20,7 +20,7 @@ type alias Model =
, lobbies : LobbyBrowser.Model
, name : String
, gameCode : Maybe GameCode
, newLobbyRequest : HttpData () Lobby.Auth
, newLobbyRequest : HttpData Never Lobby.Auth
, joinLobbyRequest : HttpData MdError Lobby.Auth
, password : Maybe String
, overlay : Maybe MdString
+25 -5
View File
@@ -3,11 +3,13 @@ module MassiveDecks.Requests.Api exposing
, joinLobby
, lobbySummaries
, newLobby
, sourceInfo
)
import Dict exposing (Dict)
import Http
import Json.Decode as Json
import MassiveDecks.Card.Source.Model as Source
import MassiveDecks.Error.Model as Error
import MassiveDecks.Models.Decoders as Decoders
import MassiveDecks.Models.Encoders as Encoders
@@ -25,7 +27,7 @@ import Url.Builder
{-| List the public lobbies.
-}
lobbySummaries : (Request.Response () (List LobbyBrowser.Summary) -> msg) -> Request msg
lobbySummaries : (Request.Response Never (List LobbyBrowser.Summary) -> msg) -> Request msg
lobbySummaries msg =
{ method = "GET"
, headers = []
@@ -39,7 +41,7 @@ lobbySummaries msg =
{-| Create a new lobby.
-}
newLobby : (Request.Response () Lobby.Auth -> msg) -> Start.LobbyCreation -> Request msg
newLobby : (Request.Response Never Lobby.Auth -> msg) -> Start.LobbyCreation -> Request msg
newLobby msg creation =
{ method = "POST"
, headers = []
@@ -51,6 +53,8 @@ newLobby msg creation =
}
{-| Join a lobby.
-}
joinLobby : (Request.Response MdError Lobby.Auth -> msg) -> GameCode -> User.Registration -> Request msg
joinLobby msg gameCode registration =
{ method = "POST"
@@ -63,7 +67,9 @@ joinLobby msg gameCode registration =
}
checkAlive : (Request.Response () (Dict Lobby.Token Bool) -> msg) -> List Lobby.Token -> Request msg
{-| Check if previously joined lobbies are still going.
-}
checkAlive : (Request.Response Never (Dict Lobby.Token Bool) -> msg) -> List Lobby.Token -> Request msg
checkAlive msg tokens =
{ method = "POST"
, headers = []
@@ -75,6 +81,20 @@ checkAlive msg tokens =
}
{-| Find out what sources the server offers.
-}
sourceInfo : (Request.Response Never Source.Info -> msg) -> Request msg
sourceInfo msg =
{ method = "GET"
, headers = []
, url = url [ "sources" ]
, body = Http.emptyBody
, expect = Request.expectResponse msg noError Decoders.sourceInfo
, timeout = Nothing
, tracker = Nothing
}
{- Private -}
@@ -92,6 +112,6 @@ url path =
Url.Builder.absolute ([ "api" ] ++ path) []
noError : Json.Decoder ()
noError : Json.Decoder Never
noError =
Json.succeed ()
Json.fail "No specific errors are expected for this request."
+1
View File
@@ -204,6 +204,7 @@ type MdString
| Cardcast -- The name of the Cardcast service.
| CardcastPlayCode -- A term referring to the play code that identifies a deck on Cardcast.
| CardcastEmptyPlayCode -- A description of the problem of the entered Cardcast play code being empty.
| BuiltIn -- A term referring to decks of cards that are provided by this instance of the game.
| APlayer -- A short description of a generic player in the game in the context of being the author of a card.
| DeckAlreadyAdded -- A description of the problem of the deck already being added to the game configuration.
| ConfigureDecks -- A name for the section of the configuration screen for changing the decks for the game.
@@ -4,7 +4,7 @@ module MassiveDecks.Strings.Languages.En exposing (pack)
This is the primary language, strings here are the canonical representation, and are suitable to translate from.
-}
import MassiveDecks.Card.Source.Cardcast.Model as Cardcast
import MassiveDecks.Card.Source.BuiltIn.Model as BuiltIn
import MassiveDecks.Card.Source.Model as Source
import MassiveDecks.Strings exposing (MdString(..))
import MassiveDecks.Strings.Translation as Translation exposing (Result(..))
@@ -15,7 +15,7 @@ pack =
{ code = "en"
, name = English
, translate = translate
, recommended = "CAHBS" |> Cardcast.playCode |> Source.Cardcast
, recommended = "cah-base-en" |> BuiltIn.Id |> Source.BuiltIn
}
@@ -714,6 +714,9 @@ translate mdString =
CardcastEmptyPlayCode ->
[ Text "Enter a ", Ref CardcastPlayCode, Text " for the deck you want to add." ]
BuiltIn ->
[ Text "Built-in" ]
APlayer ->
[ Text "A Player" ]
@@ -1,8 +1,8 @@
module MassiveDecks.Strings.Languages.It exposing (pack)
{- General Italian translation -}
{- Italian translation -}
import MassiveDecks.Card.Source.Cardcast.Model as Cardcast
import MassiveDecks.Card.Source.BuiltIn.Model as BuiltIn
import MassiveDecks.Card.Source.Model as Source
import MassiveDecks.Strings exposing (MdString(..))
import MassiveDecks.Strings.Translation as Translation exposing (Result(..))
@@ -13,7 +13,7 @@ pack =
{ code = "it"
, name = Italian
, translate = translate
, recommended = "CAHBS" |> Cardcast.playCode |> Source.Cardcast
, recommended = "cah-base-en" |> BuiltIn.Id |> Source.BuiltIn
}
@@ -482,9 +482,11 @@ translate mdString =
[ Text (String.fromInt numberOfCards) ]
-- Lobby
-- TODO: Translate
LobbyNameLabel ->
[ Text "Game Name" ]
-- TODO: Translate
DefaultLobbyName { owner } ->
[ Text owner, Text "'s Game" ]
@@ -730,6 +732,10 @@ translate mdString =
CardcastEmptyPlayCode ->
[ Text "Inserisci il ", Ref CardcastPlayCode, Text " per il mazzo che vuoi aggiungere." ]
-- TODO: Translate
BuiltIn ->
[ Text "Built-in" ]
APlayer ->
[ Text "Un giocatore" ]
@@ -3,7 +3,7 @@ module MassiveDecks.Strings.Languages.PtBR exposing (pack)
{-| Brazilian Portuguese translation.
-}
import MassiveDecks.Card.Source.Cardcast.Model as Cardcast
import MassiveDecks.Card.Source.BuiltIn.Model as BuiltIn
import MassiveDecks.Card.Source.Model as Source
import MassiveDecks.Strings exposing (MdString(..))
import MassiveDecks.Strings.Translation as Translation exposing (Result(..))
@@ -14,7 +14,7 @@ pack =
{ code = "ptBR"
, name = BrazilianPortuguese
, translate = translate
, recommended = "CAHBS" |> Cardcast.playCode |> Source.Cardcast
, recommended = "cah-base-en" |> BuiltIn.Id |> Source.BuiltIn
}
@@ -482,9 +482,11 @@ translate mdString =
[ Text (String.fromInt numberOfCards) ]
-- Lobby
-- TODO: Translate
LobbyNameLabel ->
[ Text "Game Name" ]
-- TODO: Translate
DefaultLobbyName { owner } ->
[ Text owner, Text "'s Game" ]
@@ -730,6 +732,10 @@ translate mdString =
CardcastEmptyPlayCode ->
[ Text "Digite um ", Ref CardcastPlayCode, Text " para o deck que você queira adicionar." ]
-- TODO: Translate
BuiltIn ->
[ Text "Built-in" ]
APlayer ->
[ Text "Um jogador" ]
+2 -2
View File
@@ -38,7 +38,7 @@
align-content: flex-start;
justify-content: flex-start;
span {
span, em, strong {
white-space: pre-wrap;
overflow-wrap: break-word;
word-break: break-all;
@@ -47,7 +47,7 @@
.slot {
display: contents;
span {
span, em, strong {
text-decoration: underline;
}
@@ -124,6 +124,11 @@
--input-padding-top-bottom: 0.9em;
}
#built-in-selector {
height: 100%;
--input-padding-top-bottom: 0.9em;
}
.house-rules {
margin-top: 2em;
}
+31
View File
@@ -16,6 +16,37 @@
basePath: "",
// This will be overwritten by the environment variable MD_BASE_PATH, if set.
// What decks players have access to.
// Note at least one source must be enabled.
sources: {
// Decks stored as static files.
// Note that the client has references to these built-in decks as defaults, so if you disable this or change the
// decks, you will need to make changes there (they are defined in the language definitions).
builtIn: {
// The directory to look for decks in.
basePath: "decks",
// Ids for decks in the order they will be presented to the user.
// The game will look for these in the directory given by basePath above, and will expect them to have the
// .deck.json5 extension.
decks: [
"cah-base-en"
]
},
// Allows players to load decks from Cardcast via their API. https://www.cardcastgame.com/
cardcast: {
// How long to wait for a response from the Cardcast API service before giving up and telling the user there is a
// problem.
timeout: "PT10S",
// The number of connections the server can make to Cardcast at one time.
// Please remember this is a service provided by Cardcast, and you should make best efforts to ensure you don't
// hit them too hard, including not setting this too high.
simultaneousConnections: 1
}
},
// Timeouts determine how long the server waits before taking certain actions.
// These values are all ISO 8601 durations.
timeouts: {
+31
View File
@@ -16,6 +16,37 @@
basePath: "",
// This will be overwritten by the environment variable MD_BASE_PATH, if set.
// What decks players have access to.
// Note at least one source must be enabled.
sources: {
// Decks stored as static files.
// Note that the client has references to these built-in decks as defaults, so if you disable this or change the
// decks, you will need to make changes there (they are defined in the language definitions).
builtIn: {
// The directory to look for decks in.
basePath: "decks",
// Ids for decks in the order they will be presented to the user.
// The game will look for these in the directory given by basePath above, and will expect them to have the
// .deck.json5 extension.
decks: [
"cah-base-en"
]
},
// Allows players to load decks from Cardcast via their API. https://www.cardcastgame.com/
cardcast: {
// How long to wait for a response from the Cardcast API service before giving up and telling the user there is a
// problem.
timeout: "PT10S",
// The number of connections the server can make to Cardcast at one time.
// Please remember this is a service provided by Cardcast, and you should make best efforts to ensure you don't
// hit them too hard, including not setting this too high.
simultaneousConnections: 1
}
},
// Timeouts determine how long the server waits before taking certain actions.
// These values are all ISO 8601 durations.
timeouts: {
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -18,6 +18,7 @@ import { LoadDeckSummary } from "../../task/load-deck-summary";
import * as Handler from "../handler";
import { Privileged } from "../privileged";
import * as Validation from "../validation.validator";
import { ServerState } from "../../server-state";
/**
* An action to change the configuration of the lobby.
@@ -80,6 +81,7 @@ function applyRules(
}
function apply(
server: ServerState,
gameCode: GameCode,
lobby: Lobby.Lobby,
existing: Config.Config,
@@ -92,7 +94,7 @@ function apply(
);
const allTasks = [...tasks];
for (const deck of updated.decks) {
const resolver = Sources.limitedResolver(deck.source);
const resolver = server.sources.limitedResolver(deck.source);
const matching = existing.decks.find((ed) => resolver.equals(ed.source));
if (matching === undefined) {
allTasks.push(new LoadDeckSummary(gameCode, deck.source));
@@ -146,7 +148,8 @@ class ConfigureActions extends Actions.Implementation<
protected handle: Handler.Custom<Configure, Lobby.Lobby> = (
auth,
lobby,
action
action,
server
) => {
const version = lobby.config.version;
for (const op of action.change) {
@@ -163,6 +166,7 @@ class ConfigureActions extends Actions.Implementation<
}
validated = _validateConfig(patched);
const { result, events, tasks } = apply(
server,
auth.gc,
lobby,
lobby.config,
+25 -2
View File
@@ -141,6 +141,22 @@ export const Schema = {
required: ["action", "token"],
type: "object",
},
BuiltIn: {
additionalProperties: false,
defaultProperties: [],
description: "A source for built-in decks..",
properties: {
id: {
type: "string",
},
source: {
enum: ["BuiltIn"],
type: "string",
},
},
required: ["id", "source"],
type: "object",
},
Cardcast: {
additionalProperties: false,
defaultProperties: [],
@@ -295,8 +311,15 @@ export const Schema = {
type: "object",
},
External: {
$ref: "#/definitions/Cardcast",
description: "A source for Cardcast.",
anyOf: [
{
$ref: "#/definitions/BuiltIn",
},
{
$ref: "#/definitions/Cardcast",
},
],
description: "An external source for a card or deck.",
},
FailReason: {
description: "The reason a deck could not be loaded.",
+21 -18
View File
@@ -40,13 +40,16 @@ export abstract class Cache {
*/
public abstract readonly config: ServerConfig.Cache;
private async get<Always extends Tagged, Sometimes extends Tagged, Result>(
source: Source.Resolver,
getCachedAlways: (
source: Source.Resolver
) => Promise<Aged<Always> | undefined>,
cacheAlways: (source: Source.Resolver, value: Always) => void,
cacheSometimes: (source: Source.Resolver, value: Sometimes) => void,
private async get<
Always extends Tagged,
Sometimes extends Tagged,
Resolver extends Source.Resolver<Source.External>,
Result
>(
source: Resolver,
getCachedAlways: (source: Resolver) => Promise<Aged<Always> | undefined>,
cacheAlways: (source: Resolver, value: Always) => void,
cacheSometimes: (source: Resolver, value: Sometimes) => void,
extract: (result: Result) => [Always, Sometimes | undefined],
miss: () => Promise<Result>
): Promise<Always> {
@@ -64,7 +67,7 @@ export abstract class Cache {
}
private async cacheExpired(
source: Source.Resolver,
source: Source.Resolver<Source.External>,
cached: Aged<Tagged>
): Promise<boolean> {
if (
@@ -85,7 +88,7 @@ export abstract class Cache {
* to do so.
*/
public async getSummary(
source: Source.Resolver,
source: Source.Resolver<Source.External>,
miss: () => Promise<Source.AtLeastSummary>
): Promise<Source.Summary> {
return this.get(
@@ -93,7 +96,7 @@ export abstract class Cache {
this.getCachedSummary.bind(this),
this.cacheSummaryBackground.bind(this),
this.cacheTemplatesBackground.bind(this),
result => [result.summary, result.templates],
(result) => [result.summary, result.templates],
miss
);
}
@@ -107,7 +110,7 @@ export abstract class Cache {
* to do so.
*/
public async getTemplates(
source: Source.Resolver,
source: Source.Resolver<Source.External>,
miss: () => Promise<Source.AtLeastTemplates>
): Promise<Decks.Templates> {
return this.get(
@@ -115,7 +118,7 @@ export abstract class Cache {
this.getCachedTemplates.bind(this),
this.cacheTemplatesBackground.bind(this),
this.cacheSummaryBackground.bind(this),
result => [result.templates, result.summary],
(result) => [result.templates, result.summary],
miss
);
}
@@ -124,19 +127,19 @@ export abstract class Cache {
* Get the given summary from the cache.
*/
public abstract async getCachedSummary(
source: Source.Resolver
source: Source.Resolver<Source.External>
): Promise<Aged<Source.Summary> | undefined>;
/**
* Store the given summary in the cache.
*/
public abstract async cacheSummary(
source: Source.Resolver,
source: Source.Resolver<Source.External>,
summary: Source.Summary
): Promise<void>;
public cacheSummaryBackground(
source: Source.Resolver,
source: Source.Resolver<Source.External>,
summary: Source.Summary
): void {
this.cacheSummary(source, summary).catch(Cache.logError);
@@ -146,19 +149,19 @@ export abstract class Cache {
* Get the given deck templates from the cache.
*/
public abstract async getCachedTemplates(
source: Source.Resolver
source: Source.Resolver<Source.External>
): Promise<Aged<Decks.Templates> | undefined>;
/**
* Store the given deck templates in the cache.
*/
public abstract async cacheTemplates(
source: Source.Resolver,
source: Source.Resolver<Source.External>,
templates: Decks.Templates
): Promise<void>;
public cacheTemplatesBackground(
source: Source.Resolver,
source: Source.Resolver<Source.External>,
templates: Decks.Templates
): void {
this.cacheTemplates(source, templates).catch(Cache.logError);
+10 -8
View File
@@ -18,42 +18,44 @@ export class InMemoryCache extends Cache.Cache {
this.config = config;
this.cache = {
summaries: new Map(),
templates: new Map()
templates: new Map(),
};
}
private static key(source: Source.Resolver): [string, string] {
private static key(
source: Source.Resolver<Source.External>
): [string, string] {
return [source.id(), source.deckId()];
}
public async cacheSummary(
source: Source.Resolver,
source: Source.Resolver<Source.External>,
summary: Source.Summary
): Promise<void> {
this.cache.summaries.set(InMemoryCache.key(source), {
cached: summary,
age: Date.now()
age: Date.now(),
});
}
public async cacheTemplates(
source: Source.Resolver,
source: Source.Resolver<Source.External>,
templates: Decks.Templates
): Promise<void> {
this.cache.templates.set(InMemoryCache.key(source), {
cached: templates,
age: Date.now()
age: Date.now(),
});
}
public async getCachedSummary(
source: Source.Resolver
source: Source.Resolver<Source.External>
): Promise<Cache.Aged<Source.Summary> | undefined> {
return this.cache.summaries.get(InMemoryCache.key(source));
}
public async getCachedTemplates(
source: Source.Resolver
source: Source.Resolver<Source.External>
): Promise<Cache.Aged<Decks.Templates> | undefined> {
return this.cache.templates.get(InMemoryCache.key(source));
}
+4 -4
View File
@@ -97,7 +97,7 @@ export class PostgresCache extends Cache.Cache {
}
public async cacheSummary(
source: Source.Resolver,
source: Source.Resolver<Source.External>,
summary: Source.Summary
): Promise<void> {
await this.pg.inTransaction(async (client) => {
@@ -133,7 +133,7 @@ export class PostgresCache extends Cache.Cache {
}
public async cacheTemplates(
source: Source.Resolver,
source: Source.Resolver<Source.External>,
templates: Decks.Templates
): Promise<void> {
await this.pg.inTransaction(async (client) => {
@@ -174,7 +174,7 @@ export class PostgresCache extends Cache.Cache {
}
public async getCachedSummary(
source: Source.Resolver
source: Source.Resolver<Source.External>
): Promise<Cache.Aged<Source.Summary> | undefined> {
return await this.pg.withClient(async (client) => {
const result = await client.query(
@@ -204,7 +204,7 @@ export class PostgresCache extends Cache.Cache {
}
public async getCachedTemplates(
source: Source.Resolver
source: Source.Resolver<Source.External>
): Promise<Cache.Aged<Decks.Templates> | undefined> {
return await this.pg.withClient(async (client) => {
const about = await client.query(
+36 -5
View File
@@ -10,7 +10,7 @@ const environmental: (keyof EnvironmentalConfig)[] = [
"listenOn",
"basePath",
"version",
"touchOnStart"
"touchOnStart",
];
export interface EnvironmentalConfig {
@@ -22,6 +22,7 @@ export interface EnvironmentalConfig {
}
export interface Config<D extends Duration> extends EnvironmentalConfig {
sources: BaseSources<D>;
timeouts: Timeouts<D>;
tasks: Tasks<D>;
storage: BaseStorage<D>;
@@ -42,6 +43,23 @@ type Tasks<D extends Duration> = {
processTickFrequency: D;
};
export interface BuiltIn {
basePath: string;
decks: string[];
}
interface BaseCardcast<D extends Duration> {
timeout: D;
simultaneousConnections: number;
}
export type Cardcast = BaseCardcast<ParsedDuration>;
interface BaseSources<D extends Duration> {
builtIn?: BuiltIn;
cardcast?: BaseCardcast<D>;
}
export type Sources = BaseSources<ParsedDuration>;
type BaseStorage<D extends Duration> = BaseInMemory<D> | BasePostgreSQL<D>;
export type Storage = BaseStorage<ParsedDuration>;
@@ -100,14 +118,14 @@ export const parseStorage = (
): BaseStorage<ParsedDuration> => ({
...storage,
abandonedTime: parseDuration(storage.abandonedTime),
garbageCollectionFrequency: parseDuration(storage.garbageCollectionFrequency)
garbageCollectionFrequency: parseDuration(storage.garbageCollectionFrequency),
});
export const parseCache = (
cache: BaseCache<UnparsedDuration>
): BaseCache<ParsedDuration> => ({
...cache,
checkAfter: parseDuration(cache.checkAfter)
checkAfter: parseDuration(cache.checkAfter),
});
export const parseTimeouts = (
@@ -121,7 +139,19 @@ export const parseTasks = (
tasks: Tasks<UnparsedDuration>
): Tasks<ParsedDuration> => ({
...tasks,
processTickFrequency: parseDuration(tasks.processTickFrequency)
processTickFrequency: parseDuration(tasks.processTickFrequency),
});
const parseCardcast = (cardcast: BaseCardcast<UnparsedDuration>): Cardcast => ({
...cardcast,
timeout: parseDuration(cardcast.timeout),
});
const parseSources = (sources: BaseSources<UnparsedDuration>): Sources => ({
...(sources.builtIn !== undefined ? { builtIn: sources.builtIn } : {}),
...(sources.cardcast !== undefined
? { cardcast: parseCardcast(sources.cardcast) }
: {}),
});
export const pullFromEnvironment = (config: Parsed): Parsed => {
@@ -141,8 +171,9 @@ export const pullFromEnvironment = (config: Parsed): Parsed => {
export const parse = (config: Unparsed): Parsed =>
pullFromEnvironment({
...config,
sources: parseSources(config.sources),
timeouts: parseTimeouts(config.timeouts),
tasks: parseTasks(config.tasks),
storage: parseStorage(config.storage),
cache: parseCache(config.cache)
cache: parseCache(config.cache),
});
+49 -6
View File
@@ -6,6 +6,7 @@ import * as Errors from "../errors";
import * as Round from "../games/game/round";
import * as Player from "../games/player";
import * as User from "../user";
import * as Source from "../games/cards/source";
abstract class ActionExecutionError extends Errors.MassiveDecksError<
Errors.Details
@@ -31,7 +32,7 @@ export class GameNotStartedError extends ActionExecutionError {
}
public details = (): Errors.Details => ({
error: "GameNotStarted"
error: "GameNotStarted",
});
}
@@ -49,7 +50,7 @@ export class UnprivilegedError extends ActionExecutionError {
}
public details = (): Errors.Details => ({
error: "Unprivileged"
error: "Unprivileged",
});
}
@@ -81,7 +82,7 @@ export class IncorrectPlayerRoleError extends ActionExecutionError {
public details = (): IncorrectPlayerRoleDetails => ({
error: "IncorrectPlayerRole",
role: this.role,
expected: this.expected
expected: this.expected,
});
}
@@ -109,7 +110,7 @@ export class IncorrectUserRoleError extends ActionExecutionError {
public details = (): IncorrectUserRoleDetails => ({
error: "IncorrectUserRole",
role: this.role,
expected: this.expected
expected: this.expected,
});
}
@@ -141,7 +142,7 @@ export class IncorrectRoundStageError extends ActionExecutionError {
public details = (): IncorrectRoundStageDetails => ({
error: "IncorrectRoundStage",
stage: this.stage,
expected: this.expected
expected: this.expected,
});
}
@@ -170,6 +171,48 @@ export class ConfigEditConflictError extends ActionExecutionError {
public details = (): ConfigEditConflictDetails => ({
error: "ConfigEditConflict",
version: this.version,
expected: this.expected
expected: this.expected,
});
}
interface SourceErrorDetails extends Errors.Details {
source: Source.External;
}
// Happens if the user asks for a deck that doesn't exist.
export class SourceNotFoundError extends ActionExecutionError {
public readonly source: Source.External;
public constructor(source: Source.External) {
super(
`The given deck (${source}) was not found at the source.`,
(undefined as unknown) as Action
);
this.source = source;
Error.captureStackTrace(this, SourceNotFoundError);
}
public details = (): SourceErrorDetails => ({
error: "SourceServiceError",
source: this.source,
});
}
// Happens if the deck service is down.
export class SourceServiceError extends ActionExecutionError {
public readonly source: Source.External;
public constructor(source: Source.External) {
super(
`The given deck source (${source.source}) was not available.`,
(undefined as unknown) as Action
);
this.source = source;
Error.captureStackTrace(this, SourceServiceError);
}
public details = (): SourceErrorDetails => ({
error: "SourceServiceError",
source: this.source,
});
}
+14 -2
View File
@@ -50,18 +50,30 @@ export interface BaseCard {
source: Source;
}
export type Style = "Em" | "Strong";
/** An empty slot for responses to be played into.*/
export interface Slot {
/**
* Defines a transformation over the content the slot is filled with.
*/
transform?: "UpperCase" | "Capitalize";
style?: Style;
}
export const isSlot = (part: Part): part is Slot => typeof part !== "string";
export interface Styled {
text: string;
style?: Style;
}
export const isSlot = (part: Part): part is Slot =>
typeof part !== "string" && !part.hasOwnProperty("text");
export const isStyled = (part: Part): part is Styled =>
typeof part !== "string" && part.hasOwnProperty("text");
/** Either text or a slot.*/
export type Part = string | Slot;
export type Part = string | Styled | Slot;
/**
* Create a new user id.
+28 -18
View File
@@ -2,6 +2,7 @@ import * as Cache from "../../cache";
import * as Decks from "./decks";
import { Cardcast } from "./sources/cardcast";
import { Custom } from "./sources/custom";
import { BuiltIn } from "./sources/builtIn";
/**
* A source for a card or deck.
@@ -11,7 +12,7 @@ export type Source = External | Custom;
/**
* An external source for a card or deck.
*/
export type External = Cardcast;
export type External = BuiltIn | Cardcast;
/**
* More information that can be looked up given a source.
@@ -54,14 +55,26 @@ export interface AtLeastTemplates {
summary?: Summary;
}
/**
* A resolver that only allows access to properties that don't require store
* access.
*/
export interface LimitedResolver<S extends External> {
id: () => string;
deckId: () => string;
loadingDetails: () => Details;
equals: (source: External) => boolean;
}
/**
* Resolve information about the given source.
*/
export abstract class Resolver implements LimitedResolver {
export abstract class Resolver<S extends External>
implements LimitedResolver<S> {
/**
* The source in question.
*/
public abstract source: Source;
public abstract source: S;
/**
* A unique id for the source as a whole.
@@ -133,31 +146,20 @@ export abstract class Resolver implements LimitedResolver {
}>;
}
/**
* A resolver that only allows access to properties that don't require store
* access.
*/
export interface LimitedResolver {
id: () => string;
deckId: () => string;
loadingDetails: () => Details;
equals: (source: External) => boolean;
}
/**
* A resolver that caches expensive responses in the store.
*/
export class CachedResolver extends Resolver {
private readonly resolver: Resolver;
export class CachedResolver<S extends External> extends Resolver<S> {
private readonly resolver: Resolver<S>;
private readonly cache: Cache.Cache;
public constructor(cache: Cache.Cache, resolver: Resolver) {
public constructor(cache: Cache.Cache, resolver: Resolver<S>) {
super();
this.cache = cache;
this.resolver = resolver;
}
public get source(): Source {
public get source(): S {
return this.resolver.source;
}
@@ -225,3 +227,11 @@ export class CachedResolver extends Resolver {
}
}
}
/**
* Get resolvers for the given source type.
*/
export interface MetaResolver<S extends External> {
limitedResolver(source: S): LimitedResolver<S>;
resolver(source: S): Resolver<S>;
}
+113 -44
View File
@@ -3,55 +3,124 @@ import * as Util from "../../util";
import * as Source from "./source";
import * as Cardcast from "./sources/cardcast";
import * as Player from "./sources/custom";
import * as Config from "../../config";
import * as BuiltIn from "./sources/builtIn";
import { SourceNotFoundError } from "../../errors/action-execution-error";
function uncachedResolver(source: Source.External): Source.Resolver {
switch (source.source) {
case "Cardcast":
return new Cardcast.Resolver(source);
default:
Util.assertNever(source.source);
async function loadIfEnabled<Config, MetaResolver>(
config: Config | undefined,
load: (value: Config) => Promise<MetaResolver>
): Promise<MetaResolver | undefined> {
if (config === undefined) {
return undefined;
} else {
return await load(config);
}
}
export class SourceNotFoundError extends Error {
public constructor() {
super("The given deck was not found at the source.");
}
}
export class SourceServiceError extends Error {
public constructor() {
super("The given source was not available.");
}
export interface ClientInfo {
builtIn?: BuiltIn.ClientInfo;
cardcast?: boolean;
}
/**
* Get the limited resolver for the given source.
*/
export const limitedResolver = (
source: Source.External
): Source.LimitedResolver => uncachedResolver(source);
export class Sources {
public readonly builtIn?: BuiltIn.MetaResolver;
public readonly cardcast?: Cardcast.MetaResolver;
/**
* Get the resolver for the given source.
*/
export const resolver = (
cache: Cache,
source: Source.External
): Source.Resolver =>
new Source.CachedResolver(cache, uncachedResolver(source));
/**
* Get the details for the given source.
*/
export const details = async (
cache: Cache,
source: Source.Source
): Promise<Source.Details> => {
switch (source.source) {
case "Custom":
return Player.details(source);
default:
return await resolver(cache, source).details();
public constructor(
builtIn?: BuiltIn.MetaResolver,
cardcast?: Cardcast.MetaResolver
) {
if (builtIn === undefined && cardcast === undefined) {
throw new Error("At least one source must be enabled.");
}
this.builtIn = builtIn;
this.cardcast = cardcast;
}
};
public clientInfo(): ClientInfo {
return {
...(this.builtIn !== undefined
? {
builtIn: this.builtIn.clientInfo(),
}
: {}),
...(this.cardcast !== undefined ? { cardcast: true } : {}),
};
}
private metaResolverIfConfigured(
source: Source.External
): Source.MetaResolver<Source.External> | undefined {
switch (source.source) {
case "BuiltIn":
return this.builtIn;
case "Cardcast":
return this.cardcast;
default:
Util.assertNever(source);
}
}
private metaResolver(
source: Source.External
): Source.MetaResolver<Source.External> {
const metaResolver = this.metaResolverIfConfigured(source);
if (metaResolver === undefined) {
throw new SourceNotFoundError(source);
} else {
return metaResolver;
}
}
/**
* Get the limited resolver for the given source.
*/
public limitedResolver(
source: Source.External
): Source.Resolver<Source.External> {
return this.metaResolver(source).resolver(source);
}
/**
* Get the resolver for the given source.
*/
public resolver(
cache: Cache,
source: Source.External
): Source.Resolver<Source.External> {
return new Source.CachedResolver(
cache,
this.metaResolver(source).resolver(source)
);
}
/**
* Get the details for the given source.
*/
details = async (
cache: Cache,
source: Source.Source
): Promise<Source.Details> => {
switch (source.source) {
case "Custom":
return Player.details(source);
default:
return await this.resolver(cache, source).details();
}
};
public static async from(config: Config.Sources): Promise<Sources> {
const [builtInMeta, cardcastMeta] = await Promise.all<
BuiltIn.MetaResolver | undefined,
Cardcast.MetaResolver | undefined
>([
loadIfEnabled(config.builtIn, BuiltIn.load),
loadIfEnabled(config.cardcast, Cardcast.load),
]);
return new Sources(builtInMeta, cardcastMeta);
}
}
@@ -0,0 +1,177 @@
import * as Source from "../source";
import * as Decks from "../decks";
import JSON5 from "json5";
import { promises as fs } from "fs";
import * as Config from "../../../config";
import * as path from "path";
import { Part } from "../card";
import * as Card from "../card";
import {
SourceNotFoundError,
SourceServiceError,
} from "../../../errors/action-execution-error";
const extension = ".deck.json5";
interface BuiltInDeck {
name: string;
calls: Part[][][];
responses: string[];
}
/**
* A source for built-in decks..
*/
export interface BuiltIn {
source: "BuiltIn";
id: string;
}
export interface ClientInfo {
decks: { name: string; id: string }[];
}
export class Resolver extends Source.Resolver<BuiltIn> {
public readonly source: BuiltIn;
private config: Config.BuiltIn;
/**
* Can be undefined because we want to error out later if the deck doesn't exist for nicer errors.
*/
private readonly storedSummary?: Source.Summary;
public constructor(
config: Config.BuiltIn,
source: BuiltIn,
summary?: Source.Summary
) {
super();
this.config = config;
this.source = source;
this.storedSummary = summary;
}
public id(): string {
return "BuiltIn";
}
public deckId(): string {
return this.source.id;
}
public loadingDetails(): Source.Details {
if (this.storedSummary !== undefined) {
return this.storedSummary.details;
}
return { name: "Deck Not Found" };
}
public equals(source: Source.External): boolean {
return source.source === "BuiltIn" && this.source.id == source.id;
}
public async getTag(): Promise<string | undefined> {
return undefined;
}
public async atLeastSummary(): Promise<Source.AtLeastSummary> {
if (this.storedSummary === undefined) {
throw new SourceNotFoundError(this.source);
}
return {
summary: this.storedSummary,
};
}
public async atLeastTemplates(): Promise<Source.AtLeastTemplates> {
return this.summaryAndTemplates();
}
public summaryAndTemplates = async (): Promise<{
summary: Source.Summary;
templates: Decks.Templates;
}> => {
if (this.storedSummary === undefined) {
throw new SourceNotFoundError(this.source);
}
try {
const rawDeck = JSON5.parse(
(
await fs.readFile(
path.join(this.config.basePath, this.source.id + extension)
)
).toString()
) as BuiltInDeck;
return {
summary: this.storedSummary,
templates: {
calls: new Set(rawDeck.calls.map(this.call)),
responses: new Set(rawDeck.responses.map(this.response)),
},
};
} catch (error) {
throw new SourceServiceError(this.source);
}
};
private call = (call: Card.Part[][]): Card.Call => ({
id: Card.id(),
parts: call,
source: this.source,
});
private response = (response: string): Card.Response => ({
id: Card.id(),
text: response,
source: this.source,
});
}
export class MetaResolver implements Source.MetaResolver<BuiltIn> {
private readonly config: Config.BuiltIn;
private readonly summaries: Map<string, Source.Summary>;
public constructor(
config: Config.BuiltIn,
summaries: Map<string, Source.Summary>
) {
this.config = config;
this.summaries = summaries;
}
limitedResolver(source: BuiltIn): Source.LimitedResolver<BuiltIn> {
return this.resolver(source);
}
resolver(source: BuiltIn): Resolver {
const summary = this.summaries.get(source.id);
return new Resolver(this.config, source, summary);
}
public clientInfo(): ClientInfo {
return {
decks: this.config.decks.map((id) => ({
name: (this.summaries.get(id) as Source.Summary).details.name,
id,
})),
};
}
}
export async function load(config: Config.BuiltIn): Promise<MetaResolver> {
const summaries = new Map<string, Source.Summary>();
for (const id of config.decks) {
const rawDeck = JSON5.parse(
(await fs.readFile(path.join(config.basePath, id + extension))).toString()
) as BuiltInDeck;
summaries.set(id, {
details: {
name: rawDeck.name,
},
calls: rawDeck.calls.length,
responses: rawDeck.responses.length,
});
}
return new MetaResolver(config, summaries);
}
+68 -45
View File
@@ -1,11 +1,15 @@
import http, { AxiosRequestConfig } from "axios";
import http, { AxiosInstance, AxiosRequestConfig } from "axios";
import genericPool from "generic-pool";
import HttpStatus from "http-status-codes";
import * as Card from "../card";
import { Slot } from "../card";
import * as Decks from "../decks";
import * as Source from "../source";
import { SourceNotFoundError, SourceServiceError } from "../sources";
import * as Config from "../../../config";
import {
SourceNotFoundError,
SourceServiceError,
} from "../../../errors/action-execution-error";
interface CCSummary {
name: string;
@@ -38,27 +42,6 @@ interface CCCard {
nsfw: boolean;
}
const config: AxiosRequestConfig = {
method: "GET",
baseURL: "https://api.cardcastgame.com/v1/",
timeout: 10000,
responseType: "json"
};
/**
* We pool requests to cardcast to stop us hitting them too hard (on top of
* caching). We only allow two simultaneous requests.
*/
const connectionPool = genericPool.createPool(
{
create: async () => http.create(config),
destroy: async _ => {
// Do nothing.
}
},
{ max: 2 }
);
const summaryUrl = (playCode: PlayCode): string => `decks/${playCode}`;
const deckUrl = (playCode: PlayCode): string => `${summaryUrl(playCode)}/cards`;
const humanViewUrl = (playCode: PlayCode): string =>
@@ -88,7 +71,7 @@ const nextWordShouldBeCapitalized = (previously: string): boolean =>
*/
// TODO: We probably want to offer some control over these heuristics.
function* parts(call: CCCard): Iterable<Card.Part> {
const upper: Slot = call.text.every(text => text === text.toUpperCase())
const upper: Slot = call.text.every((text) => text === text.toUpperCase())
? { transform: "UpperCase" }
: {};
let first = true;
@@ -114,21 +97,26 @@ function* parts(call: CCCard): Iterable<Card.Part> {
const call = (source: Cardcast, call: CCCard): Card.Call => ({
id: Card.id(),
parts: [Array.from(parts(call))],
source: source
source: source,
});
const response = (source: Cardcast, response: CCCard): Card.Response => ({
id: Card.id(),
text: response.text[0],
source: source
source: source,
});
export class Resolver extends Source.Resolver {
export class Resolver extends Source.Resolver<Cardcast> {
public readonly source: Cardcast;
private readonly connectionPool: genericPool.Pool<AxiosInstance>;
public constructor(source: Cardcast) {
public constructor(
source: Cardcast,
connectionPool: genericPool.Pool<AxiosInstance>
) {
super();
this.source = source;
this.connectionPool = connectionPool;
}
public id(): string {
@@ -142,7 +130,7 @@ export class Resolver extends Source.Resolver {
public loadingDetails(): Source.Details {
return {
name: `Cardcast ${this.source.playCode}`,
url: humanViewUrl(this.source.playCode)
url: humanViewUrl(this.source.playCode),
};
}
@@ -158,49 +146,47 @@ export class Resolver extends Source.Resolver {
}
public async atLeastSummary(): Promise<Source.AtLeastSummary> {
const summary = await Resolver.get<CCSummary>(
summaryUrl(this.source.playCode)
);
const summary = await this.get<CCSummary>(summaryUrl(this.source.playCode));
return {
summary: {
details: {
name: summary.name,
url: humanViewUrl(this.source.playCode)
url: humanViewUrl(this.source.playCode),
},
calls: Number.parseInt(summary.call_count, 10),
responses: Number.parseInt(summary.response_count, 10),
tag: summary.updated_at
}
tag: summary.updated_at,
},
};
}
public async atLeastTemplates(): Promise<Source.AtLeastTemplates> {
const deck = await Resolver.get<CCDeck>(deckUrl(this.source.playCode));
const deck = await this.get<CCDeck>(deckUrl(this.source.playCode));
return {
templates: {
calls: new Set(deck.calls.map(c => call(this.source, c))),
responses: new Set(deck.responses.map(r => response(this.source, r)))
}
calls: new Set(deck.calls.map((c) => call(this.source, c))),
responses: new Set(deck.responses.map((r) => response(this.source, r))),
},
};
}
private static async get<T>(url: string): Promise<T> {
const connection = await connectionPool.acquire();
private async get<T>(url: string): Promise<T> {
const connection = await this.connectionPool.acquire();
try {
return (await connection.get(url)).data;
} catch (error) {
if (error.response) {
const response = error.response;
if (response.status === HttpStatus.NOT_FOUND) {
throw new SourceNotFoundError();
throw new SourceNotFoundError(this.source);
} else {
throw new SourceServiceError();
throw new SourceServiceError(this.source);
}
} else {
throw error;
}
} finally {
await connectionPool.release(connection);
await this.connectionPool.release(connection);
}
}
@@ -209,6 +195,43 @@ export class Resolver extends Source.Resolver {
templates: Decks.Templates;
}> => ({
summary: await this.summary(),
templates: await this.templates()
templates: await this.templates(),
});
}
export class MetaResolver implements Source.MetaResolver<Cardcast> {
/**
* We pool requests to cardcast to stop us hitting them too hard (on top of caching).
*/
private readonly connectionPool: genericPool.Pool<AxiosInstance>;
public constructor(config: Config.Cardcast) {
const httpConfig: AxiosRequestConfig = {
method: "GET",
baseURL: "https://api.cardcastgame.com/v1/",
timeout: config.timeout,
responseType: "json",
};
this.connectionPool = genericPool.createPool(
{
create: async () => http.create(httpConfig),
destroy: async (_) => {
// Do nothing.
},
},
{ max: config.simultaneousConnections }
);
}
limitedResolver(source: Cardcast): Resolver {
return this.resolver(source);
}
resolver(source: Cardcast): Resolver {
return new Resolver(source, this.connectionPool);
}
}
export const load = async (config: Config.Cardcast): Promise<MetaResolver> =>
new MetaResolver(config);
+24 -19
View File
@@ -3,12 +3,11 @@ import express, { NextFunction, Request, Response } from "express";
import "express-async-errors";
import expressWinston from "express-winston";
import ws from "express-ws";
import fs from "fs";
import { promises as fs } from "fs";
import helmet from "helmet";
import HttpStatus from "http-status-codes";
import JSON5 from "json5";
import sourceMapSupport from "source-map-support";
import { promisify } from "util";
import wu from "wu";
import * as CheckAlive from "./action/initial/check-alive";
import * as CreateLobby from "./action/initial/create-lobby";
@@ -31,11 +30,11 @@ import * as Token from "./user/token";
sourceMapSupport.install();
process.on("uncaughtException", function(error) {
process.on("uncaughtException", function (error) {
Logging.logException("Uncaught exception: ", error);
});
process.on("unhandledRejection", function(reason, promise) {
process.on("unhandledRejection", function (reason, promise) {
if (reason instanceof Error) {
Logging.logException(`Unhandled rejection for ${promise}.`, reason);
} else {
@@ -51,7 +50,7 @@ function getConfigFilePath(): string {
async function main(): Promise<void> {
const config = ServerConfig.parse(
JSON5.parse(
(await promisify(fs.readFile)(getConfigFilePath())).toString()
(await fs.readFile(getConfigFilePath())).toString()
) as ServerConfig.Unparsed
);
@@ -75,7 +74,7 @@ async function main(): Promise<void> {
app.use(
expressWinston.logger({
winstonInstance: Logging.logger
winstonInstance: Logging.logger,
})
);
@@ -123,12 +122,12 @@ async function main(): Promise<void> {
const registration = RegisterUser.validate(req.body);
const newUser = User.create(registration);
const id = await Change.applyAndReturn(state, gameCode, lobby => {
const id = await Change.applyAndReturn(state, gameCode, (lobby) => {
if (lobby.config.password !== registration.password) {
throw new InvalidLobbyPasswordError();
}
if (
wu(Object.values(lobby.users)).find(u => u.name === registration.name)
wu(Object.values(lobby.users)).find((u) => u.name === registration.name)
) {
throw new UsernameAlreadyInUseError(registration.name);
}
@@ -155,22 +154,22 @@ async function main(): Promise<void> {
lobby,
events: [
Event.targetAll(PresenceChanged.joined(id, newUser)),
...(unpause.events !== undefined ? unpause.events : [])
...(unpause.events !== undefined ? unpause.events : []),
],
timeouts: [
{
timeout: UserDisconnect.of(id),
after: config.timeouts.disconnectionGracePeriod
after: config.timeouts.disconnectionGracePeriod,
},
...(unpause.timeouts !== undefined ? unpause.timeouts : [])
]
...(unpause.timeouts !== undefined ? unpause.timeouts : []),
],
},
returnValue: id
returnValue: id,
};
});
const claims: Token.Claims = {
gc: gameCode,
uid: id
uid: id,
};
res.json(Token.create(claims, await state.store.id(), config.secret));
});
@@ -180,6 +179,10 @@ async function main(): Promise<void> {
state.socketManager.add(state, gameCode, socket);
});
app.get("/api/sources", async (req, res) => {
res.json(state.sources.clientInfo());
});
app.use((error: Error, req: Request, res: Response, next: NextFunction) => {
if (res.headersSent) {
next(error);
@@ -195,7 +198,7 @@ async function main(): Promise<void> {
app.use(
expressWinston.errorLogger({
winstonInstance: Logging.logger,
msg: "{{err.message}}"
msg: "{{err.message}}",
})
);
@@ -237,18 +240,20 @@ async function main(): Promise<void> {
state.tasks
.loadFromStore(state)
.catch(error => Logging.logException("Error running store tasks:", error));
.catch((error) =>
Logging.logException("Error running store tasks:", error)
);
app.listen(config.listenOn, async () => {
Logging.logger.info(`Listening on ${config.listenOn}.`);
if (config.touchOnStart !== null) {
const f = await promisify(fs.open)(config.touchOnStart, "w");
await promisify(fs.close)(f);
const f = await fs.open(config.touchOnStart, "w");
await f.close();
}
});
}
main().catch(error => {
main().catch((error) => {
Logging.logException("Application exception:", error);
process.exit(1);
});
+5 -1
View File
@@ -5,9 +5,11 @@ import { SocketManager } from "./socket-manager";
import { Store } from "./store";
import * as Stores from "./store/stores";
import * as Tasks from "./task/tasks";
import { Sources } from "./games/cards/sources";
export interface ServerState {
config: Config.Parsed;
sources: Sources;
store: Store;
cache: Cache;
socketManager: SocketManager;
@@ -15,16 +17,18 @@ export interface ServerState {
}
export async function create(config: Config.Parsed): Promise<ServerState> {
const sources = await Sources.from(config.sources);
const store = await Stores.from(config.storage);
const cache = await caches.from(config.cache);
const socketManager = new SocketManager();
const tasks = new Tasks.Queue(config.tasks.rateLimit);
return {
sources,
config,
store,
cache,
socketManager,
tasks
tasks,
};
}
+39 -22
View File
@@ -2,17 +2,16 @@ import Rfc6902 from "rfc6902";
import * as Event from "../event";
import * as Configured from "../events/lobby-event/configured";
import * as Source from "../games/cards/source";
import * as Sources from "../games/cards/sources";
import {
SourceNotFoundError,
SourceServiceError
} from "../games/cards/sources";
import { Lobby } from "../lobby";
import { Change } from "../lobby/change";
import * as Config from "../lobby/config";
import { GameCode } from "../lobby/game-code";
import { ServerState } from "../server-state";
import * as Task from "../task";
import {
SourceNotFoundError,
SourceServiceError,
} from "../errors/action-execution-error";
export class LoadDeckSummary extends Task.TaskBase<Source.Summary> {
private readonly source: Source.External;
@@ -23,7 +22,8 @@ export class LoadDeckSummary extends Task.TaskBase<Source.Summary> {
}
protected async begin(server: ServerState): Promise<Source.Summary> {
const loaded = await Sources.resolver(server.cache, this.source)
const loaded = await server.sources
.resolver(server.cache, this.source)
// We are intentionally ensuring the templates get cached here in advance,
// but don't actually need to do anything with them at this point.
.summaryAndTemplates();
@@ -32,35 +32,48 @@ export class LoadDeckSummary extends Task.TaskBase<Source.Summary> {
private resolveInternal(
lobby: Lobby,
modify: (source: Config.ConfiguredSource) => void
modify: (source: Config.ConfiguredSource) => void,
server: ServerState
): Change {
const lobbyConfig = lobby.config;
const oldConfig = JSON.parse(JSON.stringify(Config.censor(lobby.config)));
const decks = lobbyConfig.decks;
const resolver = Sources.limitedResolver(this.source);
const target = decks.find(deck => resolver.equals(deck.source));
const resolver = server.sources.limitedResolver(this.source);
const target = decks.find((deck) => resolver.equals(deck.source));
if (target !== undefined) {
modify(target);
lobbyConfig.version += 1;
const patch = Rfc6902.createPatch(oldConfig, Config.censor(lobbyConfig));
return {
lobby,
events: [Event.targetAll(Configured.of(patch))]
events: [Event.targetAll(Configured.of(patch))],
};
} else {
return {};
}
}
protected resolve(lobby: Lobby, work: Source.Summary): Change {
return this.resolveInternal(lobby, summarised => {
if (!Config.isFailed(summarised)) {
summarised.summary = { ...work, tag: undefined };
}
});
protected resolve(
lobby: Lobby,
work: Source.Summary,
server: ServerState
): Change {
return this.resolveInternal(
lobby,
(summarised) => {
if (!Config.isFailed(summarised)) {
summarised.summary = { ...work, tag: undefined };
}
},
server
);
}
protected resolveError(lobby: Lobby, error: Error): Change {
protected resolveError(
lobby: Lobby,
error: Error,
server: ServerState
): Change {
let reason: Config.FailReason;
if (error instanceof SourceNotFoundError) {
reason = "NotFound";
@@ -69,11 +82,15 @@ export class LoadDeckSummary extends Task.TaskBase<Source.Summary> {
} else {
throw error;
}
return this.resolveInternal(lobby, failed => {
if (!failed.hasOwnProperty("summary")) {
(failed as Config.FailedSource).failure = reason;
}
});
return this.resolveInternal(
lobby,
(failed) => {
if (!failed.hasOwnProperty("summary")) {
(failed as Config.FailedSource).failure = reason;
}
},
server
);
}
public static *discover(
+3 -3
View File
@@ -19,8 +19,8 @@ export class StartGame extends Task.TaskBase<Decks.Templates[]> {
protected async begin(server: ServerState): Promise<Decks.Templates[]> {
return Promise.all(
wu(this.decks).map(deck =>
Sources.resolver(server.cache, deck).templates()
wu(this.decks).map((deck) =>
server.sources.resolver(server.cache, deck).templates()
)
);
}
@@ -42,7 +42,7 @@ export class StartGame extends Task.TaskBase<Decks.Templates[]> {
return {
lobby,
events: atStartOfRound.events,
timeouts: atStartOfRound.timeouts
timeouts: atStartOfRound.timeouts,
};
}