{-# LANGUAGE TupleSections #-}
-- | Server operations common to many modules.
module Game.LambdaHack.Server.CommonM
  ( revealAll, generalMoveItem, deduceQuits
  , writeSaveAll, verifyCaches, deduceKilled, electLeader, setFreshLeader
  , updatePer, projectFail, addActorFromGroup, registerActor
  , discoverIfMinorEffects, pickWeaponServer, currentSkillsServer, allGroupItems
  , addCondition, removeConditionSingle, addSleep, removeSleepSingle
  , addKillToAnalytics
#ifdef EXPOSE_INTERNAL
    -- * Internal operations
  , revealItems, revealPerceptionLid, containerMoveItem, quitF, keepArenaFact
  , anyActorsAlive, updatePerFromNew, recomputeCachePer
  , projectBla, addProjectile, addNonProjectile, addActorIid
  , getCacheLucid, getCacheTotal
#endif
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import qualified Data.EnumMap.Strict as EM
import qualified Data.EnumSet as ES
import qualified Data.IntMap.Strict as IM
import           Data.Ratio

import           Game.LambdaHack.Atomic
import           Game.LambdaHack.Common.Actor
import           Game.LambdaHack.Common.ActorState
import           Game.LambdaHack.Common.Analytics
import           Game.LambdaHack.Common.Area
import           Game.LambdaHack.Common.ClientOptions
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.Perception
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 qualified Game.LambdaHack.Content.CaveKind as CK
import           Game.LambdaHack.Content.FactionKind
import           Game.LambdaHack.Content.ItemKind (ItemKind)
import qualified Game.LambdaHack.Content.ItemKind as IK
import           Game.LambdaHack.Core.Random
import qualified Game.LambdaHack.Definition.Ability as Ability
import           Game.LambdaHack.Definition.Defs
import qualified Game.LambdaHack.Definition.DefsInternal as DefsInternal
import           Game.LambdaHack.Server.Fov
import           Game.LambdaHack.Server.ItemM
import           Game.LambdaHack.Server.ItemRev
import           Game.LambdaHack.Server.MonadServer
import           Game.LambdaHack.Server.ServerOptions
import           Game.LambdaHack.Server.State

revealItems :: MonadServerAtomic m => FactionId -> m ()
revealItems fid = do
  COps{coitem} <- getsState scops
  ServerOptions{sclientOptions} <- getsServer soptions
  discoAspect <- getsState sdiscoAspect
  let keptSecret kind ar = IA.isHumanTrinket kind
                           || IA.checkFlag Ability.MetaGame ar
      discover aid store iid _ = do
        itemKindId <- getsState $ getIidKindIdServer iid
        let arItem = discoAspect EM.! iid
            c = CActor aid store
            itemKind = okind coitem itemKindId
        unless (keptSecret itemKind arItem) $  -- a hack
          execUpdAtomic $ UpdDiscover c iid itemKindId arItem
      f (aid, b) =
        -- CStash is IDed for each actor of each faction, which is fine,
        -- even though it may introduce a slight lag at gameover.
        join $ getsState $ mapActorItems_ (discover aid) b
  -- Don't ID projectiles, their items are not really owned by the party.
  aids <- getsState $ fidActorNotProjGlobalAssocs fid
  mapM_ f aids
  dungeon <- getsState sdungeon
  let minLid = fst $ minimumBy (comparing (ldepth . snd))
                   $ EM.assocs dungeon
      discoverSample iid = do
        itemKindId <- getsState $ getIidKindIdServer iid
        let arItem = discoAspect EM.! iid
            cdummy = CTrunk fid minLid originPoint
            itemKind = okind coitem itemKindId
        -- Due to @cdummy@, the met and unmet secret things will appear
        -- at gameover among actors in the debug mode. Tough luck.
        execUpdAtomic $ if keptSecret itemKind arItem
                        then UpdSpotItem False iid quantSingle cdummy
                        else UpdDiscover cdummy iid itemKindId arItem
  generationAn <- getsServer sgenerationAn
  getKindId <- getsState $ flip getIidKindIdServer
  let kindsEqual iid iid2 = getKindId iid == getKindId iid2 && iid /= iid2
      nonDupSample em iid 0 = not $ any (kindsEqual iid) $ EM.keys em
      nonDupSample _ _ _ = True
      nonDupGen = EM.map (\em -> EM.filterWithKey (nonDupSample em) em)
                         generationAn
  -- Remove samples that are supplanted by real items.
  -- If there are mutliple UI factions, the second run will be vacuus,
  -- but it's important to do that before the first try to identify things
  -- to prevent spam from identifying samples that are not needed.
  modifyServer $ \ser -> ser {sgenerationAn = nonDupGen}
  when (sexposeActors sclientOptions) $
    -- Few, if any, need ID, but we can't rule out unusual content.
    mapM_ discoverSample $ EM.keys $ nonDupGen EM.! STrunk
  when (sexposeItems sclientOptions) $ do
    mapM_ discoverSample $ EM.keys $ nonDupGen EM.! SItem
    mapM_ discoverSample $ EM.keys $ nonDupGen EM.! SEmbed
    mapM_ discoverSample $ EM.keys $ nonDupGen EM.! SOrgan
    mapM_ discoverSample $ EM.keys $ nonDupGen EM.! SCondition
    mapM_ discoverSample $ EM.keys $ nonDupGen EM.! SBlast

revealAll :: MonadServerAtomic m => FactionId -> m ()
revealAll fid = do
  revealItems fid
  execUpdAtomic $ UpdMuteMessages fid True
  dungeon <- getsState sdungeon
  -- Perception needs to be sent explicitly, because normal management
  -- assumes an action must happen on a level to invalidate and regenerate
  -- perception on the level (and actors must survive!).
  -- Also, we'd rather hack here and in `verifyCaches` that complicate
  -- the already complex perception creation and caching code.
  mapM_ (revealPerceptionLid fid) $ EM.assocs dungeon
  execUpdAtomic $ UpdMuteMessages fid False

revealPerceptionLid :: MonadServerAtomic m
                    => FactionId -> (LevelId, Level) -> m ()
revealPerceptionLid fid (lid, lvl) = do
  let (x0, y0, x1, y1) = fromArea $ larea lvl
      fullSet = ES.fromDistinctAscList [ Point x y
                                       | y <- [y0 .. y1]
                                       , x <- [x0 .. x1] ]
      perNew = Perception
        { psight = PerVisible fullSet
        , psmell = PerSmelled ES.empty  -- don't obscure
        }
  updatePerFromNew fid lid perNew

-- | Generate the atomic updates that jointly perform a given item move.
generalMoveItem :: MonadStateRead m
                => Bool -> ItemId -> Int -> Container -> Container
                -> m [UpdAtomic]
generalMoveItem _ iid k (CActor aid1 cstore1) c2@(CActor aid2 cstore2)
  | aid1 == aid2 = do
    moveStash <- moveStashIfNeeded c2
    return $! moveStash ++ [UpdMoveItem iid k aid1 cstore1 cstore2]
generalMoveItem verbose iid k c1 c2 = containerMoveItem verbose iid k c1 c2

containerMoveItem :: MonadStateRead m
                  => Bool -> ItemId -> Int -> Container -> Container
                  -> m [UpdAtomic]
containerMoveItem verbose iid k c1 c2 = do
  bag <- getsState $ getContainerBag c1
  case iid `EM.lookup` bag of
    Nothing -> error $ "" `showFailure` (iid, k, c1, c2)
    Just (_, it) -> do
      moveStash <- moveStashIfNeeded c2
      return $ [UpdLoseItem verbose iid (k, take k it) c1]
               ++ moveStash
               ++ [UpdSpotItem verbose iid (k, take k it) c2]

quitF :: MonadServerAtomic m => Status -> FactionId -> m ()
quitF status fid = do
  fact <- getsState $ (EM.! fid) . sfactionD
  let oldSt = gquit fact
  -- Note that it's the _old_ status that we check here.
  case stOutcome <$> oldSt of
    Just Killed -> return ()    -- Do not overwrite in case
    Just Defeated -> return ()  -- many things happen in 1 turn.
    Just Conquer -> return ()
    Just Escape -> return ()
    _ -> do
      let !_A = assert (stOutcome status `notElem` [Camping, Restart]
                        `blame` "Camping and Restart are handled separately"
                        `swith` (stOutcome <$> oldSt, status, fid)) ()
      -- This runs regardless of the _new_ status.
      manalytics <-
        if fhasUI $ gkind fact then do
          keepAutomated <- getsServer $ skeepAutomated . soptions
          -- Try to remove AI control of the UI faction, to show gameover info.
          when (gunderAI fact && not keepAutomated) $
            execUpdAtomic $ UpdAutoFaction fid False
          revealAll fid
          -- Likely, by this time UI faction is no longer AI-controlled,
          -- so the score will get registered.
          registerScore status fid
          factionAn <- getsServer sfactionAn
          generationAn <- getsServer sgenerationAn
          return $ Just (factionAn, generationAn)
        else return Nothing
      execUpdAtomic $ UpdQuitFaction fid oldSt (Just status) manalytics
      modifyServer $ \ser -> ser {sbreakLoop = True}  -- check game over

-- Send any UpdQuitFaction actions that can be deduced from factions'
-- current state.
deduceQuits :: MonadServerAtomic m => FactionId -> Status -> m ()
deduceQuits fid0 status@Status{stOutcome}
  | stOutcome `elem` [Defeated, Camping, Restart, Conquer] =
    error $ "no quitting to deduce" `showFailure` (fid0, status)
deduceQuits fid0 status = do
  fact0 <- getsState $ (EM.! fid0) . sfactionD
  let factHasUI = fhasUI . gkind
      quitFaction (stOutcome, (fid, _)) = quitF status{stOutcome} fid
      mapQuitF outfids = do
        let (withUI, withoutUI) =
              partition (factHasUI . snd . snd)
                        ((stOutcome status, (fid0, fact0)) : outfids)
        mapM_ quitFaction (withoutUI ++ withUI)
      inGameOutcome (fid, fact) = do
        let mout | fid == fid0 = Just $ stOutcome status
                 | otherwise = stOutcome <$> gquit fact
        case mout of
          Just Killed -> False
          Just Defeated -> False
          Just Restart -> False  -- effectively, commits suicide
          _ -> True
  factionD <- getsState sfactionD
  let assocsInGame = filter inGameOutcome $ EM.assocs factionD
      assocsKeepArena = filter (keepArenaFact . snd) assocsInGame
      assocsUI = filter (factHasUI . snd) assocsInGame
      nonHorrorAIG = filter (not . isHorrorFact . snd) assocsInGame
      worldPeace =
        all (\(fid1, _) -> all (\(fid2, fact2) -> not $ isFoe fid2 fact2 fid1)
                           nonHorrorAIG)
        nonHorrorAIG
      othersInGame = filter ((/= fid0) . fst) assocsInGame
  if | null assocsUI ->
       -- Only non-UI players left in the game and they all win.
       mapQuitF $ zip (repeat Conquer) othersInGame
     | null assocsKeepArena ->
       -- Only leaderless and spawners remain (the latter may sometimes
       -- have no leader, just as the former), so they win,
       -- or we could get stuck in a state with no active arena
       -- and so no spawns.
       mapQuitF $ zip (repeat Conquer) othersInGame
     | worldPeace ->
       -- Nobody is at war any more, so all win (e.g., horrors, but never mind).
       mapQuitF $ zip (repeat Conquer) othersInGame
     | stOutcome status == Escape -> do
       -- Otherwise, in a game with many warring teams alive,
       -- only complete Victory matters, until enough of them die.
       let (victors, losers) =
             partition (\(fi, _) -> isFriend fid0 fact0 fi) othersInGame
       mapQuitF $ zip (repeat Escape) victors ++ zip (repeat Defeated) losers
     | otherwise -> quitF status fid0

-- | Save game on server and all clients.
writeSaveAll :: MonadServerAtomic m => Bool -> Bool -> m ()
writeSaveAll uiRequested evenForNoConfirmGames = do
  bench <- getsServer $ sbenchmark . sclientOptions . soptions
  noConfirmsGame <- isNoConfirmsGame
  when (uiRequested
        || not bench && (not noConfirmsGame || evenForNoConfirmGames)) $ do
    execUpdAtomic UpdWriteSave
    saveServer
#ifdef WITH_EXPENSIVE_ASSERTIONS
    -- This check is sometimes repeated in @gameExit@, but we don't care about
    -- speed of shutdown and even more so in WITH_EXPENSIVE_ASSERTIONS mode.
    verifyCaches
#endif

verifyCaches :: MonadServer m => m ()
verifyCaches = do
  sperCacheFid <- getsServer sperCacheFid
  sperValidFid <- getsServer sperValidFid
  sactorMaxSkills2 <- getsState sactorMaxSkills
  sfovLucidLid <- getsServer sfovLucidLid
  sfovClearLid <- getsServer sfovClearLid
  sfovLitLid <- getsServer sfovLitLid
  sperFid <- getsServer sperFid
  actorMaxSkills <- getsState maxSkillsInDungeon
  ( fovLitLid, fovClearLid, fovLucidLid
   ,perValidFid, perCacheFid, perFid ) <- getsState perFidInDungeon
  rngs <- getsServer srngs  -- initial display may scroll off terminal memory
  factionD <- getsState sfactionD
  -- Perception off UI faction at game over is illegal (revealed to the player
  -- in 'revealAll'), which is fine, because it's never used.
  -- Don't verify perception in such cases. All the caches from which
  -- legal perception would be created at that point are legal and verified,
  -- which is almost as tight.
  let gameOverUI fact = fhasUI (gkind fact)
                        && maybe False ((/= Camping) . stOutcome) (gquit fact)
      isGameOverUI = any gameOverUI $ EM.elems factionD
      !_A7 = assert (sfovLitLid == fovLitLid
                     `blame` "wrong accumulated sfovLitLid"
                     `swith` (sfovLitLid, fovLitLid, rngs)) ()
      !_A6 = assert (sfovClearLid == fovClearLid
                     `blame` "wrong accumulated sfovClearLid"
                     `swith` (sfovClearLid, fovClearLid, rngs)) ()
      !_A5 = assert (sactorMaxSkills2 == actorMaxSkills
                     `blame` "wrong accumulated sactorMaxSkills"
                     `swith` (sactorMaxSkills2, actorMaxSkills, rngs)) ()
      !_A4 = assert (sfovLucidLid == fovLucidLid
                     `blame` "wrong accumulated sfovLucidLid"
                     `swith` (sfovLucidLid, fovLucidLid, rngs)) ()
      !_A3 = assert (sperValidFid == perValidFid
                     `blame` "wrong accumulated sperValidFid"
                     `swith` (sperValidFid, perValidFid, rngs)) ()
      !_A2 = assert (sperCacheFid == perCacheFid
                     `blame` "wrong accumulated sperCacheFid"
                     `swith` (sperCacheFid, perCacheFid, rngs)) ()
      !_A1 = assert (isGameOverUI || sperFid == perFid
                     `blame` "wrong accumulated perception"
                     `swith` (sperFid, perFid, rngs)) ()
  return ()

-- | Tell whether a faction that we know is still in game, keeps arena.
-- Keeping arena means, if the faction is still in game,
-- it always has a leader in the dungeon somewhere.
-- So, leaderless factions and spawner factions do not keep an arena,
-- even though the latter usually has a leader for most of the game.
keepArenaFact :: Faction -> Bool
keepArenaFact fact = fhasPointman (gkind fact) && fneverEmpty (gkind fact)

-- We assume the actor in the second argument has HP <= 0 or is going to be
-- dominated right now. Even if the actor is to be dominated,
-- @bfid@ of the actor body is still the old faction.
deduceKilled :: MonadServerAtomic m => ActorId -> m ()
deduceKilled aid = do
  body <- getsState $ getActorBody aid
  fact <- getsState $ (EM.! bfid body) . sfactionD
  when (fneverEmpty $ gkind fact) $ do
    actorsAlive <- anyActorsAlive (bfid body) aid
    unless actorsAlive $
      deduceQuits (bfid body) $ Status Killed (fromEnum $ blid body) Nothing

anyActorsAlive :: MonadServer m => FactionId -> ActorId -> m Bool
anyActorsAlive fid aid = do
  as <- getsState $ fidActorNotProjGlobalAssocs fid
  -- We test HP here, in case more than one actor goes to 0 HP in the same turn.
  return $! any (\(aid2, b2) -> aid2 /= aid && bhp b2 > 0) as

electLeader :: MonadServerAtomic m => FactionId -> LevelId -> ActorId -> m ()
electLeader fid lid aidToReplace = do
  mleader <- getsState $ gleader . (EM.! fid) . sfactionD
  when (mleader == Just aidToReplace) $ do
    allOurs <- getsState $ fidActorNotProjGlobalAssocs fid  -- not only on level
    let -- Prefer actors on this level and with positive HP and not sleeping.
        -- Exclude @aidToReplace@, even if not dead (e.g., if being dominated).
        (positive, negative) = partition (\(_, b) -> bhp b > 0) allOurs
        (awake, sleeping) = partition (\(_, b) -> bwatch b /= WSleep) positive
    onThisLevel <- getsState $ fidActorRegularAssocs fid lid
    let candidates = filter (\(_, b) -> bwatch b /= WSleep) onThisLevel
                     ++ awake ++ sleeping ++ negative
        mleaderNew = find (/= aidToReplace) $ map fst candidates
    execUpdAtomic $ UpdLeadFaction fid mleader mleaderNew

setFreshLeader :: MonadServerAtomic m => FactionId -> ActorId -> m ()
setFreshLeader fid aid = do
  fact <- getsState $ (EM.! fid) . sfactionD
  when (fhasPointman (gkind fact)) $ do
    -- First update and send Perception so that the new leader
    -- may report his environment.
    b <- getsState $ getActorBody aid
    let !_A = assert (not $ bproj b) ()
    valid <- getsServer $ (EM.! blid b) . (EM.! fid) . sperValidFid
    unless valid $ updatePer fid (blid b)
    execUpdAtomic $ UpdLeadFaction fid (gleader fact) (Just aid)

updatePer :: MonadServerAtomic m => FactionId -> LevelId -> m ()
updatePer fid lid = do
  -- Performed in the State after action, e.g., with a new actor.
  perNew <- recomputeCachePer fid lid
  updatePerFromNew fid lid perNew

updatePerFromNew :: MonadServerAtomic m
                 => FactionId -> LevelId -> Perception -> m ()
updatePerFromNew fid lid perNew = do
  -- Even if nothing needed to be done, perception is now validated.
  modifyServer $ \ser ->
    ser {sperValidFid = EM.adjust (EM.insert lid True) fid $ sperValidFid ser}
  sperFidOld <- getsServer sperFid
  let perOld = sperFidOld EM.! fid EM.! lid
      inPer = diffPer perNew perOld
      outPer = diffPer perOld perNew
  unless (nullPer outPer && nullPer inPer) $ do
    -- Perception is modified on the server and sent to the client
    -- together with all the revealed info.
    let fper = EM.adjust (EM.insert lid perNew) fid
    modifyServer $ \ser -> ser {sperFid = fper $ sperFid ser}
    execSendPer fid lid outPer inPer perNew

recomputeCachePer :: MonadServer m => FactionId -> LevelId -> m Perception
recomputeCachePer fid lid = do
  total <- getCacheTotal fid lid
  fovLucid <- getCacheLucid lid
  getsState $ perceptionFromPTotal fid lid fovLucid total

-- The missile item is removed from the store only if the projection
-- went into effect (no failure occured).
projectFail :: MonadServerAtomic m
            => ActorId    -- ^ actor causing the projection
            -> ActorId    -- ^ actor projecting the item (is on current level)
            -> Point      -- ^ starting position of the projectile;
                          --   usually, but not always, position of @origin@
            -> Point      -- ^ target position of the projectile
            -> Int        -- ^ digital line parameter
            -> Bool       -- ^ whether to start at the origin's position
            -> ItemId     -- ^ the item to be projected
            -> CStore     -- ^ which store the items comes from
            -> Bool       -- ^ whether the item is a blast
            -> m (Maybe ReqFailure)
projectFail propeller origin oxy tpxy eps center iid cstore blast = do
  COps{coTileSpeedup} <- getsState scops
  body <- getsState $ getActorBody origin
  let lid = blid body
  lvl <- getLevel lid
  case bresenhamsLineAlgorithm eps oxy tpxy of
    Nothing -> return $ Just ProjectAimOnself
    Just [] -> error $ "projecting from the edge of level"
                       `showFailure` (oxy, tpxy)
    Just (pos : restUnlimited) -> do
      bag <- getsState $ getBodyStoreBag body cstore
      case EM.lookup iid bag of
        Nothing -> return $ Just ProjectOutOfReach
        Just _kit -> do
          itemFull <- getsState $ itemToFull iid
          actorSk <- currentSkillsServer origin
          actorMaxSk <- getsState $ getActorMaxSkills origin
          let skill = Ability.getSk Ability.SkProject actorSk
              forced = blast || bproj body
              calmE = calmEnough body actorMaxSk
              legal = permittedProject forced skill calmE itemFull
              arItem = aspectRecordFull itemFull
          case legal of
            Left reqFail -> return $ Just reqFail
            Right _ -> do
              let lobable = IA.checkFlag Ability.Lobable arItem
                  rest = if lobable
                         then take (chessDist oxy tpxy - 1) restUnlimited
                         else restUnlimited
                  t = lvl `at` pos
              if | not $ Tile.isWalkable coTileSpeedup t ->
                   return $ Just ProjectBlockTerrain
                 | occupiedBigLvl pos lvl ->
                   if blast then do
                     -- Hit the blocking actor by starting the explosion
                     -- particle where the projectile landed, not a step away.
                     -- The same when the spot has the explosive embed,
                     -- regardless if it's walkable (@pos@ is, that's enough).
                     -- No problem even if there's a big actor where
                     -- the projectile starts, though it's wierd it may get
                     -- away unharmed sometimes.
                     projectBla propeller origin oxy (pos:rest)
                                iid cstore blast
                     return Nothing
                   else return $ Just ProjectBlockActor
                 | otherwise -> do
                   -- Make the explosion less regular and weaker at the edges.
                   if blast && center then
                     -- Start in the center, not around, even if the center
                     -- is a non-walkable tile with the exploding embed
                     -- or if a big actor is there.
                     projectBla propeller origin oxy (pos:rest)
                                iid cstore blast
                   else
                     projectBla propeller origin pos rest
                                iid cstore blast
                   return Nothing

projectBla :: MonadServerAtomic m
           => ActorId    -- ^ actor causing the projection
           -> ActorId    -- ^ actor projecting the item (is on current lvl)
           -> Point      -- ^ starting point of the projectile
           -> [Point]    -- ^ rest of the trajectory of the projectile
           -> ItemId     -- ^ the item to be projected
           -> CStore     -- ^ which store the items comes from
           -> Bool       -- ^ whether the item is a blast
           -> m ()
projectBla propeller origin pos rest iid cstore blast = do
  body <- getsState $ getActorBody origin
  let lid = blid body
  localTime <- getsState $ getLocalTime lid
  unless blast $ execSfxAtomic $ SfxProject origin iid
  bag <- getsState $ getBodyStoreBag body cstore
  ItemFull{itemKind} <- getsState $ itemToFull iid
  case iid `EM.lookup` bag of
    Nothing -> error $ "" `showFailure` (origin, pos, rest, iid, cstore)
    Just kit@(_, it) -> do
      let delay =
            if IK.iweight itemKind == 0
            then timeTurn  -- big delay at start, e.g., to easily read hologram
            else timeZero  -- avoid running into own projectiles
          btime = absoluteTimeAdd delay localTime
      addProjectile propeller pos rest iid kit lid (bfid body) btime
      let c = CActor origin cstore
      execUpdAtomic $ UpdLoseItem False iid (1, take 1 it) c

addActorFromGroup :: MonadServerAtomic m
                  => GroupName ItemKind -> FactionId -> Point -> LevelId -> Time
                  -> m (Maybe ActorId)
addActorFromGroup actorGroup fid pos lid time = do
  Level{ldepth} <- getLevel lid
  -- We bootstrap the actor by first creating the trunk of the actor's body
  -- that contains the fixed properties of all actors of that kind.
  freq <- prepareItemKind 0 ldepth [(actorGroup, 1)]
  m2 <- rollItemAspect freq ldepth
  case m2 of
    NoNewItem -> return Nothing
    NewItem _ itemKnown itemFull itemQuant -> do
      let itemFullKit = (itemFull, itemQuant)
      Just <$> registerActor False itemKnown itemFullKit fid pos lid time

registerActor :: MonadServerAtomic m
              => Bool -> ItemKnown -> ItemFullKit
              -> FactionId -> Point -> LevelId -> Time
              -> m ActorId
registerActor summoned (ItemKnown kindIx ar _) (itemFullRaw, kit)
              bfid pos lid time = do
  COps{cocave} <- getsState scops
  let container = CTrunk bfid lid pos
      jfid = Just bfid
      itemKnown = ItemKnown kindIx ar jfid
      itemFull = itemFullRaw {itemBase = (itemBase itemFullRaw) {jfid}}
  trunkId <- registerItem False (itemFull, kit) itemKnown container
  aid <- addNonProjectile summoned trunkId (itemFull, kit) bfid pos lid time
  fact <- getsState $ (EM.! bfid) . sfactionD
  actorMaxSk <- getsState $ getActorMaxSkills aid
  condAnyFoeAdj <- getsState $ anyFoeAdj aid
  Level{lkind} <- getLevel lid
  let cinitSleep = CK.cinitSleep $ okind cocave lkind
  when (cinitSleep /= CK.InitSleepBanned
        && canSleep actorMaxSk
        && not condAnyFoeAdj
        && not summoned
        && not (fhasGender (gkind fact))) $ do  -- heroes never start asleep
    -- A lot of actors will wake up at once anyway, so let most start sleeping.
    let sleepOdds = if prefersSleep actorMaxSk then 19%20 else 2%3
    sleeps <- rndToAction $ chance sleepOdds
    when (cinitSleep == CK.InitSleepAlways || sleeps) $ addSleep aid
  return aid

addProjectile :: MonadServerAtomic m
              => ActorId -> Point -> [Point] -> ItemId -> ItemQuant -> LevelId
              -> FactionId -> Time
              -> m ()
addProjectile propeller pos rest iid (_, it) lid fid time = do
  itemFull <- getsState $ itemToFull iid
  let arItem = aspectRecordFull itemFull
      IK.ThrowMod{IK.throwHP} = IA.aToThrow arItem
      (trajectory, (speed, _)) =
        IA.itemTrajectory arItem (itemKind itemFull) (pos : rest)
      -- Trunk is added to equipment, not to organs, because it's the
      -- projected item, so it's carried, not grown.
      tweakBody b = b { bhp = xM throwHP
                      , btrajectory = Just (trajectory, speed)
                      , beqp = EM.singleton iid (1, take 1 it) }
  aid <- addActorIid iid itemFull True fid pos lid tweakBody
  bp <- getsState $ getActorBody propeller
  -- If propeller is a projectile, it may produce other projectiles, e.g.,
  -- by exploding, so it's not voluntary, so others are to blame.
  -- However, we can't easily see whether a pushed non-projectile actor
  -- produced a projectile due to colliding or voluntarily, so we assign
  -- blame to him.
  originator <- if bproj bp
                then getsServer $ EM.findWithDefault propeller propeller
                                  . strajPushedBy
                else return propeller
  modifyServer $ \ser ->
    ser { strajTime = updateActorTime fid lid aid time $ strajTime ser
        , strajPushedBy = EM.insert aid originator $ strajPushedBy ser }

addNonProjectile :: MonadServerAtomic m
                 => Bool -> ItemId -> ItemFullKit -> FactionId -> Point
                 -> LevelId -> Time
                 -> m ActorId
addNonProjectile summoned trunkId (itemFull, kit) fid pos lid time = do
  let tweakBody b = b { borgan = EM.singleton trunkId kit
                      , bcalm = if summoned
                                then xM 5  -- a tiny buffer before domination
                                else bcalm b }
  aid <- addActorIid trunkId itemFull False fid pos lid tweakBody
  -- We assume actor is never born pushed.
  modifyServer $ \ser ->
    ser {sactorTime = updateActorTime fid lid aid time $ sactorTime ser}
  return aid

addActorIid :: MonadServerAtomic m
            => ItemId -> ItemFull -> Bool -> FactionId -> Point -> LevelId
            -> (Actor -> Actor)
            -> m ActorId
addActorIid trunkId ItemFull{itemBase, itemKind, itemDisco=ItemDiscoFull arItem}
            bproj fid pos lid tweakBody = do
  COps{coitem} <- getsState scops
  -- Initial HP and Calm is based only on trunk and ignores organs.
  let trunkMaxHP = max 2 $ IA.getSkill Ability.SkMaxHP arItem
      hp = xM trunkMaxHP `div` 2
      -- Slightly reduced starting Calm to auto-id items that refill Calm
      -- and to let animals do some initial exploration before going to sleep.
      -- Higher reduction would cause confusingly low sight range at game
      -- start and even inability to handle equipment.
      calm = xM (max 1 $ IA.getSkill Ability.SkMaxCalm arItem - 10)
  -- Create actor.
  factionD <- getsState sfactionD
  curChalSer <- getsServer $ scurChalSer . soptions
  let fact = factionD EM.! fid
      teamContinuityOurs = fteam (gkind fact)
  bnumberTeam <-
    if bproj then return Nothing else do
      stcounter <- getsServer stcounter
      let number = EM.findWithDefault 0 teamContinuityOurs stcounter
      modifyServer $ \ser -> ser {stcounter =
        EM.insert teamContinuityOurs (succ number) stcounter}
      return $ Just (number, teamContinuityOurs)
  let bnumber = fst <$> bnumberTeam
  -- If difficulty is below standard, HP is added to the UI factions,
  -- otherwise HP is added to their enemies.
  -- If no UI factions, their role is taken by the escapees (for testing).
  let diffBonusCoeff = difficultyCoeff $ cdiff curChalSer
      -- For most projectiles (exceptions are, e.g.,  maxHP boosting rings),
      -- SkMaxHP is zero, which means they drop after one hit regardless
      -- of extra bhp they have due to piercing. That is fine.
      -- If we want armoured missiles, that should not be done via piercing,
      -- but via SkMaxHP of the thrown items. Rings that are piercing
      -- by coincidence are harmless, too. However, piercing should not be
      -- added to missiles via SkMaxHP or equipping them would be beneficial
      -- in a hard to balance way (e.g., one bullet adds 10 SkMaxHP).
      boostFact = not bproj
                  && if diffBonusCoeff > 0
                     then any (fhasUI . gkind . snd)
                              (filter (\(fi, fa) -> isFriend fi fa fid)
                                      (EM.assocs factionD))
                     else any (fhasUI . gkind  . snd)
                              (filter (\(fi, fa) -> isFoe fi fa fid)
                                      (EM.assocs factionD))
      finalHP | boostFact = min (xM 899)  -- no more than UI can stand
                                (hp * 2 ^ abs diffBonusCoeff)
              | otherwise = hp
      -- Prevent too high max HP resulting in panic when low HP/max HP ratio.
      maxHP = min (finalHP + xM 100) (2 * finalHP)
      bonusHP = fromEnum (maxHP `div` oneM) - trunkMaxHP
      healthOrgans = [(Just bonusHP, (IK.S_BONUS_HP, COrgan)) | bonusHP /= 0]
      b = actorTemplate trunkId bnumber finalHP calm pos lid fid bproj
      withTrunk =
        b { bweapon = if IA.checkFlag Ability.Meleeable arItem then 1 else 0
          , bweapBenign =
              if IA.checkFlag Ability.Meleeable arItem
                 && IA.checkFlag Ability.Benign arItem then 1 else 0 }
      bodyTweaked = tweakBody withTrunk
  aid <- getsServer sacounter
  modifyServer $ \ser -> ser {sacounter = succ aid}
  execUpdAtomic $ UpdCreateActor aid bodyTweaked [(trunkId, itemBase)]
  unless bproj $ do
    steamGearCur <- getsServer steamGearCur
    let gearList = case bnumberTeam of
          Nothing -> []
          Just (number, teamContinuity) ->
            case teamContinuity `EM.lookup` steamGearCur of
              Nothing -> []
              Just im -> IM.findWithDefault [] number im
    -- Create, register and insert all initial actor items, including
    -- the bonus health organs from difficulty setting.
    forM_ (healthOrgans ++ map (Nothing,) (IK.ikit itemKind))
          $ \(mk, (ikGrp, cstore)) -> do
     -- TODO: remove ASAP. This is a hack that prevents AI from stealing
     -- backstories until there is enough of them in Allure.
     -- Instead, pre-generate 20 player heroes to make sure all unique
     -- backstories are available to the player and so that the order
     -- of games played doesn't affect their availability.
     if ikGrp == DefsInternal.GroupName "backstory"
        && isJust bnumberTeam
        && (snd <$> bnumberTeam) /= Just teamExplorer
     then return ()
     else do
      let container = CActor aid cstore
      Level{ldepth} <- getLevel lid
      mIidEtc <- case lookup ikGrp gearList of
        Nothing -> do
          let itemFreq = [(ikGrp, 1)]
          -- Power depth of new items unaffected by number of spawned actors.
          freq <- prepareItemKind 0 ldepth itemFreq
          mIidEtc <- rollAndRegisterItem False ldepth freq container mk
          case (bnumberTeam, mIidEtc) of
            (Just (number, teamContinuity), Just (_, (itemFull2, _))) -> do
              let arItem2 = aspectRecordFull itemFull2
                  inMetaGame = IA.checkFlag Ability.MetaGame arItem2
                  itemKindId2 = itemKindId itemFull2
              when inMetaGame $ do
                let altInner ml = Just $ (ikGrp, itemKindId2) : fromMaybe [] ml
                    alt mim =
                      Just $ IM.alter altInner number $ fromMaybe IM.empty mim
                modifyServer $ \ser ->
                  ser {steamGear = EM.alter alt teamContinuity $ steamGear ser}
            _ -> return ()
          return mIidEtc
        Just itemKindId2 -> do
          let gearListNew = delete (ikGrp, itemKindId2) gearList
              (number, teamContinuity) = fromJust bnumberTeam
              alt mim =
                Just $ IM.insert number gearListNew $ fromMaybe IM.empty mim
          modifyServer $ \ser ->
            ser {steamGearCur = EM.alter alt teamContinuity steamGearCur}
          let itemKind2 = okind coitem itemKindId2
              freq = pure (ikGrp, itemKindId2, itemKind2)
          rollAndRegisterItem False ldepth freq container mk
      case mIidEtc of
        Nothing -> error $ "" `showFailure` (lid, ikGrp, container, mk)
        Just (iid, (itemFull2, _)) ->
          when (cstore /= CGround) $
            -- The items are created owned by actors, so won't be picked up,
            -- so we have to discover them now, if eligible.
            discoverIfMinorEffects container iid (itemKindId itemFull2)
  return aid
addActorIid _ _ _ _ _ _ _ = error "addActorIid: server ignorant about an item"

discoverIfMinorEffects :: MonadServerAtomic m
                       => Container -> ItemId -> ContentId ItemKind -> m ()
discoverIfMinorEffects c iid itemKindId = do
  COps{coitem} <- getsState scops
  discoAspect <- getsState sdiscoAspect
  let arItem = discoAspect EM.! iid
      itemKind = okind coitem itemKindId
   -- Otherwise, discover by use when item's effects get activated later on.
  when (IA.onlyMinorEffects arItem itemKind
        && not (IA.isHumanTrinket itemKind)) $
    execUpdAtomic $ UpdDiscover c iid itemKindId arItem

pickWeaponServer :: MonadServer m
                 => ActorId -> ActorId -> m (Maybe (ItemId, CStore))
pickWeaponServer source target = do
  eqpAssocs <- getsState $ kitAssocs source [CEqp]
  bodyAssocs <- getsState $ kitAssocs source [COrgan]
  actorSk <- currentSkillsServer source
  sb <- getsState $ getActorBody source
  tb <- getsState $ getActorBody target
  let kitAssRaw = eqpAssocs ++ bodyAssocs
      forced = bproj sb
      kitAss | forced = kitAssRaw  -- for projectiles, anything is weapon
             | otherwise =
                 filter (IA.checkFlag Ability.Meleeable
                         . aspectRecordFull . fst . snd) kitAssRaw
      benign itemFull = let arItem = aspectRecordFull itemFull
                        in IA.checkFlag Ability.Benign arItem
  -- Server ignores item effects or it would leak item discovery info.
  -- Hence, weapons with powerful burning or wouding are undervalued.
  -- In particular, it even uses weapons that would heal an opponent.
  -- But server decides only in exceptiona cases, e.g. projectile collision
  -- or melee in place of an impossible displace. Otherwise, client decides.
  strongest <- pickWeaponM False Nothing kitAss actorSk source
  case strongest of
    [] -> return Nothing
    (_, _, _, _, _, (itemFull, _)) : _ | not forced
                                         && benign itemFull && bproj tb ->
      return Nothing  -- if strongest is benign, don't waste fun on a projectile
    iis@((value1, hasEffect1, timeout1, _, _, _) : _) -> do
      let minIis = takeWhile (\(value, hasEffect, timeout, _, _, _) ->
                                 value == value1
                                 && hasEffect == hasEffect1
                                 && timeout == timeout1)
                             iis
      (_, _, _, _, iid, _) <- rndToAction $ oneOf minIis
      let cstore = if isJust (lookup iid bodyAssocs) then COrgan else CEqp
      return $ Just (iid, cstore)

-- @MonadStateRead@ would be enough, but the logic is sound only on server.
currentSkillsServer :: MonadServer m => ActorId -> m Ability.Skills
currentSkillsServer aid  = do
  body <- getsState $ getActorBody aid
  mleader <- getsState $ gleader . (EM.! bfid body) . sfactionD
  getsState $ actorCurrentSkills mleader aid

getCacheLucid :: MonadServer m => LevelId -> m FovLucid
getCacheLucid lid = do
  fovClearLid <- getsServer sfovClearLid
  fovLitLid <- getsServer sfovLitLid
  fovLucidLid <- getsServer sfovLucidLid
  let getNewLucid = getsState $ \s ->
        lucidFromLevel fovClearLid fovLitLid s lid (sdungeon s EM.! lid)
  case EM.lookup lid fovLucidLid of
    Just (FovValid fovLucid) -> return fovLucid
    _ -> do
      newLucid <- getNewLucid
      modifyServer $ \ser ->
        ser {sfovLucidLid = EM.insert lid (FovValid newLucid)
                            $ sfovLucidLid ser}
      return newLucid

getCacheTotal :: MonadServer m => FactionId -> LevelId -> m CacheBeforeLucid
getCacheTotal fid lid = do
  sperCacheFidOld <- getsServer sperCacheFid
  let perCacheOld = sperCacheFidOld EM.! fid EM.! lid
  case ptotal perCacheOld of
    FovValid total -> return total
    FovInvalid -> do
      actorMaxSkills <- getsState sactorMaxSkills
      fovClearLid <- getsServer sfovClearLid
      getActorB <- getsState $ flip getActorBody
      let perActorNew =
            perActorFromLevel (perActor perCacheOld) getActorB
                              actorMaxSkills (fovClearLid EM.! lid)
          -- We don't check if any actor changed, because almost surely one is.
          -- Exception: when an actor is destroyed, but then union differs, too.
          total = totalFromPerActor perActorNew
          perCache = PerceptionCache { ptotal = FovValid total
                                     , perActor = perActorNew }
          fperCache = EM.adjust (EM.insert lid perCache) fid
      modifyServer $ \ser -> ser {sperCacheFid = fperCache $ sperCacheFid ser}
      return total

allGroupItems :: MonadServerAtomic m
              => CStore -> GroupName ItemKind -> ActorId
              -> m [(ItemId, ItemQuant)]
allGroupItems store grp target = do
  COps{coitem} <- getsState scops
  b <- getsState $ getActorBody target
  assocsCStore <- getsState $ EM.assocs . getBodyStoreBag b store
  getKindId <- getsState $ flip getIidKindIdServer
  let assocsKindId = map (\as@(iid, _) -> (getKindId iid, as)) assocsCStore
      hasGroup (itemKindId, _) =
        maybe False (> 0) $ lookup grp $ IK.ifreq $ okind coitem itemKindId
  return $! map snd $ sortBy (comparing fst) $ filter hasGroup assocsKindId

addCondition :: MonadServerAtomic m
             => Bool -> GroupName ItemKind -> ActorId -> m ()
addCondition verbose name aid = do
  b <- getsState $ getActorBody aid
  Level{ldepth} <- getLevel $ blid b
  let c = CActor aid COrgan
  -- Power depth of new items unaffected by number of spawned actors.
  freq <- prepareItemKind 0 ldepth [(name, 1)]
  mresult <- rollAndRegisterItem verbose ldepth freq c Nothing
  assert (isJust mresult) $ return ()

removeConditionSingle :: MonadServerAtomic m
                      => GroupName ItemKind -> ActorId -> m Int
removeConditionSingle name aid = do
  let c = CActor aid COrgan
  is <- allGroupItems COrgan name aid
  case is of
    [(iid, (nAll, itemTimer))] -> do
      execUpdAtomic $ UpdLoseItem False iid (1, itemTimer) c
      return $ nAll - 1
    _ -> error $ "missing or multiple item" `showFailure` (name, is)

addSleep :: MonadServerAtomic m => ActorId -> m ()
addSleep aid = do
  b <- getsState $ getActorBody aid
  addCondition True IK.S_ASLEEP aid
  execUpdAtomic $ UpdWaitActor aid (bwatch b) WSleep

removeSleepSingle :: MonadServerAtomic m => ActorId -> m ()
removeSleepSingle aid = do
  nAll <- removeConditionSingle IK.S_ASLEEP aid
  when (nAll == 0) $
    execUpdAtomic $ UpdWaitActor aid WWake WWatch

addKillToAnalytics :: MonadServerAtomic m
                   => ActorId -> KillHow -> FactionId -> ItemId -> m ()
addKillToAnalytics aid killHow fid iid = do
  actorD <- getsState sactorD
  case EM.lookup aid actorD of
    Just b ->
      modifyServer $ \ser ->
        ser { sfactionAn = addFactionKill (bfid b) killHow fid iid
                           $ sfactionAn ser
            , sactorAn = addActorKill aid killHow fid iid
                         $ sactorAn ser }
    Nothing -> return ()  -- killer dead, too late to assign blame
