1 -- This file is part of Intricacy
2 -- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
4 -- This program is free software: you can redistribute it and/or modify
5 -- it under the terms of version 3 of the GNU General Public License as
6 -- published by the Free Software Foundation, or any later version.
8 -- You should have received a copy of the GNU General Public License
9 -- along with this program. If not, see http://www.gnu.org/licenses/.
11 {-# LANGUAGE LambdaCase #-}
12 {-# LANGUAGE ScopedTypeVariables #-}
14 module Interact
(interactUI
) where
16 import Control
.Applicative
17 import Control
.Concurrent
18 import Control
.Concurrent
.STM
19 import Control
.Monad
.Catch
20 import Control
.Monad
.State
21 import Control
.Monad
.Trans
.Except
22 import Control
.Monad
.Trans
.Maybe
23 import Control
.Monad
.Writer
25 import qualified Data
.ByteString
.Char8
as CS
26 import qualified Data
.ByteString
.Lazy
as BL
28 import Data
.Function
(on
)
31 import qualified Data
.Map
as Map
33 import qualified Data
.Vector
as Vector
35 import System
.Directory
36 import System
.FilePath
38 import Crypto
.Hash
.Algorithms
(SHA256
(..))
39 import Crypto
.PubKey
.RSA
.OAEP
(defaultOAEPParams
, encrypt
)
40 import Crypto
.PubKey
.RSA
.Types
(PublicKey
)
63 newtype InteractSuccess
= InteractSuccess
Bool
65 interactUI
:: UIMonad uiM
=> MainStateT uiM InteractSuccess
66 interactUI
= (fromMaybe (InteractSuccess
False) <$>) . runMaybeT
$ do
67 gets initiationRequired
>>? lift doInitiation
68 gets initiationRequired
>>? mzero
72 when (im
== IMEdit
) setSelectedPosFromMouse
73 when (im
== IMMeta
) $ do
76 -- draw before testing auth, lest a timeout mean a blank screen
81 setMark
False startMark
85 initiationRequired s
= ms2im s
== IMMeta
&& not (initiated s
)
88 when (im
== IMPlay
) checkWon
89 when (im
== IMMeta
) $ (checkAsync
>>) $ void
.runMaybeT
$
90 mourNameSelected
>>? lift purgeInvalidUndecls
92 cmds
<- lift
$ getSomeInput im
93 runExceptT
(mapM_ (processCommand im
) cmds
) >>=
95 ((lift clearMessage
>>) . return)
98 -- | unblock input whenever the newAsync TVar is set to True
99 spawnUnblockerThread
= do
100 flag
<- gets newAsync
101 unblock
<- lift unblockInput
102 liftIO
$ forkIO
$ forever
$ do
103 atomically
$ readTVar flag
>>= check
>> writeTVar flag
False
106 runSubMainState
:: UIMonad uiM
=> MainState
-> MainStateT uiM
(InteractSuccess
,MainState
)
107 runSubMainState mSt
= lift
(runStateT interactUI mSt
) <* cleanOnPop
108 where cleanOnPop
= do
111 when (im
== IMEdit
) setSelectedPosFromMouse
113 execSubMainState
:: UIMonad uiM
=> MainState
-> MainStateT uiM MainState
114 execSubMainState
= (snd <$>) . runSubMainState
116 doInitiation
:: UIMonad uiM
=> MainStateT uiM
()
118 (InteractSuccess complete
, s
) <- runSubMainState
=<< liftIO initInitState
119 liftIO
$ writeInitState s
121 modify
$ \s
-> s
{initiated
= True}
122 mauth
<- gets curAuth
123 when (isNothing mauth
) $ do
124 cbdg
<- lift
$ getUIBinding IMMeta
$ CmdSelCodename Nothing
125 rbdg
<- lift
$ getUIBinding IMMeta
(CmdRegister
False)
126 let showPage p prompt
= lift
$ withNoBG
$ showHelp IMMeta p
>>?
do
127 void
$ textInput prompt
1 False True Nothing Nothing
128 showPage
(HelpPageInitiated
1) "[Initiation complete. Press a key or RMB to continue]"
129 showPage
(HelpPageInitiated
2) "[Press a key or RMB to continue]"
130 showPage
(HelpPageInitiated
3) "[Press a key or RMB to continue]"
132 "To join the game: pick a codename ('"++cbdg
++
133 "') and register it ('"++rbdg
++"')."
137 if null cmds
then getSomeInput im
else return cmds
139 processCommand
:: UIMonad uiM
=> InputMode
-> Command
-> ExceptT InteractSuccess
(MainStateT uiM
) ()
140 processCommand im CmdQuit
= do
142 IMPlay
-> lift
(or <$> sequence [gets psIsSub
, gets psSaved
, gets
(null . psGameStateMoveStack
)])
143 >>? throwE
$ InteractSuccess
False
144 IMEdit
-> lift editStateUnsaved
>>! throwE
$ InteractSuccess
True
145 _
-> throwE
$ InteractSuccess
False
146 title
<- lift getTitle
147 (lift
. lift
. confirm
) ("Really quit"
148 ++ (if im
== IMEdit
then " without saving" else "")
149 ++ maybe "" ((" from "++) . fst) title
++ "?")
150 >>? throwE
$ InteractSuccess
False
151 processCommand im CmdForceQuit
= throwE
$ InteractSuccess
False
152 processCommand IMPlay CmdOpen
= do
153 st
<- gets psCurrentState
154 frame
<- gets psFrame
155 if checkSolved
(frame
,st
)
156 then throwE
$ InteractSuccess
True
157 else lift
.lift
$ drawError
"Locked!"
159 processCommand IMInit
(CmdSolveInit Nothing
) = void
.runMaybeT
$ do
160 tutSolved
<- lift
. gets
$ tutSolved
. tutProgress
161 accessible
<- lift
. gets
$ accessibleInitLocks tutSolved
. initLocks
162 v
<- if Map
.null accessible
then return zero
else do
163 let nameMap
= Map
.fromList
$ ("TUT",zero
) :
164 [(initLockName l
, v
) |
(v
,l
) <- Map
.toList accessible
]
165 names
= Map
.keys nameMap
166 name
<- (map toUpper <$>) . MaybeT
. lift
. lift
$
167 textInput
("Solve which? ["
168 ++ intercalate
"," (take 3 names
)
169 ++ if length names
> 3 then ",...]" else "]")
170 3 False True (Just names
) Nothing
171 MaybeT
. return $ Map
.lookup name nameMap
172 lift
. processCommand IMInit
. CmdSolveInit
$ Just v
173 processCommand IMInit
(CmdSolveInit
(Just v
)) | v
== zero
= lift
.void
.runMaybeT
$ do
174 tutdir
<- liftIO
$ getDataPath
"tutorial"
175 tuts
<- liftIO
. ignoreIOErr
$
176 sort . map (takeWhile (/='.')) . filter (isSuffixOf ".lock") <$>
177 getDirectoryContents tutdir
178 when (null tuts
) $ do
179 lift
.lift
$ drawError
"No tutorial levels found"
181 let dotut i msps
= do
182 let name
= tuts
!! (i
-1)
183 let pref
= tutdir
++ [pathSeparator
] ++ name
184 (lock
,_
) <- MaybeT
$ liftIO
$ readLock
(pref
++ ".lock")
185 text
<- liftIO
$ fromMaybe "" . listToMaybe <$> readStrings
(pref
++ ".text")
186 solveLockSaving i msps
(Just i
) lock
$ Just
$ "Tutorial " ++ show i
++ ": " ++ text
187 if i
+1 <= length tuts
188 then dotut
(i
+1) Nothing
190 modify
$ \is
-> is
{tutProgress
= TutProgress
True 1 Nothing
}
191 lift
$ drawMessage
"Tutorial complete!"
192 TutProgress _ onLevel msps
<- lift
$ gets tutProgress
194 processCommand IMInit
(CmdSolveInit
(Just v
)) = void
.runMaybeT
$ do
195 l
@InitLock
{ initLockDesc
=desc
, initLockLock
=lock
, initLockPartial
=partial
} <-
196 MaybeT
. lift
$ gets
(Map
.lookup v
. initLocks
)
198 (InteractSuccess solved
, ps
) <- lift
. runSubMainState
$
199 maybe newPlayState restorePlayState partial
(reframe lock
) [] (Just desc
) Nothing
False True
200 let updateLock initLock
= initLock
{ initLockSolved
= initLockSolved initLock || solved
201 , initLockPartial
= Just
$ savePlayState ps
}
202 lift
. modify
$ \is
-> is
{ initLocks
= Map
.adjust updateLock v
$ initLocks is
}
203 when (solved
&& isLastInitLock l
) . throwE
$ InteractSuccess
True
205 processCommand im cmd
= lift
$ processCommand
' im cmd
207 processCommand
' :: UIMonad uiM
=> InputMode
-> Command
-> MainStateT uiM
()
208 processCommand
' im CmdHelp
= lift
$ do
209 helpPages
<- case im
of
210 IMInit
-> return [HelpPageGame
]
211 IMMeta
-> return [HelpPageInput
, HelpPageGame
]
213 first
<- not <$> liftIO hasLocks
214 return $ HelpPageInput
: [HelpPageFirstEdit | first
]
215 _
-> return [HelpPageInput
]
216 let showPage p
= withNoBG
$ showHelp im p
>>?
do
217 void
$ textInput
"[press a key or RMB]" 1 False True Nothing Nothing
218 mapM_ showPage helpPages
219 processCommand
' im
(CmdBind mcmd
)= lift
$ (>> endPrompt
) $ runMaybeT
$ do
220 cmd
<- liftMaybe mcmd `mplus`
do
221 lift
$ drawPrompt
False "Command to bind: "
223 cmd
<- MaybeT
$ listToMaybe <$> getInput im
224 guard $ not.null $ describeCommand cmd
226 lift
$ drawPrompt
False ("key to bind to \"" ++ describeCommand cmd
++ "\" (repeat existing user binding to delete): ")
227 ch
<- MaybeT getChRaw
229 lift
$ setUIBinding im cmd ch
230 processCommand
' _ CmdToggleColourMode
= lift toggleColourMode
231 processCommand
' _ CmdSuspend
= lift suspend
232 processCommand
' _ CmdRedraw
= lift redraw
233 processCommand
' im CmdClear
= do
235 when (im
== IMMeta
) $ modify
$ \ms
-> ms
{ retiredLocks
= Nothing
}
236 processCommand
' im CmdMark
= void
.runMaybeT
$ do
237 guard $ im `
elem`
[IMEdit
, IMPlay
, IMReplay
]
238 str
<- MaybeT
$ lift
$ textInput
"Type character for mark: "
239 1 False True Nothing Nothing
240 ch
<- liftMaybe
$ listToMaybe str
241 guard $ ch `
notElem`
[startMark
, '\'']
242 lift
$ setMark
True ch
243 processCommand
' im CmdJumpMark
= void
.runMaybeT
$ do
244 guard $ im `
elem`
[IMEdit
, IMPlay
, IMReplay
]
245 marks
<- lift marksSet
246 str
<- MaybeT
$ lift
$ textInput
247 ("Jump to mark [" ++ intersperse ',' marks
++ "]: ")
248 1 False True (Just
$ (:[]) <$> marks
) Nothing
249 ch
<- liftMaybe
$ listToMaybe str
251 processCommand
' im CmdReset
= jumpMark startMark
253 processCommand
' IMMeta CmdInitiation
= doInitiation
254 processCommand
' IMMeta
(CmdSelCodename mname
) = void
.runMaybeT
$ do
255 mauth
<- gets curAuth
256 name
<- msum [ liftMaybe mname
258 newCodename
<- (map toUpper <$>) $ MaybeT
$ lift
$
259 textInput
"Select codename:"
260 3 False False Nothing Nothing
261 guard $ length newCodename
== 3
264 guard $ validCodeName name
266 modify
$ \ms
-> ms
{ codenameStack
= name
:codenameStack ms
}
269 processCommand
' IMMeta CmdHome
= void
.runMaybeT
$ do
270 ourName
<- mgetOurName
272 modify
$ \ms
-> ms
{ codenameStack
= ourName
:codenameStack ms
}
274 processCommand
' IMMeta CmdBackCodename
= do
275 stack
<- gets codenameStack
276 when (length stack
> 1) $ do
277 modify
$ \ms
-> ms
{ codenameStack
= tail stack
}
279 processCommand
' IMMeta CmdSetServer
= void
.runMaybeT
$ do
280 saddr
<- gets curServer
281 saddrs
<- liftIO knownServers
282 newSaddr
' <- MaybeT
$ ((>>= strToSaddr
) <$>) $
283 lift
$ textInput
"Set server:" 256 False False
284 (Just
$ saddrStr
<$> saddrs
) (Just
$ saddrStr saddr
)
285 let newSaddr
= if nullSaddr newSaddr
' then defaultServerAddr
else newSaddr
'
286 modify
$ \ms
-> ms
{ curServer
= newSaddr
}
287 msum [ void
.MaybeT
$ getFreshRecBlocking RecServerInfo
288 , modify
(\ms
-> ms
{ curServer
= saddr
}) >> mzero
]
290 modify
$ \ms
-> ms
{curAuth
= Nothing
}
291 get
>>= liftIO
. writeServerSolns saddr
292 (undecls
,partials
) <- liftIO
(readServerSolns newSaddr
)
293 modify
$ \ms
-> ms
{ undeclareds
=undecls
, partialSolutions
=partials
}
294 rnamestvar
<- gets randomCodenames
295 liftIO
$ atomically
$ writeTVar rnamestvar
[]
298 processCommand
' IMMeta CmdToggleCacheOnly
= do
299 newCOnly
<- gets
$ not . cacheOnly
300 modify
$ \ms
-> ms
{cacheOnly
= newCOnly
}
302 invalidateAllUInfo
>> invalidateAllIndexedLocks
304 processCommand
' IMMeta
(CmdRegister _
) = void
.runMaybeT
$ do
305 regName
<- mgetCurName
306 mauth
<- gets curAuth
307 let isUs
= maybe False ((==regName
).authUser
) mauth
310 confirmOrBail
"Log out?"
311 modify
$ \ms
-> ms
{curAuth
= Nothing
}
313 confirmOrBail
"Reset password?"
314 void
.lift
.runMaybeT
$ do
315 passwd
<- inputPassword regName
True "Enter new password:"
317 resp
<- curServerAction
$ ResetPassword passwd
320 lift
$ drawMessage
"New password set."
321 modify
$ \ms
-> ms
{curAuth
= Just
$ Auth regName passwd
}
322 ServerError err
-> lift
$ drawError err
323 _
-> lift
$ drawMessage
$ "Bad server response: " ++ show resp
325 confirmOrBail
"Configure email notifications?"
330 lift
.lift
$ drawError
"Sorry, this codename is already taken."
332 confirmOrBail
$ "Register codename " ++ regName
++ "?"
333 passwd
<- inputPassword regName
True "Enter new password:"
335 modify
$ \ms
-> ms
{curAuth
= Just
$ Auth regName passwd
}
336 resp
<- curServerAction Register
339 invalidateUInfo regName
341 conf
<- lift
$ confirm
"Registered! Would you like to be notified by email when someone solves your lock?"
342 if conf
then void
$ runMaybeT setNotifications
else lift
$ drawMessage
"Notifications disabled."
343 ServerError err
-> do
345 modify
$ \ms
-> ms
{curAuth
= Nothing
}
346 _
-> lift
$ drawMessage
$ "Bad server response: " ++ show resp
349 where setNotifications
= do
350 address
<- MaybeT
$ lift
$ textInput
"Enter address, or leave blank to disable notifications:" 128 False False Nothing Nothing
352 resp
<- curServerAction
$ SetEmail address
354 ServerAck
-> lift
$ drawMessage
$ if null address
then "Notifications disabled." else "Address set."
355 ServerError err
-> lift
$ drawError err
356 _
-> lift
$ drawMessage
$ "Bad server response: " ++ show resp
359 processCommand
' IMMeta CmdAuth
= void
.runMaybeT
$ do
360 auth
<- lift
$ gets curAuth
361 if isJust auth
then do
362 confirmOrBail
"Log out?"
363 modify
$ \ms
-> ms
{curAuth
= Nothing
}
366 passwd
<- inputPassword name
False $ "Enter password for "++name
++":"
368 modify
$ \ms
-> ms
{curAuth
= Just
$ Auth name passwd
}
369 resp
<- curServerAction Authenticate
371 ServerAck
-> lift
$ drawMessage
"Authenticated."
372 ServerMessage msg
-> lift
$ drawMessage
$ "Server: " ++ msg
373 ServerError err
-> do
375 modify
$ \ms
-> ms
{curAuth
= auth
}
376 _
-> lift
$ drawMessage
$ "Bad server response: " ++ show resp
378 processCommand
' IMMeta
(CmdSolve midx
) = void
.runMaybeT
$ do
380 uinfo
<- mgetUInfo name
381 idx
<- msum [ liftMaybe midx
382 , askLockIndex
"Solve which lock?" "No lock to solve!" (\i
-> isJust $ userLocks uinfo
! i
) ]
383 ls
<- liftMaybe
$ lockSpec
<$> userLocks uinfo
! idx
384 undecls
<- lift
(gets undeclareds
)
386 undecl
<- liftMaybe
$ find (\(Undeclared _ ls
' _
) -> ls
== ls
') undecls
387 MaybeT
$ gets curAuth
388 confirmOrBail
"Declare existing solution?"
389 void
.lift
.runMaybeT
$ -- ignores MaybeT failures
392 RCLock lock
<- MaybeT
$ getFreshRecBlocking
$ RecLock ls
393 mpartial
<- gets
(Map
.lookup ls
. partialSolutions
)
394 soln
<- solveLockSaving ls mpartial Nothing lock
$ Just
$
395 "solving " ++ name
++ ":" ++ [lockIndexChar idx
] ++ " (#" ++ show ls
++")"
396 mourName
<- lift
$ gets
((authUser
<$>) . curAuth
)
397 guard $ mourName
/= Just name
398 let undecl
= Undeclared soln ls
(ActiveLock name idx
)
400 MaybeT
$ gets curAuth
401 confirmOrBail
"Declare solution?"
403 , unless (any (\(Undeclared _ ls
' _
) -> ls
== ls
') undecls
) $
404 modify
$ \ms
-> ms
{ undeclareds
= undecl
: undeclareds ms
}
407 processCommand
' IMMeta
(CmdPlayLockSpec mls
) = void
.runMaybeT
$ do
408 ls
<- msum [ liftMaybe mls
410 tls
<- MaybeT
. lift
$ textInput
"Lock number:" 16 False False Nothing Nothing
411 liftMaybe
$ readMay tls
413 RCLock lock
<- MaybeT
$ getFreshRecBlocking
$ RecLock ls
414 solveLock lock Nothing
$ Just
$ "solving " ++ show ls
416 processCommand
' IMMeta
(CmdDeclare mundecl
) = void
.runMaybeT
$ do
417 guard =<< mourNameSelected
419 undecls
<- lift
$ gets undeclareds
420 guard $ not $ null undecls
421 declare
=<< msum [ liftMaybe mundecl
422 , if length undecls
== 1
423 then return $ head undecls
425 which
<- MaybeT
$ lift
$ textInput
426 "Declare which solution?"
427 5 False True Nothing Nothing
431 guard $ 0 < i
&& i
<= length undecls
432 return $ undecls
!! (i
-1)
435 | undecl
@(Undeclared _ _
(ActiveLock name
' i
)) <- undecls
436 , (take (length which
) (name
' ++ ":" ++ [lockIndexChar i
]) ==
437 map toUpper which
) ||
(name
'==name
&& [lockIndexChar i
] == which
)
441 processCommand
' IMMeta
(CmdViewSolution mnote
) = void
.runMaybeT
$ do
442 note
<- liftMaybe mnote `mplus`
do
443 ourName
<- mgetOurName
445 uinfo
<- mgetUInfo name
446 noteses
<- lift
$ sequence
449 Just lockinfo
-> (++lockSolutions lockinfo
) <$> do
450 ns
<- getNotesReadOn lockinfo
451 return $ if length ns
< 3 then [] else ns
452 | mlockinfo
<- elems $ userLocks uinfo
]
453 idx
<- askLockIndex
"View solution to which lock?" "No solutions to view" $ not.null.(noteses
!!)
454 let notes
= noteses
!!idx
455 authors
= noteAuthor
<$> notes
456 author
<- if length notes
== 1
457 then return $ noteAuthor
$ head notes
458 else (map toUpper <$>) $ MaybeT
$ lift
$
459 textInput
("View solution by which player? ["
460 ++ intercalate
"," (take 3 authors
)
461 ++ if length authors
> 3 then ",...]" else "]")
462 3 False True (Just authors
) Nothing
463 liftMaybe
$ find ((==author
).noteAuthor
) notes
464 let ActiveLock name idx
= noteOn note
465 uinfo
<- mgetUInfo name
466 ls
<- lockSpec
<$> MaybeT
(return $ userLocks uinfo
! idx
)
467 RCLock lock
<- MaybeT
$ getFreshRecBlocking
$ RecLock ls
468 RCSolution soln
<- MaybeT
$ getFreshRecBlocking
$ RecNote note
469 lift
$ execSubMainState
$ newReplayState
(snd.reframe
$lock
) soln
$ Just
$
470 "viewing solution by " ++ noteAuthor note
++ " to " ++ name
++ [':',lockIndexChar idx
]
472 processCommand
' IMMeta
(CmdPlaceLock midx
) = void
.runMaybeT
$ do
473 guard =<< mourNameSelected
474 ourName
<- mgetOurName
475 (lock
,msoln
) <- MaybeT
(gets curLock
) `mplus`
do
476 ebdg
<- lift
.lift
$ getUIBinding IMMeta CmdEdit
477 lift
.lift
$ drawError
$ "No lock selected; '"++ebdg
++"' to edit one."
479 lockpath
<- lift
$ gets curLockPath
480 ourUInfo
<- mgetUInfo ourName
481 idx
<- (liftMaybe midx `mplus`
) $
482 askLockIndex
("Place " ++ show lockpath
++ " in which slot?") "bug" $ const True
483 when (isJust $ userLocks ourUInfo
! idx
) $
484 confirmOrBail
"Really retire existing lock?"
485 soln
<- (liftMaybe msoln `mplus`
) $ solveLock lock Nothing
$ Just
"testing lock"
486 lift
$ curServerActionAsyncThenInvalidate
487 (SetLock lock idx soln
)
488 (Just
(SomeCodenames
[ourName
]))
490 processCommand
' IMMeta CmdSelectLock
= void
.runMaybeT
$ do
491 lockdir
<- liftIO
$ confFilePath
"locks"
492 paths
<- liftIO
$ map (drop (length lockdir
+ 1)) <$> getDirContentsRec lockdir
493 path
<- MaybeT
$ lift
$ textInput
"Lock name:" 1024 False False (Just paths
) Nothing
494 lift
$ setLockPath path
495 processCommand
' IMMeta CmdNextLock
=
496 gets curLockPath
>>= liftIO
. nextLock
True >>= setLockPath
497 processCommand
' IMMeta CmdPrevLock
=
498 gets curLockPath
>>= liftIO
. nextLock
False >>= setLockPath
499 processCommand
' IMMeta CmdNextPage
=
500 gets listOffsetMax
>>!
501 modify
$ \ms
-> ms
{ listOffset
= listOffset ms
+ 1 }
502 processCommand
' IMMeta CmdPrevPage
=
503 modify
$ \ms
-> ms
{ listOffset
= max 0 $ listOffset ms
- 1 }
504 processCommand
' IMMeta CmdEdit
= void
.runMaybeT
$ do
505 (lock
, msoln
) <- MaybeT
(gets curLock
) `mplus`
do
509 RCServerInfo
(ServerInfo size _
) <- MaybeT
$ getFreshRecBlocking RecServerInfo
512 sizet
<- MaybeT
$ lift
$ textInput
513 ("Lock size: [3-" ++ show maxlocksize
++ "]") 2 False False Nothing Nothing
514 size
<- liftMaybe
$ readMay sizet
515 guard $ 3 <= size
&& size
<= maxlocksize
518 return (baseLock size
, Nothing
)
519 not <$> liftIO hasLocks
>>?
do
520 lift
.lift
$ withNoBG
$ showHelp IMEdit HelpPageFirstEdit
>>?
do
522 "[Press a key or RMB to continue; you can review this help later with '?']"
523 1 False True Nothing Nothing
524 path
<- lift
$ gets curLockPath
525 newPath
<- MaybeT
$ (esPath
<$>) $ execSubMainState
$
526 newEditState
(reframe lock
) msoln
(if null path
then Nothing
else Just path
)
527 lift
$ setLockPath newPath
528 processCommand
' IMMeta CmdShowRetired
= void
.runMaybeT
$ do
530 newRL
<- lift
(gets retiredLocks
) >>= \case
532 RCLockSpecs lss
<- MaybeT
$ getFreshRecBlocking
$ RecRetiredLocks name
535 lift
.lift
$ drawError
"Player has no retired locks."
537 else return $ Just lss
538 Just _
-> return Nothing
539 lift
$ modify
$ \ms
-> ms
{retiredLocks
= newRL
}
541 processCommand
' IMPlay CmdUndo
= do
542 st
<- gets psCurrentState
543 stack
<- gets psGameStateMoveStack
544 ustms
<- gets psUndoneStack
545 unless (null stack
) $ do
546 let (st
',pm
) = head stack
547 modify
$ \ps
-> ps
{psCurrentState
=st
', psGameStateMoveStack
= tail stack
,
548 psLastAlerts
= [], psUndoneStack
= pm
:ustms
}
549 processCommand
' IMPlay CmdRedo
= do
550 ustms
<- gets psUndoneStack
554 st
<- gets psCurrentState
555 (st
',alerts
) <- lift
$ doPhysicsTick pm st
557 modify
$ \ps
-> ps
{psLastAlerts
= alerts
, psUndoneStack
= ustms
'}
558 processCommand
' IMPlay
(CmdManipulateToolAt pos
) = do
559 board
<- gets
(stateBoard
. psCurrentState
)
560 wsel
<- gets wrenchSelected
561 void
.runMaybeT
$ msum $ (do
562 tile
<- liftMaybe
$ snd <$> Map
.lookup pos board
563 guard $ case tile
of {WrenchTile _
-> True; HookTile
-> True; _
-> False}
564 lift
$ processCommand
' IMPlay
$ CmdTile tile
) : [ do
565 tile
<- liftMaybe
$ snd <$> Map
.lookup (d
+^pos
) board
566 guard $ tileType tile
== if wsel
then WrenchTile zero
else HookTile
567 lift
$ processCommand
' IMPlay
$ CmdDir WHSSelected
$ neg d
569 processCommand
' IMPlay
(CmdDrag pos dir
) = do
570 board
<- gets
(stateBoard
. psCurrentState
)
571 wsel
<- gets wrenchSelected
573 tp
<- liftMaybe
$ tileType
. snd <$> Map
.lookup pos board
574 msum [ guard $ tp
== HookTile
576 guard $ tp
== WrenchTile zero
577 board
' <- lift
$ gets
((stateBoard
. fst . runWriter
. physicsTick
(WrenchPush dir
)) . psCurrentState
)
579 tp
' <- liftMaybe
$ tileType
. snd <$> Map
.lookup pos
' board
'
580 guard $ tp
' == WrenchTile zero
582 , let pos
' = d
*^ dir
+^ pos
]
583 ++ [ (lift
.lift
$ warpPointer pos
) >> mzero
]
585 lift
$ processCommand
' IMPlay
$ CmdDir WHSSelected dir
586 board
' <- gets
(stateBoard
. psCurrentState
)
588 tp
' <- liftMaybe
$ tileType
. snd <$> Map
.lookup pos
' board
'
589 guard $ tp
' == if wsel
then WrenchTile zero
else HookTile
590 lift
.lift
$ warpPointer pos
'
591 | pos
' <- (+^pos
) <$> hexDisc
2 ]
593 processCommand
' IMPlay cmd
= do
594 wsel
<- gets wrenchSelected
595 st
<- gets psCurrentState
597 | whs
== WHSWrench ||
(whs
== WHSSelected
&& wsel
) =
598 Just
$ WrenchPush dir
599 |
otherwise = Just
$ HookPush dir
601 {- | whs == WHSHook || (whs == WHSSelected && not wsel)=
602 Just $ HookTorque dir
603 | otherwise = Nothing -}
604 = Just
$ HookTorque dir
607 CmdTile
(WrenchTile _
) -> (True, Nothing
)
608 CmdTile HookTile
-> (False, Nothing
)
609 CmdTile
(ArmTile _ _
) -> (False, Nothing
)
610 CmdToggle
-> (not wsel
, Nothing
)
611 CmdDir whs dir
-> (wsel
, push whs dir
)
612 CmdRotate whs dir
-> (wsel
, torque whs dir
)
613 CmdWait
-> (wsel
, Just NullPM
)
614 CmdSelect
-> (wsel
, Just NullPM
)
616 modify
$ \ps
-> ps
{wrenchSelected
= wsel
'}
620 (st
',alerts
) <- lift
$ doPhysicsTick pm
' st
621 modify
$ \ps
-> ps
{psLastAlerts
= alerts
}
624 processCommand
' IMReplay
(CmdReplayBack
1) = void
.runMaybeT
$ do
625 (st
',pm
) <- MaybeT
$ gets
(listToMaybe . rsGameStateMoveStack
)
626 lift
$ modify
$ \rs
-> rs
{rsCurrentState
=st
'
628 , rsGameStateMoveStack
= tail $ rsGameStateMoveStack rs
629 , rsMoveStack
= pm
:rsMoveStack rs
}
630 processCommand
' IMReplay
(CmdReplayBack n
) = replicateM_ n
$
631 processCommand
' IMReplay
(CmdReplayBack
1)
632 processCommand
' IMReplay
(CmdReplayForward
1) = void
.runMaybeT
$ do
633 pm
<- MaybeT
$ gets
(listToMaybe . rsMoveStack
)
635 st
<- gets rsCurrentState
636 (st
',alerts
) <- lift
$ doPhysicsTick pm st
637 modify
$ \rs
-> rs
{rsCurrentState
= st
'
638 , rsLastAlerts
= alerts
639 , rsGameStateMoveStack
= (st
,pm
):rsGameStateMoveStack rs
640 , rsMoveStack
= tail $ rsMoveStack rs
}
641 processCommand
' IMReplay
(CmdReplayForward n
) = replicateM_ n
$
642 processCommand
' IMReplay
(CmdReplayForward
1)
643 processCommand
' IMReplay CmdUndo
= processCommand
' IMReplay
(CmdReplayBack
1)
644 processCommand
' IMReplay CmdRedo
= processCommand
' IMReplay
(CmdReplayForward
1)
646 processCommand
' IMEdit CmdPlay
= do
647 st
<- gets esGameState
648 frame
<- gets esFrame
649 modify
$ \es
-> es
{selectedPiece
= Nothing
}
651 processCommand
' IMEdit CmdTest
= do
652 frame
<- gets esFrame
653 modifyEState
(\st
-> snd $ canonify
(frame
, st
))
654 modify
$ \es
-> es
{selectedPiece
= Nothing
}
656 st
<- gets esGameState
657 curSoln
<- runMaybeT
$ do
658 (st
', soln
) <- MaybeT
$ gets esTested
662 soln
<- solveLock
(frame
,st
) curSoln
$ Just
$ "testing " ++ fromMaybe "[unnamed lock]" mpath
663 lift
$ modify
$ \es
-> es
{ esTested
= Just
(st
, soln
) }
664 processCommand
' IMEdit CmdUndo
= do
665 st
<- gets esGameState
666 sts
<- gets esGameStateStack
667 usts
<- gets esUndoneStack
668 unless (null sts
) $ modify
$ \es
-> es
{esGameState
= head sts
, esGameStateStack
= tail sts
, esUndoneStack
= st
:usts
}
669 processCommand
' IMEdit CmdRedo
= do
670 usts
<- gets esUndoneStack
675 modify
$ \es
-> es
{esUndoneStack
= usts
'}
676 processCommand
' IMEdit CmdUnselect
=
677 modify
$ \es
-> es
{selectedPiece
= Nothing
}
678 processCommand
' IMEdit CmdSelect
= do
679 selPiece
<- gets selectedPiece
680 selPos
<- gets selectedPos
681 st
<- gets esGameState
685 else fmap fst . Map
.lookup selPos
$ stateBoard st
686 modify
$ \es
-> es
{selectedPiece
= selPiece
'}
687 processCommand
' IMEdit
(CmdDir _ dir
) = do
688 selPos
<- gets selectedPos
689 selPiece
<- gets selectedPiece
690 frame
<- gets esFrame
692 Nothing
-> modify
$ \es
-> es
{selectedPos
= checkEditable frame selPos
$ dir
+^ selPos
}
693 Just p
-> doForce
$ Push p dir
694 processCommand
' IMEdit
(CmdMoveTo newPos
) =
695 setSelectedPos newPos
696 processCommand
' IMEdit
(CmdDrag pos dir
) = do
697 board
<- gets
(stateBoard
. esGameState
)
699 selIdx
<- MaybeT
$ gets selectedPiece
700 idx
<- liftMaybe
$ fst <$> Map
.lookup pos board
701 guard $ idx
== selIdx
702 lift
$ processCommand
' IMEdit
$ CmdDir WHSSelected dir
703 board
' <- gets
(stateBoard
. esGameState
)
705 idx
' <- liftMaybe
$ fst <$> Map
.lookup pos
' board
'
706 guard $ idx
' == selIdx
707 lift
.lift
$ warpPointer pos
'
708 | pos
' <- [dir
+^pos
, pos
] ]
709 processCommand
' IMEdit
(CmdRotate _ dir
) = do
710 selPiece
<- gets selectedPiece
711 st
<- gets esGameState
715 let PlacedPiece _ pc
= getpp st p
716 torquable
= case pc
of
721 then doForce
$ Torque p dir
722 else adjustSpringTension p dir
723 processCommand
' IMEdit
(CmdTile tile
) = do
724 selPos
<- gets selectedPos
725 drawTile selPos
(Just tile
) False
726 processCommand
' IMEdit
(CmdPaint tile
) = do
727 selPos
<- gets selectedPos
728 drawTile selPos tile
True
729 processCommand
' IMEdit
(CmdPaintFromTo tile from to
) = do
730 frame
<- gets esFrame
731 paintTilePath frame tile
(truncateToEditable frame from
) (truncateToEditable frame to
)
732 processCommand
' IMEdit CmdMerge
= do
733 selPos
<- gets selectedPos
734 st
<- gets esGameState
735 lift
$ drawMessage
"Merge in which direction?"
737 cmd
<- lift
$ head <$> getSomeInput IMEdit
739 CmdDir _ mergeDir
-> return $ Just mergeDir
740 CmdDrag _ mergeDir
-> return $ Just mergeDir
741 CmdMoveTo _
-> getDir
745 Just mergeDir
-> modifyEState
$ mergeTiles selPos mergeDir
True
747 -- XXX: merging might invalidate selectedPiece
748 modify
$ \es
-> es
{selectedPiece
= Nothing
}
750 processCommand
' IMEdit CmdWait
= do
751 st
<- gets esGameState
752 (st
',_
) <- lift
$ doPhysicsTick NullPM st
755 processCommand
' IMEdit CmdDelete
= do
756 selPos
<- gets selectedPos
757 selPiece
<- gets selectedPiece
758 st
<- gets esGameState
760 Nothing
-> drawTile selPos Nothing
False
761 Just p
-> do modify
$ \es
-> es
{selectedPiece
= Nothing
}
762 modifyEState
$ delPiece p
763 processCommand
' IMEdit CmdWriteState
= void
.runMaybeT
$ do
764 path
<- lift
$ gets esPath
765 newPath
<- MaybeT
$ lift
$ textInput
"Save lock as:" 1024 False False Nothing path
766 guard $ not $ null newPath
767 fullPath
<- liftIO
$ fullLockPath newPath
768 liftIO
(fileExists fullPath
) >>?
769 confirmOrBail
$ "Really overwrite '"++fullPath
++"'?"
771 st
<- gets esGameState
772 frame
<- gets esFrame
773 msoln
<- getCurTestSoln
774 merr
<- liftIO
$ (writeAsciiLockFile fullPath msoln
(canonify
(frame
, st
)) >> return Nothing
)
775 `catchIO`
(return . Just
. show)
776 modify
$ \es
-> es
{lastSavedState
= Just
(st
,isJust msoln
)}
778 Nothing
-> modify
$ \es
-> es
{esPath
= Just newPath
}
779 Just err
-> lift
$ drawError
$ "Write failed: "++err
780 processCommand
' _ _
= return ()
782 inputPassword
:: UIMonad uiM
=> Codename
-> Bool -> String -> MaybeT
(MainStateT uiM
) String
783 inputPassword name confirm prompt
= do
784 pw
<- MaybeT
$ lift
$ textInput prompt
64 True False Nothing Nothing
785 guard $ not $ null pw
787 pw
' <- MaybeT
$ lift
$ textInput
"Confirm password:" 64 True False Nothing Nothing
788 when (pw
/= pw
') $ do
789 lift
.lift
$ drawError
"Passwords don't match!"
791 RCPublicKey publicKey
<- MaybeT
$ getFreshRecBlocking RecPublicKey
792 encryptPassword publicKey name pw
794 -- | Salt and encrypt a password, to protect users' passwords from sniffing
795 -- and dictionary attack. We can hope that they wouldn't use valuable
796 -- passwords, but we shouldn't assume it.
797 -- Note that in all other respects, the protocol is entirely insecure -
798 -- nothing else is encrypted, and anyone sniffing an encrypted password can
799 -- replay it to authenticate as the user.
800 encryptPassword
:: UIMonad uiM
=>
801 PublicKey
-> String -> String -> MaybeT
(MainStateT uiM
) String
802 encryptPassword publicKey name password
= MaybeT
. liftIO
.
803 handle
(\(e
:: SomeException
) -> return Nothing
) $ do
804 Right c
<- encrypt
(defaultOAEPParams SHA256
) publicKey
. CS
.pack
$ hashed
805 return . Just
. CS
.unpack
$ c
806 where hashed
= hash
$ "IY" ++ name
++ password
808 setSelectedPosFromMouse
:: UIMonad uiM
=> MainStateT uiM
()
809 setSelectedPosFromMouse
= lift getUIMousePos
>>= maybe (return ()) setSelectedPos
811 setSelectedPos
:: Monad m
=> HexPos
-> MainStateT m
()
812 setSelectedPos pos
= do
813 frame
<- gets esFrame
814 modify
$ \es
-> es
{selectedPos
= truncateToEditable frame pos
}
816 subPlay
:: UIMonad uiM
=> Lock
-> MainStateT uiM
()
818 pushEState
. psCurrentState
=<< execSubMainState
(newPlayState lock
[] Nothing Nothing
True False)
820 solveLock
:: UIMonad uiM
=> Lock
-> Maybe Solution
-> Maybe String -> MaybeT
(MainStateT uiM
) Solution
821 solveLock
= solveLock
' Nothing
822 solveLock
' tutLevel lock mSoln title
= do
823 (InteractSuccess solved
, ps
) <- lift
$ runSubMainState
$
824 newPlayState
(reframe lock
) (fromMaybe [] mSoln
) title tutLevel
False False
826 return . reverse $ snd <$> psGameStateMoveStack ps
828 solveLockSaving
:: UIMonad uiM
=> LockSpec
-> Maybe SavedPlayState
-> Maybe Int -> Lock
-> Maybe String -> MaybeT
(MainStateT uiM
) Solution
829 solveLockSaving ls msps tutLevel lock title
= do
830 let isTut
= isJust tutLevel
831 (InteractSuccess solved
, ps
) <- lift
$ runSubMainState
$
832 maybe newPlayState restorePlayState msps
(reframe lock
) [] title tutLevel
False True
835 unless isTut
. lift
. modify
$ \ms
-> ms
{ partialSolutions
= Map
.delete ls
$ partialSolutions ms
}
836 return . reverse $ snd <$> psGameStateMoveStack ps
838 lift
$ modify
$ \ms
-> if isTut
839 then ms
{ tutProgress
= (tutProgress ms
)
840 { tutLevel
= ls
, tutPartial
= Just
$ savePlayState ps
} }
841 else ms
{ partialSolutions
= Map
.insert ls
(savePlayState ps
) $ partialSolutions ms
}