File: HandleRequestM.hs

package info (click to toggle)
haskell-lambdahack 0.11.0.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,056 kB
  • sloc: haskell: 45,636; makefile: 219
file content (1247 lines) | stat: -rw-r--r-- 61,510 bytes parent folder | download | duplicates (3)
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
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
-- | Semantics of requests
-- .
-- A couple of them do not take time, the rest does.
-- Note that since the results are atomic commands, which are executed
-- only later (on the server and some of the clients), all condition
-- are checkd by the semantic functions in the context of the state
-- before the server command. Even if one or more atomic actions
-- are already issued by the point an expression is evaluated, they do not
-- influence the outcome of the evaluation.
module Game.LambdaHack.Server.HandleRequestM
  ( handleRequestAI, handleRequestUI, handleRequestTimed, switchLeader
  , reqMoveGeneric, reqDisplaceGeneric, reqAlterFail
  , reqGameDropAndExit, reqGameSaveAndExit
#ifdef EXPOSE_INTERNAL
    -- * Internal operations
  , execFailure, checkWaiting, processWatchfulness, affectStash
  , managePerRequest, handleRequestTimedCases, affectSmell
  , reqMove, reqMelee, reqMeleeChecked, reqDisplace, reqAlter
  , reqWait, reqWait10, reqYell, reqMoveItems, reqMoveItem, reqProject, reqApply
  , reqGameRestart, reqGameSave, reqDoctrine, reqAutomate
#endif
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import qualified Data.EnumMap.Strict as EM
import qualified Data.Text as T
import qualified Text.Show.Pretty as Show.Pretty

import           Game.LambdaHack.Atomic
import           Game.LambdaHack.Client
  (ReqAI (..), ReqUI (..), RequestTimed (..))
import           Game.LambdaHack.Common.Actor
import           Game.LambdaHack.Common.ActorState
import           Game.LambdaHack.Common.Analytics
import           Game.LambdaHack.Common.Faction
import           Game.LambdaHack.Common.Item
import qualified Game.LambdaHack.Common.ItemAspect as IA
import           Game.LambdaHack.Common.Kind
import           Game.LambdaHack.Common.Level
import           Game.LambdaHack.Common.Misc
import           Game.LambdaHack.Common.MonadStateRead
import           Game.LambdaHack.Common.Point
import           Game.LambdaHack.Common.ReqFailure
import           Game.LambdaHack.Common.State
import qualified Game.LambdaHack.Common.Tile as Tile
import           Game.LambdaHack.Common.Time
import           Game.LambdaHack.Common.Types
import           Game.LambdaHack.Common.Vector
import           Game.LambdaHack.Content.FactionKind
import qualified Game.LambdaHack.Content.ItemKind as IK
import           Game.LambdaHack.Content.ModeKind
import qualified Game.LambdaHack.Content.TileKind as TK
import qualified Game.LambdaHack.Definition.Ability as Ability
import           Game.LambdaHack.Definition.Defs
import           Game.LambdaHack.Server.CommonM
import           Game.LambdaHack.Server.HandleEffectM
import           Game.LambdaHack.Server.ItemM
import           Game.LambdaHack.Server.MonadServer
import           Game.LambdaHack.Server.PeriodicM
import           Game.LambdaHack.Server.ServerOptions
import           Game.LambdaHack.Server.State

execFailure :: MonadServerAtomic m
            => ActorId -> RequestTimed -> ReqFailure -> m ()
execFailure aid req failureSer = do
  -- Clients should rarely do that (only in case of invisible actors)
  -- so we report it to the client, but do not crash
  -- (server should work OK with stupid clients, too).
  body <- getsState $ getActorBody aid
  let fid = bfid body
      msg = showReqFailure failureSer
      impossible = impossibleReqFailure failureSer
      debugShow :: Show a => a -> Text
      debugShow = T.pack . Show.Pretty.ppShow
      possiblyAlarm = if impossible
                      then debugPossiblyPrintAndExit
                      else debugPossiblyPrint
  possiblyAlarm $
    "Server: execFailure:" <+> msg <> "\n"
    <> debugShow body <> "\n" <> debugShow req <> "\n" <> debugShow failureSer
  execSfxAtomic $ SfxMsgFid fid $ SfxUnexpected failureSer

-- | The semantics of server commands.
-- AI always takes time and so doesn't loop.
handleRequestAI :: MonadServerAtomic m
                => ReqAI
                -> m (Maybe RequestTimed)
handleRequestAI cmd = case cmd of
  ReqAITimed cmdT -> return $ Just cmdT
  ReqAINop -> return Nothing

-- | The semantics of server commands. Only the first two cases affect time.
handleRequestUI :: MonadServerAtomic m
                => FactionId -> ActorId -> ReqUI
                -> m (Maybe RequestTimed)
handleRequestUI fid aid cmd = case cmd of
  ReqUITimed cmdT -> return $ Just cmdT
  ReqUIGameRestart t d -> reqGameRestart aid t d >> return Nothing
  ReqUIGameDropAndExit -> reqGameDropAndExit aid >> return Nothing
  ReqUIGameSaveAndExit -> reqGameSaveAndExit aid >> return Nothing
  ReqUIGameSave -> reqGameSave >> return Nothing
  ReqUIDoctrine toT -> reqDoctrine fid toT >> return Nothing
  ReqUIAutomate -> reqAutomate fid >> return Nothing
  ReqUINop -> return Nothing

checkWaiting :: RequestTimed -> Maybe Bool
checkWaiting cmd = case cmd of
  ReqWait -> Just True  -- true wait, with bracing, no overhead, etc.
  ReqWait10 -> Just False  -- false wait, only one clip at a time
  _ -> Nothing

-- | This is a shorthand. Instead of setting @bwatch@ in @ReqWait@
-- and unsetting in all other requests, we call this once after
-- executing a request.
-- In game state, we collect the number of server requests pertaining
-- to the actor (the number of actor's "moves"), through which
-- the actor was waiting.
processWatchfulness :: MonadServerAtomic m => Maybe Bool -> ActorId -> m ()
processWatchfulness mwait aid = do
  b <- getsState $ getActorBody aid
  actorMaxSk <- getsState $ getActorMaxSkills aid
  let uneasy = deltasSerious (bcalmDelta b) || not (calmEnough b actorMaxSk)
  case bwatch b of
    WWatch ->
      when (mwait == Just True) $  -- only long wait switches to wait state
        if Ability.getSk Ability.SkWait actorMaxSk >= 2 then do
          addCondition False IK.S_BRACED aid
          execUpdAtomic $ UpdWaitActor aid WWatch (WWait 1)
        else
          execUpdAtomic $ UpdWaitActor aid WWatch (WWait 0)
    WWait 0 -> case mwait of  -- actor couldn't brace last time
      Just True -> return ()  -- if he still waits, keep him stuck unbraced
      _ -> execUpdAtomic $ UpdWaitActor aid (WWait 0) WWatch
    WWait n -> case mwait of
      Just True ->  -- only proper wait prevents switching to watchfulness
        if n >= 500 then  -- enough dozing to fall asleep
          if not uneasy  -- won't wake up at once
             && canSleep actorMaxSk  -- enough skills
          then do
            nAll <- removeConditionSingle IK.S_BRACED aid
            let !_A = assert (nAll == 0) ()
            addSleep aid
          else
            -- Start dozing from scratch to prevent hopeless skill checks.
            execUpdAtomic $ UpdWaitActor aid (WWait n) (WWait 1)
        else
          -- Doze some more before checking sleep eligibility.
          execUpdAtomic $ UpdWaitActor aid (WWait n) (WWait $ n + 1)
      _ -> do
        nAll <- removeConditionSingle IK.S_BRACED aid
        let !_A = assert (nAll == 0 `blame` nAll) ()
        execUpdAtomic $ UpdWaitActor aid (WWait n) WWatch
    WSleep ->
      if mwait /= Just False  -- lurk can't wake up regardless; too short
         && (isNothing mwait  -- not a wait
             || uneasy  -- spooked
             || not (deltaBenign $ bhpDelta b))  -- any HP lost
      then execUpdAtomic $ UpdWaitActor aid WSleep WWake
      else execUpdAtomic $ UpdRefillHP aid 10000
             -- no @xM@, so slow, but each turn HP gauge green;
             -- this is 1HP per 100 turns, so it's 10 times slower
             -- than a necklace that gives 1HP per 10 turns;
             -- so if an actor sleeps for the duration of a 1000 turns,
             -- which may be the time it takes to fully explore a level,
             -- 10HP would be gained, so weak actors would wake up twice over,
             -- which is fine: sleeping long enough to sidestep them at will,
             -- but attacking, e.g., a group with explosives, is good choice
             -- as well; so both stealth and mayhem fun correct tactically
    WWake -> unless (mwait == Just False) $  -- lurk can't wake up; too fast
      removeSleepSingle aid

-- Even very primitive actors that can't pick up items can take over stash,
-- to prevent them inadvertedly protecting enemy stash from skilled ones
-- by standing over it (which AI tends to do).
affectStash :: MonadServerAtomic m => Actor -> m ()
affectStash b = do
  let locateStash (fid, fact) = case gstash fact of
        Just (lidS, posS)
          | lidS == blid b && posS == bpos b && fid /= bfid b ->
            execUpdAtomic $ UpdLoseStashFaction True fid lidS posS
        _ -> return ()
  factionD <- getsState sfactionD
  mapM_ locateStash $ EM.assocs factionD

handleRequestTimed :: MonadServerAtomic m
                   => FactionId -> ActorId -> RequestTimed -> m Bool
handleRequestTimed fid aid cmd = do
  let mwait = checkWaiting cmd
  b <- getsState $ getActorBody aid
  -- Note that only the ordinary 1-turn wait eliminates overhead.
  -- The more fine-graned waits don't make actors braced and induce
  -- overhead, so that they have some drawbacks in addition to the
  -- benefit of seeing approaching danger up to almost a turn faster.
  -- It may be too late to block then, but not too late to sidestep or attack.
  unless (mwait == Just True) $ overheadActorTime fid (blid b)
  advanceTime aid (if mwait == Just False then 10 else 100) True
  handleRequestTimedCases aid cmd
  managePerRequest aid
  -- Note that due to the order, actor was still braced or sleeping
  -- throughout request processing, etc. So, if he hits himself kinetically,
  -- his armor from bracing previous turn is still in effect.
  processWatchfulness mwait aid
  return $! isNothing mwait  -- for speed, we report if @cmd@ harmless

-- | Clear deltas for Calm and HP for proper UI display and AI hints.
managePerRequest :: MonadServerAtomic m => ActorId -> m ()
managePerRequest aid = do
  b <- getsState $ getActorBody aid
  affectStash b
  let clearMark = 0
  unless (bcalmDelta b == ResDelta (0, 0) (0, 0)) $
    -- Clear delta for the next actor move.
    execUpdAtomic $ UpdRefillCalm aid clearMark
  unless (bhpDelta b == ResDelta (0, 0) (0, 0)) $
    -- Clear delta for the next actor move.
    execUpdAtomic $ UpdRefillHP aid clearMark

handleRequestTimedCases :: MonadServerAtomic m
                        => ActorId -> RequestTimed -> m ()
handleRequestTimedCases aid cmd = case cmd of
  ReqMove target -> reqMove aid target
  ReqMelee target iid cstore -> reqMelee aid target iid cstore
  ReqDisplace target -> reqDisplace aid target
  ReqAlter tpos -> reqAlter aid tpos
  ReqWait -> reqWait aid
  ReqWait10 -> reqWait10 aid
  ReqYell -> reqYell aid
  ReqMoveItems l -> reqMoveItems aid l
  ReqProject p eps iid cstore -> reqProject aid p eps iid cstore
  ReqApply iid cstore -> reqApply aid iid cstore

switchLeader :: MonadServerAtomic m => FactionId -> ActorId -> m ()
{-# INLINE switchLeader #-}
switchLeader fid aidNew = do
  fact <- getsState $ (EM.! fid) . sfactionD
  bPre <- getsState $ getActorBody aidNew
  let mleader = gleader fact
      !_A1 = assert (Just aidNew /= mleader
                     && not (bproj bPre)
                     `blame` (aidNew, bPre, fid, fact)) ()
      !_A2 = assert (bfid bPre == fid
                     `blame` "client tries to move other faction actors"
                     `swith` (aidNew, bPre, fid, fact)) ()
  let banned = bannedPointmanSwitchBetweenLevels fact
  arena <- case mleader of
    Nothing -> return $! blid bPre
    Just leader -> do
      b <- getsState $ getActorBody leader
      return $! blid b
  if blid bPre /= arena && banned  -- catch the cheating clients
  then execFailure aidNew ReqWait{-hack-} NoChangeDunLeader
  else do
    execUpdAtomic $ UpdLeadFaction fid mleader (Just aidNew)
    -- We exchange times of the old and new leader.
    -- This permits an abuse, because a slow tank can be moved fast
    -- by alternating between it and many fast actors (until all of them
    -- get slowed down by this and none remain). But at least the sum
    -- of all times of a faction is conserved. And we avoid double moves
    -- against the UI player caused by his leader changes. There may still
    -- happen double moves caused by AI leader changes, but that's rare.
    -- The flip side is the possibility of multi-moves of the UI player
    -- as in the case of the tank.
    -- Warning: when the action is performed on the server,
    -- the time of the actor is different than when client prepared that
    -- action, so any client checks involving time should discount this.
    case mleader of
      Just aidOld | aidOld /= aidNew -> swapTime aidOld aidNew
      _ -> return ()

-- * ReqMove

-- | Add a smell trace for the actor to the level. If smell already there
-- and the actor can smell, remove smell. Projectiles are ignored.
-- As long as an actor can smell, he doesn't leave any smell ever.
-- Smell trace is never left in water tiles.
affectSmell :: MonadServerAtomic m => ActorId -> m ()
affectSmell aid = do
  COps{coTileSpeedup} <- getsState scops
  b <- getsState $ getActorBody aid
  lvl <- getLevel $ blid b
  let aquatic = Tile.isAquatic coTileSpeedup $ lvl `at` bpos b
  unless (bproj b || aquatic) $ do
    actorMaxSk <- getsState $ getActorMaxSkills aid
    let smellRadius = Ability.getSk Ability.SkSmell actorMaxSk
        hasOdor = Ability.getSk Ability.SkOdor actorMaxSk > 0
    when (hasOdor || smellRadius > 0) $ do
      localTime <- getsState $ getLocalTime $ blid b
      let oldS = fromMaybe timeZero $ EM.lookup (bpos b) . lsmell $ lvl
          newTime = timeShift localTime smellTimeout
          newS = if smellRadius > 0
                 then timeZero
                 else newTime
      when (oldS /= newS) $
        execUpdAtomic $ UpdAlterSmell (blid b) (bpos b) oldS newS

-- | Actor moves or attacks or alters by bumping.
-- Note that client may not be able to see an invisible monster
-- so it's the server that determines if melee took place, etc.
-- Also, only the server is authorized to check if a move is legal
-- and it needs full context for that, e.g., the initial actor position
-- to check if melee attack does not try to reach to a distant tile.
reqMove :: MonadServerAtomic m => ActorId -> Vector -> m ()
reqMove = reqMoveGeneric True True

reqMoveGeneric :: MonadServerAtomic m
               => Bool -> Bool -> ActorId -> Vector -> m ()
reqMoveGeneric voluntary mayAttack source dir = do
  COps{coTileSpeedup} <- getsState scops
  actorSk <- currentSkillsServer source
  sb <- getsState $ getActorBody source
  let abInSkill sk = isJust (btrajectory sb)
                     || Ability.getSk sk actorSk > 0
      lid = blid sb
  lvl <- getLevel lid
  let spos = bpos sb
      tpos = spos `shift` dir
  -- This predicate is symmetric wrt source and target, though the effect
  -- of collision may not be (the source projectiles applies its effect
  -- on the target particles, but loses 1 HP due to the collision).
  -- The condition implies that it's impossible to shoot down a bullet
  -- with a bullet, but a bullet can shoot down a burstable target,
  -- as well as be swept away by it, and two burstable projectiles
  -- burst when meeting mid-air. Projectiles that are not bursting
  -- nor damaging never collide with any projectile.
  collides <- getsState $ \s tb ->
    let sitemKind = getIidKindServer (btrunk sb) s
        titemKind = getIidKindServer (btrunk tb) s
        sar = sdiscoAspect s EM.! btrunk sb
        tar = sdiscoAspect s EM.! btrunk tb
        -- Such projectiles are prone to bursting or are themselves
        -- particles of an explosion shockwave.
        bursting arItem =
          IA.checkFlag Ability.Fragile arItem
          && IA.checkFlag Ability.Lobable arItem
        sbursting = bursting sar
        tbursting = bursting tar
        -- Such projectiles, even if not bursting themselves, can cause
        -- another projectile to burst.
        sdamaging = IK.isDamagingKind sitemKind
        tdamaging = IK.isDamagingKind titemKind
        -- Avoid explosion extinguishing itself via its own particles colliding.
        sameBlast = IA.checkFlag Ability.Blast sar
                    && getIidKindIdServer (btrunk sb) s
                       == getIidKindIdServer (btrunk tb) s
    in not sameBlast
       && (sbursting && (tdamaging || tbursting)
           || (tbursting && (sdamaging || sbursting)))
  -- We start by checking actors at the target position.
  tgt <- getsState $ posToAidAssocs tpos lid
  case tgt of
    (target, tb) : _ | mayAttack && (not (bproj sb)
                                     || not (bproj tb)
                                     || collides tb) -> do
      -- A projectile is too small and insubstantial to hit another projectile,
      -- unless it's large enough or tends to explode (fragile and lobable).
      -- The actor in the way is visible or not; server sees him always.
      -- Below the only weapon (the only item) of projectiles is picked.
      mweapon <- pickWeaponServer source target
      case mweapon of
        Just (wp, cstore) | abInSkill Ability.SkMelee ->
          reqMeleeChecked voluntary source target wp cstore
        _ -> return ()  -- waiting, even if no @SkWait@ skill
      -- Movement of projectiles only happens after melee and a check
      -- if they survive, so that if they don't, they explode in front
      -- of enemy, not under him, so that already first explosion blasts
      -- reach him, not only potential secondary explosions.
      when (bproj sb) $ do
        b2 <- getsState $ getActorBody source
        unless (actorDying b2) $ reqMoveGeneric voluntary False source dir
    _ ->
      -- Either the position is empty, or all involved actors are proj.
      -- Movement requires full access and skill.
      if Tile.isWalkable coTileSpeedup $ lvl `at` tpos then
        if abInSkill Ability.SkMove then do
          execUpdAtomic $ UpdMoveActor source spos tpos
          affectSmell source
          -- No remote ransacking nor underfoot effects by projectiles,
          -- through which a projectile could cook its only item,
          -- but retain the old raw name and which would spam water
          -- slowness every time a projectile flies over water.
          unless (bproj sb) $
            -- Counts as bumping, because terrain transformation probably
            -- not intended, because the goal was probably just to move
            -- and then modifying the terrain is an unwelcome side effect.
            -- Barged into a tile, so normal effects need to activate,
            -- while crafting requires explicit altering.
            void $ reqAlterFail True EffBare voluntary source tpos
       else execFailure source (ReqMove dir) MoveUnskilled
      else do
        -- If not walkable, this must be altering by bumping.
        -- If voluntary then probably intentional so report any errors.
        mfail <- reqAlterFail True EffBare voluntary source tpos
        when voluntary $ do
          let req = ReqMove dir
          maybe (return ()) (execFailure source req) mfail

-- * ReqMelee

-- | Resolves the result of an actor moving into another.
-- Actors on unwalkable positions can be attacked without any restrictions.
-- For instance, an actor embedded in a wall can be attacked from
-- an adjacent position. This function is analogous to projectGroupItem,
-- but for melee and not using up the weapon.
-- No problem if there are many projectiles at the spot. We just
-- attack the one specified.
reqMelee :: MonadServerAtomic m
         => ActorId -> ActorId -> ItemId -> CStore -> m ()
reqMelee source target iid cstore = do
  actorSk <- currentSkillsServer source
  if Ability.getSk Ability.SkMelee actorSk > 0 then
    reqMeleeChecked True source target iid cstore
  else execFailure source (ReqMelee target iid cstore) MeleeUnskilled

reqMeleeChecked :: forall m. MonadServerAtomic m
                => Bool -> ActorId -> ActorId -> ItemId -> CStore -> m ()
reqMeleeChecked voluntary source target iid cstore = do
  sb <- getsState $ getActorBody source
  tb <- getsState $ getActorBody target
  discoAspect <- getsState sdiscoAspect
  let req = ReqMelee target iid cstore
      arWeapon = discoAspect EM.! iid
      meleeableEnough = bproj sb || IA.checkFlag Ability.Meleeable arWeapon
  if source == target then execFailure source req MeleeSelf
  else if not (checkAdjacent sb tb) then execFailure source req MeleeDistant
  else if not meleeableEnough then execFailure source req MeleeNotWeapon
  else do
    -- If @voluntary@ is set, blame is exact, otherwise, an approximation.
    killer <- if | voluntary -> assert (not (bproj sb)) $ return source
                 | bproj sb -> getsServer $ EM.findWithDefault source source
                               . strajPushedBy
                 | otherwise -> return source
    actorSk <- currentSkillsServer source
    let arTrunk = discoAspect EM.! btrunk tb
        sfid = bfid sb
        tfid = bfid tb
        -- Let the missile drop down, but don't remove its trajectory
        -- so that it doesn't pretend to have hit a wall.
        haltTrajectory :: KillHow -> ActorId -> Actor -> m ()
        haltTrajectory killHow aid b = case btrajectory b of
          btra@(Just (l, speed)) | not $ null l -> do
            execUpdAtomic $ UpdTrajectory aid btra $ Just ([], speed)
            let arTrunkAid = discoAspect EM.! btrunk b
            when (bproj b && not (IA.checkFlag Ability.Blast arTrunkAid)) $
              addKillToAnalytics killer killHow (bfid b) (btrunk b)
          _ -> return ()
    -- Only catch if braced. Never steal trunk from an already caught
    -- projectile or one with many items inside.
    if bproj tb
       && EM.size (beqp tb) == 1
       && not (IA.checkFlag Ability.Blast arTrunk)
       && actorWaits sb  -- still valid while request being processed
       && Ability.getSk Ability.SkMoveItem actorSk > 0  -- animals can't
    then do
      -- Catching the projectile, that is, stealing the item from its eqp.
      -- No effect from our weapon (organ) is applied to the projectile
      -- and the weapon (organ) is never destroyed, even if not durable.
      -- Pushed actor doesn't stop flight by catching the projectile
      -- nor does he lose 1HP.
      -- This is not overpowered, because usually at least one partial wait
      -- is needed to sync (if not, attacker should switch missiles)
      -- and so only every other missile can be caught. Normal sidestepping
      -- or sync and displace, if in a corridor, is as effective
      -- and blocking can be even more so, depending on powers of the missile.
      -- Missiles are really easy to defend against, but sight (and so, Calm)
      -- is the key, as well as light, ambush around a corner, etc.
      execSfxAtomic $ SfxSteal source target iid
      case EM.assocs $ beqp tb of
        [(iid2, (k, _))] -> do
          upds <- generalMoveItem True iid2 k (CActor target CEqp)
                                              (CActor source CStash)
          mapM_ execUpdAtomic upds
          itemFull <- getsState $ itemToFull iid2
          discoverIfMinorEffects (CActor source CStash)
                                 iid2 (itemKindId itemFull)
        err -> error $ "" `showFailure` err
      haltTrajectory KillCatch target tb
    else do
      if bproj sb && bproj tb then do
        -- Special case for collision of projectiles, because they just
        -- symmetrically ram into each other, so picking one to hit another,
        -- based on random timing, would be wrong.
        -- Instead of suffering melee attack, let the target projectile
        -- get smashed and burst (if fragile and if not piercing).
        -- The source projectile terminates flight (unless pierces) later on.
        when (bhp tb > oneM) $
          execUpdAtomic $ UpdRefillHP target minusM
        when (bhp tb <= oneM) $ do
          -- If projectile has too low HP to pierce, terminate its flight.
          let killHow | IA.checkFlag Ability.Blast arWeapon = KillKineticBlast
                      | otherwise = KillKineticRanged
          haltTrajectory killHow target tb
        -- Avoid spam when two explosions collide.
        unless (IA.checkFlag Ability.Blast arWeapon
                && IA.checkFlag Ability.Blast arTrunk) $
          execSfxAtomic $ SfxStrike source target iid
      else do
        -- Normal hit, with effects, but first auto-apply defences.
        let mayDestroyTarget = not (bproj tb) || bhp tb <= oneM
            effApplyFlagsTarget = EffApplyFlags
              { effToUse            = EffBare
              , effVoluntary        = voluntary
              , effUseAllCopies     = False
              , effKineticPerformed = False
              , effActivation       = if bproj sb
                                      then Ability.ActivationUnderRanged
                                      else Ability.ActivationUnderMelee
              , effMayDestroy       = mayDestroyTarget
              }
        unless (bproj tb) $
          autoApply effApplyFlagsTarget killer target tb
          $ if bproj sb then Ability.UnderRanged else Ability.UnderMelee
        -- This might have changed the actors.
        sb2 <- getsState $ getActorBody source
        targetMaxSk <- getsState $ getActorMaxSkills target
        if | bproj sb2
             && Ability.getSk Ability.SkDeflectRanged targetMaxSk > 0 -> do
               cutCalm target
               execSfxAtomic $ SfxRecoil source target iid
           | Ability.getSk Ability.SkDeflectMelee targetMaxSk > 0 -> do
               cutCalm target
               execSfxAtomic $ SfxRecoil source target iid
           | otherwise -> do
               -- Msgs inside @SfxStrike@ describe the source part
               -- of the strike.
               execSfxAtomic $ SfxStrike source target iid
               let c = CActor source cstore
                   mayDestroySource = not (bproj sb2) || bhp sb2 <= oneM
                     -- piercing projectiles may not have their weapon destroyed
               -- Msgs inside @itemEffect@ describe the target part
               -- of the strike.
               -- If any effects and aspects, this is also where they are
               -- identified.
               -- Here also the kinetic damage is applied,
               -- before any effects are.
               --
               -- Note: that "hornet swarm detect items" via a scrolls
               -- is intentional,
               -- even though unrealistic and funny. Otherwise actors
               -- could protect
               -- themselves from some projectiles by lowering their apply stat.
               -- Also, the animal faction won't have too much benefit
               -- from that info,
               -- so the problem is not balance, but the goofy message.
               let effApplyFlagsSource = EffApplyFlags
                     { effToUse            = EffBare
                     , effVoluntary        = voluntary
                     , effUseAllCopies     = False
                     , effKineticPerformed = False
                     , effActivation       = Ability.ActivationMeleeable
                     , effMayDestroy       = mayDestroySource
                     }
               void $ kineticEffectAndDestroy effApplyFlagsSource killer
                                              source target iid c
      sb2 <- getsState $ getActorBody source
      case btrajectory sb2 of
        Just{} | not voluntary -> do
          -- Deduct a hitpoint for a pierce of a projectile
          -- or due to a hurled actor colliding with another (seen from
          -- @voluntary@, as opposed to hurled actor actively meleeing another).
          -- Don't deduct if no pierce, to prevent spam.
          -- Never kill in this way.
          when (bhp sb2 > oneM) $ do
            execUpdAtomic $ UpdRefillHP source minusM
            unless (bproj sb2) $ do
              execSfxAtomic $
                SfxMsgFid (bfid sb2) $ SfxCollideActor source target
              unless (bproj tb) $
                execSfxAtomic $
                  SfxMsgFid (bfid tb) $ SfxCollideActor source target
          when (not (bproj sb2) || bhp sb2 <= oneM) $
            -- Non-projectiles can't pierce, so terminate their flight.
            -- If projectile has too low HP to pierce, ditto.
            haltTrajectory KillActorLaunch source sb2
        _ -> return ()
      -- The only way to start a war is to slap an enemy voluntarily..
      -- Being hit by and hitting projectiles, as well as via pushing,
      -- count as unintentional friendly fire.
      sfact <- getsState $ (EM.! sfid) . sfactionD
      let friendlyFire = bproj sb2 || bproj tb || not voluntary
          fromDipl = EM.findWithDefault Unknown tfid (gdipl sfact)
      unless (friendlyFire
              || isFoe sfid sfact tfid  -- already at war
              || isFriend sfid sfact tfid) $  -- allies never at war
        execUpdAtomic $ UpdDiplFaction sfid tfid fromDipl War

autoApply :: MonadServerAtomic m
          => EffApplyFlags -> ActorId -> ActorId -> Actor -> Ability.Flag
          -> m ()
autoApply effApplyFlags killer target tb flag = do
  let autoApplyIid c iid = do
        itemFull <- getsState $ itemToFull iid
        let arItem = aspectRecordFull itemFull
        when (IA.checkFlag flag arItem) $
          void $ effectAndDestroyAndAddKill effApplyFlags killer target target
                                            iid c itemFull
  mapM_ (autoApplyIid $ CActor target CEqp) $ EM.keys $ beqp tb
  mapM_ (autoApplyIid $ CActor target COrgan) $ EM.keys $ borgan tb

-- * ReqDisplace

-- | Actor tries to swap positions with another.
reqDisplace :: MonadServerAtomic m => ActorId -> ActorId -> m ()
reqDisplace = reqDisplaceGeneric True

reqDisplaceGeneric :: MonadServerAtomic m => Bool -> ActorId -> ActorId -> m ()
reqDisplaceGeneric voluntary source target = do
  COps{coTileSpeedup} <- getsState scops
  actorSk <- currentSkillsServer source
  sb <- getsState $ getActorBody source
  let abInSkill sk = isJust (btrajectory sb)
                     || Ability.getSk sk actorSk > 0
  tb <- getsState $ getActorBody target
  tfact <- getsState $ (EM.! bfid tb) . sfactionD
  let spos = bpos sb
      tpos = bpos tb
      atWar = isFoe (bfid tb) tfact (bfid sb)
      req = ReqDisplace target
  actorMaxSk <- getsState $ getActorMaxSkills target
  dEnemy <- getsState $ dispEnemy source target actorMaxSk
  if | not (abInSkill Ability.SkDisplace) ->
         execFailure source req DisplaceUnskilled
     | not (checkAdjacent sb tb) -> execFailure source req DisplaceDistant
     | atWar && not dEnemy -> do  -- if not at war, can displace always
       -- We don't fail with DisplaceImmobile and DisplaceSupported.
       -- because it's quite common they can't be determined by the attacker,
       -- and so the failure would be too alarming to the player.
       -- If the character melees instead, the player can tell displace failed.
       -- As for the other failures, they are impossible and we don't
       -- verify here that they don't occur, for simplicity.
       mweapon <- pickWeaponServer source target
       case mweapon of
         Just (wp, cstore) | abInSkill Ability.SkMelee ->
           reqMeleeChecked voluntary source target wp cstore
         _ -> return ()  -- waiting, even if no @SkWait@ skill
     | otherwise -> do
       let lid = blid sb
       lvl <- getLevel lid
       -- Displacing requires full access.
       if Tile.isWalkable coTileSpeedup $ lvl `at` tpos then
         case posToAidsLvl tpos lvl of
           [] -> error $ "" `showFailure` (source, sb, target, tb)
           [_] -> do
             execUpdAtomic $ UpdDisplaceActor source target
             -- We leave or wipe out smell, for consistency, but it's not
             -- absolute consistency, e.g., blinking doesn't touch smell,
             -- so sometimes smellers will backtrack once to wipe smell. OK.
             affectSmell source
             affectSmell target
             -- Counts as bumping, because terrain transformation not intended.
             void $ reqAlterFail True EffBare False source tpos
               -- possibly alter or activate
             void $ reqAlterFail True EffBare False target spos
           _ -> execFailure source req DisplaceMultiple
       else
         -- Client foolishly tries to displace an actor without access.
         execFailure source req DisplaceAccess

-- * ReqAlter

-- | Search and/or alter the tile.
reqAlter :: MonadServerAtomic m => ActorId -> Point -> m ()
reqAlter source tpos = do
  COps{coTileSpeedup} <- getsState scops
  sb <- getsState $ getActorBody source
  lvl <- getLevel $ blid sb
  -- This is explicit tile triggering. Walkable tiles are sparse enough
  -- that crafting effects can be activated without any others
  -- and without changing the tile and this is usually beneficial,
  -- so always attempted. OTOH, squeezing a hand into a non-walkable tile
  -- or barging into walkable tiles (but not as a projectile) activates all.
  let effToUse = if Tile.isWalkable coTileSpeedup (lvl `at` tpos)
                 then EffOnCombine
                 else EffBareAndOnCombine
  mfail <- reqAlterFail False effToUse True source tpos
  let req = ReqAlter tpos
  maybe (return ()) (execFailure source req) mfail

reqAlterFail :: forall m. MonadServerAtomic m
             => Bool -> EffToUse -> Bool -> ActorId -> Point
             -> m (Maybe ReqFailure)
reqAlterFail bumping effToUse voluntary source tpos = do
  cops@COps{cotile, coTileSpeedup, corule} <- getsState scops
  sb <- getsState $ getActorBody source
  actorMaxSk <- getsState $ getActorMaxSkills source
  let calmE = calmEnough sb actorMaxSk
      lid = blid sb
  sClient <- getsServer $ (EM.! bfid sb) . sclientStates
  itemToF <- getsState $ flip itemToFull
  actorSk <- currentSkillsServer source
  localTime <- getsState $ getLocalTime lid
  embeds <- getsState $ getEmbedBag lid tpos
  lvl <- getLevel lid
  getKind <- getsState $ flip getIidKindServer
  let serverTile = lvl `at` tpos
      lvlClient = (EM.! lid) . sdungeon $ sClient
      clientTile = lvlClient `at` tpos
      hiddenTile = Tile.hideAs cotile serverTile
      alterSkill = Ability.getSk Ability.SkAlter actorSk
      tileMinSkill = Tile.alterMinSkill coTileSpeedup serverTile
      revealEmbeds = unless (EM.null embeds) $
        execUpdAtomic $ UpdSpotItemBag True (CEmbed lid tpos) embeds
      embedKindList =
        map (\(iid, kit) -> (getKind iid, (iid, kit))) (EM.assocs embeds)
      sbItemKind = getKind $ btrunk sb
      -- Prevent embeds triggering each other's exploding embeds
      -- via feeble mists, in the worst case, in a loop. However,
      -- if a tile can be changed with an item (e.g., the mist trunk)
      -- but without activating embeds, mists do fine.
      projNoDamage = bproj sb && not (IK.isDamagingKind sbItemKind)
      tryApplyEmbed (iid, kit) = do
        let itemFull = itemToF iid
            -- Let even completely apply-unskilled actors trigger basic embeds.
            -- See the note about no skill check when melee triggers effects.
            legal = permittedApply corule localTime maxBound calmE Nothing
                                   itemFull kit
        case legal of
          Left ApplyNoEffects -> return UseDud  -- pure flavour embed
          Left reqFail -> do
            -- The failure is fully expected, because client may choose
            -- to trigger some embeds, knowing that others won't fire.
            execSfxAtomic $ SfxMsgFid (bfid sb)
                          $ SfxExpectedEmbed iid lid reqFail
            return UseDud
          _ -> itemEffectEmbedded effToUse voluntary source lid tpos iid
                 -- when @effToUse == EffOnCombine@, terrain, e.g., fire,
                 -- may be removed safely, without adverse effects
                 -- by crafting, even any silly crafting as an exploit; OK
      underFeet = tpos == bpos sb  -- if enter and alter, be more permissive
      blockedByItem = EM.member tpos (lfloor lvl)
  if chessDist tpos (bpos sb) > 1
  then return $ Just AlterDistant
  else if Just clientTile == hiddenTile then  -- searches
    -- Only non-projectile actors with SkAlter > 1 can search terrain.
    -- Even projectiles with large altering bonuses can't.
    if bproj sb || not underFeet && alterSkill <= 1
    then return $ Just AlterUnskilled  -- don't leak about searching
    else do
      -- Blocking by items nor actors does not prevent searching.
      -- Searching broadcasted, in case actors from other factions are present
      -- so that they can learn the tile and learn our action.
      -- If they already know the tile, they will just consider our action
      -- a waste of time and ignore the command.
      execUpdAtomic $ UpdSearchTile source tpos serverTile
      -- Searching also reveals the embedded items of the tile.
      -- If the items are already seen by the client
      -- (e.g., due to item detection, despite tile being still hidden),
      -- the command is ignored on the client.
      revealEmbeds
      -- If the entries are already seen by the client
      -- the command is ignored on the client.
      case EM.lookup tpos $ lentry lvl of
        Nothing -> return ()
        Just entry -> execUpdAtomic $ UpdSpotEntry lid [(tpos, entry)]
      -- Seaching triggers the embeds as well, after they are revealed.
      -- The rationale is that the items were all the time present
      -- (just invisible to the client), so they need to be triggered.
      -- The exception is changable tiles, because they are not so easy
      -- to trigger; they need previous or subsequent altering.
      unless (Tile.isModifiable coTileSpeedup serverTile || projNoDamage) $
        mapM_ (void <$> tryApplyEmbed)
              (sortEmbeds cops serverTile embedKindList)
      return Nothing  -- searching is always success
  else
    -- Here either @clientTile == serverTile@ or the client
    -- is misguided re tile at that position, e.g., it is a projectile
    -- that can't see the tile and the tile was not revealed so far.
    -- In either case, try to alter the tile. If the messages
    -- are confusing, that's fair, situation is confusing.
    if not (bproj sb || underFeet)  -- no global skill check in these cases
       && alterSkill < tileMinSkill
    then return $ Just AlterUnskilled  -- don't leak about altering
    else do
      -- Save the original content of ground and eqp to abort transformations
      -- if any item is removed, possibly an item intended as the fuel.
      groundBag <- getsState $ getBodyStoreBag sb CGround
      eqpBag <- getsState $ getBodyStoreBag sb CEqp
      -- Compute items to use for transformation early, before any extra
      -- items added by activated embeds, to use only intended items as fuel.
      -- Use even unidentified items --- one more way to id by use.
      kitAssG <- getsState $ kitAssocs source [CGround]
      kitAssE <- getsState $ kitAssocs source [CEqp]
      let kitAss = listToolsToConsume kitAssG kitAssE
          announceTileChange =
            -- If no embeds and the only thing that happens is the change
            -- of the tile, don't display a message, because the change
            -- is visible on the map (unless it changes into itself)
            -- and there's nothing more to speak about.
            -- However, even with embeds, don't spam if wading through
            -- terrain and changing it each step.
            unless (underFeet || EM.null embeds) $
              execSfxAtomic $ SfxTrigger source lid tpos serverTile
          changeTo tgroup = do
            -- No @SfxAlter@, because the effect is obvious (e.g., opened door).
            let nightCond kt = not (Tile.kindHasFeature TK.Walkable kt
                                    && Tile.kindHasFeature TK.Clear kt)
                               || (if lnight lvl then id else not)
                                    (Tile.kindHasFeature TK.Dark kt)
            -- Sometimes the tile is determined precisely by the ambient light
            -- of the source tiles. If not, default to cave day/night condition.
            mtoTile <- rndToAction $ opick cotile tgroup nightCond
            toTile <- maybe (rndToAction
                             $ fromMaybe (error $ "" `showFailure` tgroup)
                               <$> opick cotile tgroup (const True))
                            return
                            mtoTile
            embeds2 <- getsState $ getEmbedBag lid tpos
            let newHasEmbeds = Tile.isEmbed coTileSpeedup toTile
            -- Don't regenerate same tile, unless it had embeds, but all spent.
            when (serverTile /= toTile
                  || EM.null embeds2 && newHasEmbeds) $ do
              -- At most one of these two will be accepted on any given client.
              when (serverTile /= toTile) $
                execUpdAtomic $ UpdAlterTile lid tpos serverTile toTile
              -- This case happens when a client does not see a searching
              -- action by another faction, but sees the subsequent altering
              -- or if another altering takes place in between.
              case hiddenTile of
                Just tHidden ->
                  execUpdAtomic $ UpdAlterTile lid tpos tHidden toTile
                Nothing -> return ()
              -- @UpdAlterExplorable@ is received by any client regardless
              -- of whether the alteration was seen and how.
              case (Tile.isExplorable coTileSpeedup serverTile,
                    Tile.isExplorable coTileSpeedup toTile) of
                (False, True) -> execUpdAtomic $ UpdAlterExplorable lid 1
                (True, False) -> execUpdAtomic $ UpdAlterExplorable lid (-1)
                _ -> return ()
              -- At the end we replace old embeds (even if partially used up)
              -- with new ones.
              -- If the source tile was hidden, the items could not be visible
              -- on a client, in which case the command would be ignored
              -- on the client, without causing any problems. Otherwise,
              -- if the position is in view, client has accurate info.
              unless (EM.null embeds2) $
                execUpdAtomic $ UpdLoseItemBag True (CEmbed lid tpos) embeds2
              -- Altering always reveals the outcome tile, so it's not hidden
              -- and so its embedded items are always visible.
              embedItemOnPos lid tpos toTile
          tryChangeWith :: ( [(Int, GroupName IK.ItemKind)]
                           , GroupName TK.TileKind )
                        -> m Bool
          tryChangeWith (tools0, tgroup) = do
            let grps0 = map (\(x, y) -> (False, x, y)) tools0
                  -- apply if durable
                (bagsToLose, iidsToApply, grps) =
                  foldl' subtractIidfromGrps (EM.empty, [], grps0) kitAss
            if null grps then do
              announceTileChange  -- first the result is foretold
              consumeItems source bagsToLose iidsToApply  -- then the cost
              changeTo tgroup  -- then result is seen
              return True
            else return False
          feats = TK.tfeature $ okind cotile serverTile
          tileActions =
            mapMaybe (parseTileAction
                        (bproj sb)
                        (underFeet || blockedByItem)  -- avoids AlterBlockItem
                        embedKindList)
                     feats
          groupWithFromAction action = case action of
            WithAction grps _ | not bumping -> Just grps
            _ -> Nothing
          groupsToAlterWith = mapMaybe groupWithFromAction tileActions
          processTileActions :: Maybe UseResult -> [TileAction] -> m Bool
          processTileActions museResult [] =
            return $! maybe False (/= UseDud) museResult
          processTileActions museResult (ta : rest) = case ta of
            EmbedAction (iid, kit) ->
              -- Embeds are activated in the order in tile definition
              -- and never after the tile is changed.
              -- If any embedded item was present and processed,
              -- but none was triggered, both free and item-consuming terrain
              -- alteration is disabled. The exception is projectiles
              -- not being able to process embeds due to skill required,
              -- which does not block future terrain alteration.
              -- Skill check for non-projectiles is performed much earlier.
              -- All projectiles have 0 skill for the purpose of embed
              -- activation, regardless of their trunk.
              if | bproj sb && tileMinSkill > 0 ->  -- local skill check
                   processTileActions museResult rest
                     -- not blocking future terrain altering, e.g., oil mist
                     -- not slowed over water tile that has @talter@ equal to 2,
                     -- but able to change it into oil spill soon after
                 | projNoDamage ->
                   processTileActions (Just UseDud) rest
                     -- projectiles having enough skill, but no damage,
                     -- not only can't activate embeds, but block future
                     -- terrain altering, e.g., oil mist not puncturing
                     -- a barrel and causing explosion, and so also
                     -- not causing it to disappear later on
                 | otherwise -> do
                     -- here falls the case of fragmentation blast puncturing
                     -- a barrel and so causing an explosion
                     triggered <- tryApplyEmbed (iid, kit)
                     let useResult = fromMaybe UseDud museResult
                     processTileActions (Just $ max useResult triggered) rest
                       -- max means that even one activated embed is enough
                       -- to alter terrain in a future action
            ToAction tgroup -> assert (not (bproj sb)) $
              -- @parseTileAction@ ensures the above assertion
              -- so that projectiles never cause normal transitions and,
              -- e.g., mists douse fires or two flames thrown, first ignites,
              -- second douses immediately afterwards
              if maybe True (== UseUp) museResult
              then do
                announceTileChange
                changeTo tgroup
                return True
              else processTileActions museResult rest
            WithAction grps tgroup -> do
              -- Note that there is no skill check if the source actors
              -- is a projectile. Permission is conveyed in @ProjYes@ instead.
              groundBag2 <- getsState $ getBodyStoreBag sb CGround
              eqpBag2 <- getsState $ getBodyStoreBag sb CEqp
              if (not bumping || null grps)
                   -- 'M' confirmation needed to consume items, bump not enough
                 && (bproj sb || voluntary || null grps)
                       -- consume only if voluntary or released as projectile
                 && (maybe True (== UseUp) museResult
                     || effToUse == EffOnCombine)
                          -- unwanted crafting shouldn't block transformations
                 && let f (k1, _) (k2, _) = k1 <= k2
                    in EM.isSubmapOfBy f groundBag groundBag2
                       && EM.isSubmapOfBy f eqpBag eqpBag2
                      -- don't transform if items, possibly intended for
                      -- transformation, removed; also when only crafting
                      -- was intended, which almost always removes some items
              then do
                altered <- tryChangeWith (grps, tgroup)
                if altered
                then return True
                else processTileActions museResult rest
              else processTileActions museResult rest
      -- Note that stray embedded items (not from tile content definition)
      -- are never activated.
      if null tileActions then
        return $! if blockedByItem
                     && not underFeet
                     && Tile.isModifiable coTileSpeedup serverTile
                  then Just AlterBlockItem  -- likely cause
                  else Just AlterNothing  -- can't do; silly client; fail
      else
        if underFeet || not (occupiedBigLvl tpos lvl)
                        && not (occupiedProjLvl tpos lvl) then do
          -- The items are first revealed for the sake of clients that
          -- may see the tile as hidden. Note that the tile is not revealed
          -- (unless it's altered later on, in which case the new one is).
          revealEmbeds
          tileTriggered <- processTileActions Nothing tileActions
          let potentiallyMissing = filter (not . null) groupsToAlterWith
          when (not tileTriggered && not underFeet && voluntary
                && not (null potentiallyMissing)) $
            execSfxAtomic $ SfxMsgFid (bfid sb)
                          $ SfxNoItemsForTile potentiallyMissing
          return Nothing  -- altered as much as items allowed; success
        else return $ Just AlterBlockActor

-- * ReqWait

-- | Do nothing. Wait skill 1 required. Bracing requires 2, sleep 3, lurking 4.
--
-- Something is sometimes done in 'processWatchfulness'.
reqWait :: MonadServerAtomic m => ActorId -> m ()
{-# INLINE reqWait #-}
reqWait source = do
  actorSk <- currentSkillsServer source
  unless (Ability.getSk Ability.SkWait actorSk > 0) $
    execFailure source ReqWait WaitUnskilled

-- * ReqWait10

-- | Do nothing.
--
-- Something is sometimes done in 'processWatchfulness'.
reqWait10 :: MonadServerAtomic m => ActorId -> m ()
{-# INLINE reqWait10 #-}
reqWait10 source = do
  actorSk <- currentSkillsServer source
  unless (Ability.getSk Ability.SkWait actorSk >= 4) $
    execFailure source ReqWait10 WaitUnskilled

-- * ReqYell

-- | Yell/yawn/stretch/taunt.
-- Wakes up (gradually) from sleep. Causes noise heard by enemies on the level
-- even if out of their hearing range.
--
-- Governed by the waiting skill (because everyone is supposed to have it).
-- unlike @ReqWait@, induces overhead.
--
-- This is similar to the effect @Yell@, but always voluntary.
reqYell :: MonadServerAtomic m => ActorId -> m ()
reqYell aid = do
  actorSk <- currentSkillsServer aid
  if | Ability.getSk Ability.SkWait actorSk > 0 ->
       -- Last yawn before waking up is displayed as a yell, but that's fine.
       -- To fix that, we'd need to move the @SfxTaunt@
       -- to @processWatchfulness@.
       execSfxAtomic $ SfxTaunt True aid
     | Ability.getSk Ability.SkMove actorSk <= 0
       || Ability.getSk Ability.SkDisplace actorSk <= 0
       || Ability.getSk Ability.SkMelee actorSk <= 0 ->
       -- Potentially, only waiting is possible, so given that it's drained,
       -- don't let the actor be stuck nor alarm about server failure.
       execSfxAtomic $ SfxTaunt False aid
     | otherwise -> do
       -- In most situation one of the 3 actions above
       -- can be performed and waiting skill is not needed for that,
       -- so given the 3 skills are available, waste turn, waiting until
       -- they can be performed, but don't alarm, because it does happen
       -- sometimes in crowds. No bracing granted, either, but mark
       -- waiting so that AI knows to change leader.
       --   execFailure aid ReqYell YellUnskilled
       b <- getsState $ getActorBody aid
       case bwatch b of
         WWait _ -> return ()
         _ -> execUpdAtomic $ UpdWaitActor aid (bwatch b) (WWait 0)

-- * ReqMoveItems

reqMoveItems :: MonadServerAtomic m
             => ActorId -> [(ItemId, Int, CStore, CStore)] -> m ()
reqMoveItems source l = do
  actorSk <- currentSkillsServer source
  if Ability.getSk Ability.SkMoveItem actorSk > 0 then do
    b <- getsState $ getActorBody source
    actorMaxSk <- getsState $ getActorMaxSkills source
    -- Server accepts item movement based on calm at the start, not end
    -- or in the middle, to avoid interrupted or partially ignored commands.
    let calmE = calmEnough b actorMaxSk
    case l of
      [] -> execFailure source (ReqMoveItems l) ItemNothing
      iid : rest -> do
        reqMoveItem False source calmE iid
        -- Dropping previous may destroy next items.
        mapM_ (reqMoveItem True source calmE) rest
  else execFailure source (ReqMoveItems l) MoveItemUnskilled

reqMoveItem :: MonadServerAtomic m
            => Bool -> ActorId -> Bool -> (ItemId, Int, CStore, CStore) -> m ()
reqMoveItem absentPermitted aid calmE (iid, kOld, fromCStore, toCStore) = do
  b <- getsState $ getActorBody aid
  let fromC = CActor aid fromCStore
      req = ReqMoveItems [(iid, kOld, fromCStore, toCStore)]
  toC <- case toCStore of
    CGround -> pickDroppable False aid b  -- drop over fog, etc.
    _ -> return $! CActor aid toCStore
  bagFrom <- getsState $ getContainerBag (CActor aid fromCStore)
  bagBefore <- getsState $ getContainerBag toC
  -- The effect of dropping previous items from this series may have
  -- increased or decreased the number of this item.
  let k = min kOld $ fst $ EM.findWithDefault (0, []) iid bagFrom
  let !_A = absentPermitted || k == kOld
  if
   | absentPermitted && k == 0 -> return ()
   | k < 1 || fromCStore == toCStore -> execFailure aid req ItemNothing
   | fromCStore == CEqp && not calmE ->
     execFailure aid req ItemNotCalm
   | toCStore == CEqp && not calmE ->
     execFailure aid req ItemNotCalm
   | toCStore == CEqp && eqpOverfull b k ->
     execFailure aid req EqpOverfull
   | otherwise -> do
    upds <- generalMoveItem True iid k fromC toC
    mapM_ execUpdAtomic upds
    itemFull <- getsState $ itemToFull iid
    -- Let any item manipulation attempt to identify, in case the item
    -- got into stash, e.g., by being thrown at the stash location,
    -- and gets identified only when equipped or dropped and picked up again.
    discoverIfMinorEffects toC iid (itemKindId itemFull)
    -- The first recharging period after equipping is random,
    -- between 1 and 2 standard timeouts of the item.
    -- Timeouts for items in shared stash are not consistent wrt the actor's
    -- local time, because actors from many levels put items there
    -- all the time (and don't rebase it to the clock of the stash's level).
    -- If wrong local time in shared stash causes an item to recharge
    -- for a very long time wrt actor on some level,
    -- the player can reset it by dropping the item and picking up again
    -- (as a flip side, a charging item in stash may sometimes
    -- be used at once on another level, with different local time, but only
    -- once, because after first use, the timeout is set to local time).
    -- This is not terribly consistent, but not recharging in stash is
    -- not better, because either we block activation of any items with timeout,
    -- or encourage moving items out of stash, recharging and moving in.
    -- Which is not fun at all, but one more thing to remember doing regularly.
    when (toCStore `elem` [CEqp, COrgan]
          && fromCStore `notElem` [CEqp, COrgan]
          || fromCStore == CStash) $ do
      let beforeIt = case iid `EM.lookup` bagBefore of
            Nothing -> []  -- no such items before move
            Just (_, it2) -> it2
      randomResetTimeout k iid itemFull beforeIt toC

-- * ReqProject

reqProject :: MonadServerAtomic m
           => ActorId    -- ^ actor projecting the item (is on current lvl)
           -> Point      -- ^ target position of the projectile
           -> Int        -- ^ digital line parameter
           -> ItemId     -- ^ the item to be projected
           -> CStore     -- ^ which store the items comes from
           -> m ()
reqProject source tpxy eps iid cstore = do
  let req = ReqProject tpxy eps iid cstore
  b <- getsState $ getActorBody source
  curChalSer <- getsServer $ scurChalSer . soptions
  fact <- getsState $ (EM.! bfid b) . sfactionD
  actorMaxSk <- getsState $ getActorMaxSkills source
  let calmE = calmEnough b actorMaxSk
  if | ckeeper curChalSer && fhasUI (gkind fact) ->
        execFailure source req ProjectFinderKeeper
     | cstore == CEqp && not calmE -> execFailure source req ItemNotCalm
     | otherwise -> do
         mfail <-
           projectFail source source (bpos b) tpxy eps False iid cstore False
         maybe (return ()) (execFailure source req) mfail

-- * ReqApply

reqApply :: MonadServerAtomic m
         => ActorId  -- ^ actor applying the item (is on current level)
         -> ItemId   -- ^ the item to be applied
         -> CStore   -- ^ the location of the item
         -> m ()
reqApply aid iid cstore = do
  COps{corule} <- getsState scops
  let req = ReqApply iid cstore
  b <- getsState $ getActorBody aid
  actorMaxSk <- getsState $ getActorMaxSkills aid
  let calmE = calmEnough b actorMaxSk
  if cstore == CEqp && not calmE then execFailure aid req ItemNotCalm
  else do
    bag <- getsState $ getBodyStoreBag b cstore
    case EM.lookup iid bag of
      Nothing -> execFailure aid req ApplyOutOfReach
      Just kit -> do
        itemFull <- getsState $ itemToFull iid
        actorSk <- currentSkillsServer aid
        localTime <- getsState $ getLocalTime (blid b)
        let skill = Ability.getSk Ability.SkApply actorSk
            legal = permittedApply corule localTime skill calmE (Just cstore)
                                   itemFull kit
        case legal of
          Left reqFail -> execFailure aid req reqFail
          Right _ -> applyItem aid iid cstore

-- * ReqGameRestart

reqGameRestart :: MonadServerAtomic m
               => ActorId -> GroupName ModeKind -> Challenge
               -> m ()
reqGameRestart aid groupName scurChalSer = do
  noConfirmsGame <- isNoConfirmsGame
  factionD <- getsState sfactionD
  let fidsUI = map fst $ filter (\(_, fact) -> fhasUI (gkind fact))
                                (EM.assocs factionD)
  -- This call to `revealItems` and `revealPerception` is really needed,
  -- because the other happens only at natural game conclusion,
  -- not at forced quitting.
  unless noConfirmsGame $
    mapM_ revealAll fidsUI
  -- Announcing end of game, we send lore, because game is over.
  b <- getsState $ getActorBody aid
  oldSt <- getsState $ gquit . (EM.! bfid b) . sfactionD
  factionAn <- getsServer sfactionAn
  generationAn <- getsServer sgenerationAn
  execUpdAtomic $ UpdQuitFaction
                    (bfid b)
                    oldSt
                    (Just $ Status Restart (fromEnum $ blid b) (Just groupName))
                    (Just (factionAn, generationAn))
  -- We don't save game and don't wait for clips end. ASAP.
  modifyServer $ \ser -> ser { sbreakASAP = True
                             , soptionsNxt = (soptionsNxt ser) {scurChalSer} }

-- * ReqGameDropAndExit

-- After we break out of the game loop, we will notice from @Camping@
-- we shouldn exit the game.
reqGameDropAndExit :: MonadServerAtomic m => ActorId -> m ()
reqGameDropAndExit aid = do
  verifyAssertExplored
  b <- getsState $ getActorBody aid
  oldSt <- getsState $ gquit . (EM.! bfid b) . sfactionD
  execUpdAtomic $ UpdQuitFaction
                    (bfid b)
                    oldSt
                    (Just $ Status Camping (fromEnum $ blid b) Nothing)
                    Nothing
  modifyServer $ \ser -> ser { sbreakASAP = True
                             , sbreakLoop = True }

verifyAssertExplored :: MonadServer m => m ()
verifyAssertExplored = do
  assertExplored <- getsServer $ sassertExplored . soptions
  case assertExplored of
    Nothing -> return ()
    Just lvlN -> do
      -- Exploration (by any party) verfied via spawning; beware of levels
      -- with disabled spawning.
      snumSpawned <- getsServer snumSpawned
      let !_A = assert (toEnum lvlN `EM.member` snumSpawned
                        || toEnum (- lvlN) `EM.member` snumSpawned
                        `blame` "by game end, exploration haven't reached the expected level depth, indicating stuck AI (or just very busy initial levels)"
                        `swith` lvlN) ()
      return ()

-- * ReqGameSaveAndExit

-- After we break out of the game loop, we will notice from @Camping@
-- we shouldn exit the game.
reqGameSaveAndExit :: MonadServerAtomic m => ActorId -> m ()
reqGameSaveAndExit aid = do
  verifyAssertExplored
  b <- getsState $ getActorBody aid
  oldSt <- getsState $ gquit . (EM.! bfid b) . sfactionD
  execUpdAtomic $ UpdQuitFaction
                    (bfid b)
                    oldSt
                    (Just $ Status Camping (fromEnum $ blid b) Nothing)
                    Nothing
  modifyServer $ \ser -> ser { sbreakASAP = True
                             , swriteSave = True }

-- * ReqGameSave

-- After we break out of the game loop, we will notice we shouldn't quit
-- the game and we will enter the game loop again.
reqGameSave :: MonadServer m => m ()
reqGameSave =
  modifyServer $ \ser -> ser { sbreakASAP = True
                             , swriteSave = True }

-- * ReqDoctrine

reqDoctrine :: MonadServerAtomic m => FactionId -> Ability.Doctrine -> m ()
reqDoctrine fid toT = do
  fromT <- getsState $ gdoctrine . (EM.! fid) . sfactionD
  execUpdAtomic $ UpdDoctrineFaction fid toT fromT

-- * ReqAutomate

reqAutomate :: MonadServerAtomic m => FactionId -> m ()
reqAutomate fid = execUpdAtomic $ UpdAutoFaction fid True