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:
+1
-1
@@ -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"
|
||||
|
||||
Generated
+3
-3
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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" ]
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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,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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user