-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathUpdate.hs
422 lines (405 loc) · 17.5 KB
/
Update.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
module Imj.Game.Update
( updateAppState
, putClientState
, mkEmptyOccurencesHist
) where
import Imj.Prelude
import Prelude(length, putStrLn)
import qualified Prelude as Unsafe(last)
import Control.Concurrent(forkIO, threadDelay)
import Control.Exception.Base(throwIO)
import Control.Monad.Reader.Class(asks)
import Control.Monad.Reader(runReaderT)
import Control.Monad.State.Strict(gets, modify')
import Data.Attoparsec.Text(parseOnly)
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import Data.Text(pack, unpack, strip, uncons)
import qualified Data.Text as Text(length)
import System.Exit(exitSuccess)
import Imj.Control.Concurrent.AsyncGroups.Class
import Imj.Game.Audio.Class
import Imj.Game.Class
import Imj.Game.Modify
import Imj.Geo.Discrete.Types
import Imj.Graphics.Class.DiscreteDistance
import Imj.Graphics.Class.HasSizedFace
import Imj.Graphics.Class.Positionable
import Imj.Graphics.Class.Render
import Imj.Graphics.ParticleSystem.Design.Update
import Imj.Graphics.Screen
import Imj.Graphics.UI.Animation.Types
import Imj.Server.Class
import Imj.Server.Types
import Imj.Event
import Imj.Game.Command
import Imj.Game.Color
import Imj.Game.Exceptions
import Imj.Game.Level
import Imj.Game.Priorities
import Imj.Game.Status
import Imj.Game.Show
import Imj.Game.Timing
import Imj.Graphics.Interpolation.Evolution
import Imj.Graphics.RecordDraw
import Imj.Graphics.Render.FromMonadReader
import Imj.Graphics.Text.ColorString hiding(putStrLn)
import Imj.Graphics.Text.RasterizedString
import Imj.Graphics.UI.Animation
import Imj.Graphics.UI.Chat
import Imj.Graphics.UI.RectContainer
import Imj.Iteration
import Imj.Log
import Imj.Music.Instruments
import Imj.Server.Command
toggleRecordEvent :: AppState g -> AppState g
toggleRecordEvent s@(AppState _ _ _ _ _ r _ _ _ _) =
s { eventHistory = mkEmptyOccurencesHist
, appStateRecordEvents = case r of
Record -> DontRecord
DontRecord -> Record
}
mkEmptyOccurencesHist :: OccurencesHist
mkEmptyOccurencesHist = OccurencesHist [] mempty
{-# INLINABLE updateAppState #-}
updateAppState :: (g ~ GameLogicT e, s ~ ServerT g
, StateValueT s ~ GameStateValue
, ServerCmdParser s
, MonadState (AppState g) m
, MonadReader e m, Client e, Render e, HasSizedFace e, AsyncGroups e, Audio e
, MonadIO m)
=> UpdateEvent g
-- ^ The 'Event' that should be handled here.
-> m ()
updateAppState (Right evt) = updateAddStateFromCliEvt evt
where
updateAddStateFromCliEvt = \case
SequenceOfEvents l -> mapM_ updateAddStateFromCliEvt l
AppEvent e ->
onClientOnlyEvent e
ChatCmd chatCmd -> stateChat $ flip (,) () . runChat chatCmd
SendChatMessage -> onSendChatMessage
ToggleEventRecording ->
modify' toggleRecordEvent
Timeout (Deadline _ _ PollExternalEvents) ->
gets pollContext >>= maybe
(return ())
(\ctxt ->
getIGame >>= liftIO . (produceEvents produceEventsByPolling) ctxt >>= either
(error . (++) "external events polling:" . show)
(\(cliEvts, srvEvts, mayNextTime) ->
asks writeToClient' >>= \toClient -> asks sendToServer' >>= \toServer -> liftIO $ do
now <- getSystemTime
case srvEvts of
[] -> return ()
[one] -> toServer one
(_:_:_) -> toServer $ SequenceOfCliEvts srvEvts
case cliEvts of
[] -> return ()
[one] -> toClient $ FromClient one
(_:_:_) -> toClient $ FromClient $ SequenceOfEvents cliEvts
maybe
(return ())
(\dt ->
void $ forkIO $ do
-- if we don't sleep here, glfw keyboard events will be handled with a very long delay.
threadDelay $ max 1 $ fromIntegral $ toMicros dt
-- note that the duration written in the 'Deadline' is not taken into account:
-- if an element is in the queue, it is handled immediately.
toClient $ FromClient $ Timeout $ Deadline (addDuration dt now) externalEventPriority PollExternalEvents
)
mayNextTime
))
Timeout (Deadline t _ (RedrawStatus f g)) ->
updateStatus (Just (f,g)) t
Timeout (Deadline t _ AnimateUI) -> do
_anim <$> getGameState >>= \a@(UIAnimation evolutions (UIAnimProgress _ it)) -> do
let nextIt@(Iteration _ nextFrame) = nextIteration it
worldAnimDeadline = fmap (flip addDuration t) $ getDeltaTime evolutions nextFrame
putAnimation $ a { getProgress = UIAnimProgress worldAnimDeadline nextIt }
Timeout (Deadline _ _ (AnimateParticleSystem key)) ->
fmap systemTimePointToParticleSystemTimePoint (liftIO getSystemTime) >>= \tps ->
gets game >>= \g ->
putGame (g {
gameParticleSystems =
Map.updateWithKey
(\_ (Prioritized p ps) -> fmap (Prioritized p) $ updateParticleSystem tps ps)
key
$ gameParticleSystems g })
CanvasSizeChanged ->
onTargetSize
RenderingTargetChanged ->
withAnim $ do -- to make the frame take its initial size
onTargetChanged >>= either (liftIO . putStrLn) return
onTargetSize
CycleRenderingOptions i j -> do
cycleRenderingOptions i j >>= either (liftIO . putStrLn) return
onTargetSize
ApplyPPUDelta deltaPPU -> do
-- the font calculation is done according to the current screen size,
-- so there may be some partial unit rectangles. If we wanted to have only
-- full unit rectangles, we could recompute the screen size based on preferred size and new ppu,
-- then adjust the font.
asks applyPPUDelta >>= \f -> f deltaPPU >>= either (liftIO . putStrLn) return
onTargetSize
ApplyFontMarginDelta d ->
asks applyFontMarginDelta >>= \f -> f d >>= either (liftIO . putStrLn) return
Log Error txt ->
error $ unpack txt
Log msgLevel txt ->
stateChat $ addMessage $ Information msgLevel txt
updateAppState (Left evt) = updateAddStateFromServerEvt evt
where
updateAddStateFromServerEvt = \case
SequenceOfSrvEvts l -> mapM_ updateAddStateFromServerEvt l
ServerAppEvt e ->
onServerEvent e
AddInstrument iid i ->
modify' $ \s -> s {appInstruments = registerInstrument iid i $ appInstruments s}
PlayMusic music ->
asks playMusic >>= \f -> gets (idToInstr . appInstruments) >>= flip f music
OnContent worldParameters ->
putServerContent worldParameters
RunCommand i cmd -> runClientCommand i cmd
CommandError cmd err ->
stateChat $ addMessage $ Information Warning $
pack (show cmd) <> " failed:" <> err
Reporting cmd ->
stateChat $ addMessage $ Information Info $ pack $ chatShow cmd
Warn msg ->
stateChat $ addMessage $ Information Warning msg
PlayerInfo notif i ->
stateChat . addMessage . ChatMessage =<< toTxt i notif
EnterState s ->
putClientState $ ClientState Ongoing s
ExitState s ->
putClientState $ ClientState Over s
AllClients eplayers -> do
asks sendToServer' >>= \f -> f $ ExitedState Excluded
putClientState $ ClientState Over Excluded
let p = Map.map mkPlayer eplayers
putPlayers p
stateChat $ addMessage $ ChatMessage $ welcome p
ConnectionAccepted i instruments -> do
withAnim $ do -- to make the frame take its initial size
putGameConnection $ Right i
modify' $ \s -> s { appInstruments = mkInstruments instruments }
ConnectionRefused sn reason ->
putGameConnection $ Left $
"[" <>
pack (show sn) <>
"]" <>
pack " is invalid:" <>
reason
Disconnected (ClientShutdown (Right ())) ->
liftIO $ exitSuccess
Disconnected (ClientShutdown (Left txt)) ->
liftIO $ throwIO $ UnexpectedProgramEnd $ "Broken Client : " <> txt
Disconnected s@(ServerShutdown _) ->
liftIO $ throwIO $ UnexpectedProgramEnd $ "Disconnected by Server: " <> pack (show s)
ServerError txt ->
liftIO $ throwIO $ ErrorFromServer txt
toTxt i notif =
(`mappend` colored (pack $ toTxt'' notif) chatMsgColor) . getPlayerUIName' <$> getPlayer i
toTxt'' = \case
Joins -> " joins the game."
WaitsToJoin -> " is waiting to join the game."
StartsGame -> " starts the game."
Done cmd@(Put _) ->
" changed " ++ chatShow cmd
Done cmd ->
" " ++ chatShow cmd
{-# INLINABLE onTargetSize #-}
onTargetSize :: (GameLogic g
, MonadState (AppState g) m
, MonadReader e m, Canvas e
, MonadIO m)
=> m ()
onTargetSize = getTargetSize >>= maybe
(return ())
(\sz -> do
let screen@(Screen _ center) = mkScreen $ Just sz
putCurScreen screen
getGameState >>= \gs@(GameState mayG anim) -> do
let ul = maybe center (_upperLeft . getViewport To screen) mayG
newAnim = setPosition ul anim
putGameState (gs { _anim = newAnim }))
{-# INLINABLE putClientState #-}
putClientState :: (MonadState (AppState g) m
, MonadReader e m, HasSizedFace e
, MonadIO m)
=> ClientState GameStateValue
-> m ()
putClientState i = do
gets game >>= \g -> putGame $ g { getClientState = i}
liftIO getSystemTime >>= updateStatus Nothing
{-# INLINABLE updateStatus #-}
updateStatus :: (MonadState (AppState s) m
, MonadReader e m, HasSizedFace e
, MonadIO m)
=> Maybe (Frame, Int)
-- ^ When Nothing, the current frame should be used.
-> Time Point System
-> m ()
updateStatus mayFrame t = gets game >>= \(Game state (Screen _ ref) _ _ drawnState' _ _ _ _ _) -> do
let drawnState = zip [0 :: Int ..] drawnState'
newStrs <- zip [0 :: Int ..] <$> go state
-- return the same evolution when the string didn't change.
part1 <- forM
(zip newStrs drawnState)
(\((i,newStr),(_,(curStr,curLine@(AnimatedLine (Evolution (Successive s) _ _ _) _ _)))) ->
if newStr == curStr
then
return (curStr,curLine)
else do
let mayPrevRecord = case s of
[] -> Nothing
_:_ -> Just $ Unsafe.last s
evolutionStart <- flip fromMaybe mayPrevRecord <$> liftIO mkZeroRecordDraw
evolutionEnd <- recordFromStrs (move (2*i) Down ref) newStr
let ev = mkEvolutionEaseQuart (Successive [evolutionStart,evolutionEnd]) $ fromSecs 1
return (newStr, AnimatedLine ev 0 Nothing))
part2 <- forM
(drop (length drawnState) newStrs)
(\(i,newStr) -> do
evolutionStart <- liftIO mkZeroRecordDraw
evolutionEnd <- recordFromStrs (move (2*i) Down ref) newStr
let ev = mkEvolutionEaseQuart (Successive [evolutionStart,evolutionEnd]) $ fromSecs 1
return (newStr, AnimatedLine ev 0 Nothing))
part3 <- catMaybes <$> forM
(drop (length newStrs) drawnState)
(\(_,(oldStr,oldRec@(AnimatedLine (Evolution (Successive s) _ _ _) _ deadline))) ->
if oldStr == ""
then
return $ maybe Nothing (const $ Just (oldStr,oldRec)) deadline
else do
let mayPrevRecord = case s of
[] -> Nothing
_:_ -> Just $ Unsafe.last s
evolutionStart <- flip fromMaybe mayPrevRecord <$> liftIO mkZeroRecordDraw
evolutionEnd <- liftIO mkZeroRecordDraw
let ev = mkEvolutionEaseInQuart (Successive [evolutionStart,evolutionEnd]) $ fromSecs 0.5
return $ Just ("", AnimatedLine ev 0 Nothing))
putDrawnState $ part1 ++ part2 ++ part3
updateStatusDeadline
where
updateStatusDeadline :: MonadState (AppState s) m => m ()
updateStatusDeadline =
zip [0..] . getDrawnClientState <$> gets game >>= mapM
(\(i, (str, AnimatedLine recordEvolution curFrame _)) -> do
let frame = fromMaybe curFrame $ maybe
Nothing
(\(targetFrame,j) -> if i==j then Just targetFrame else Nothing)
mayFrame
minDt = fromSecs 0.015
significantDeadline f sofar = maybe
(succ f, sofar)
(\dt ->
let newDuration = dt |+| fromMaybe zeroDuration sofar
in if newDuration > minDt
then
(succ f, Just newDuration)
else
significantDeadline (succ f) $ Just newDuration)
$ getDeltaTimeToNextFrame recordEvolution f
(deadlineFrame, deadlineGap) = significantDeadline frame Nothing
deadline = fmap (\d -> Deadline (addDuration d t) redrawStatusPriority $ RedrawStatus deadlineFrame i) deadlineGap
return (str, AnimatedLine recordEvolution frame deadline))
>>= putDrawnState
recordFromStrs ref (ColorString [(txt,_)])
| Text.length txt == 1 =
let (c,_) = fromMaybe (error "logic") $ uncons txt
in informProgressively ref $ mkRasterizedString [c] grayGradient
recordFromStrs ref unique
| countChars unique < 3 =
informProgressively ref $ mkRasterizedStringFromColorString unique
| otherwise =
liftIO mkRecordDraw >>= \e -> do
flip runReaderT e $ drawAligned_ unique $ mkCentered ref
liftIO (finalizeRecord e)
informProgressively ref x = do
face <- asks getSizedFace
e <- liftIO mkRecordDraw
liftIO (x face) >>= flip runReaderT e . drawVerticallyCentered ref
liftIO $ finalizeRecord e
go = \case
ClientState Over x -> case x of
Excluded ->
inform "Joining..."
Included y -> case y of
Setup ->
inform "..."
(PlayLevel _) ->
inform "Please wait..."
ClientState Ongoing x -> case x of
Excluded ->
inform "A game is currently running on the server, please wait..."
Included y -> case y of
Setup ->
return []
PlayLevel (Countdown n Running) ->
inform $ pack $ show n
PlayLevel status ->
statusMsg status
statusMsg = \case
New -> return [color "Waiting for game start..."]
CancelledNoConnectedPlayer -> return [color "Game cancelled, all players left."]
Paused disconnectedPlayers x -> -- TODO we could draw the previous status too (stack of status)
intercalate ", " <$> showPlayerNames disconnectedPlayers >>= \them ->
flip (++) [color "Game paused, waiting for [" <> them <> color "] to reconnect..."] <$> statusMsg x
Running -> return []
WaitingForOthersToEndLevel _ ->
-- We silence it because in real life with good connections we won't wait more than typical network latency,
-- and we don't want to make an exception to the "display in full before transitionning" policy.
-- Note that if the connection of another player is broken, we are in the 'Paused' branch
-- NOTE (TODO) The server could test latencies of clients using pings, and take it into account.
-- It would be interesting to find a technique to prevent clients from intentionally being slow to reply
-- to such pings.
return []
-- Original unsilenced message:
--intercalate ", " <$> showPlayerNames stillPlaying >>= \them ->
--return [color "Waiting for [" <> them <> color "] to finish..."]
Countdown n x ->
flip (++) [colored ("(" <> pack (show n) <> ")") neutralMessageColorFg] <$> statusMsg x
OutcomeValidated o -> return $ map (flip colored' $ messageColor o) $ case o of
(Lost reason) -> ["You lose", "(" <> reason <> ")"]
Won -> ["You win!"]
WhenAllPressedAKey x (Just _) _ -> statusMsg x
WhenAllPressedAKey x Nothing havePressed ->
getMyId >>= maybe
(error "todo")
(\me -> flip (++) <$> maybe
(error "logic")
(\iHavePressed ->
if iHavePressed
then
intercalate ", " <$> showPlayerNames (Map.keysSet $ Map.filter (== False) havePressed) >>= \them ->
return [color "Waiting for [" <> them <> color "] to press a key..."]
else
return [colored "Press a key to continue..." neutralMessageColorFg])
(Map.lookup me havePressed)
<*> statusMsg x)
inform m = return [color m]
color = flip colored' (messageColor Won)
showPlayerNames = mapM showPlayerName . Set.toList
onSendChatMessage :: (GameLogicT e ~ g
, ServerCmdParser (ServerT g)
, MonadState (AppState g) m
, MonadReader e m, Client e
, MonadIO m)
=> m ()
onSendChatMessage =
fmap strip (stateChat takeMessage) >>= \msg -> do
f <- asks sendToServer'
either
(stateChat . addMessage . Information Warning . (<>) ("Error while parsing: " <> msg <> " : ") . pack)
(f . OnCommand)
$ parseOnly command msg