-- | Operations for starting and restarting the game. module Game.LambdaHack.Server.StartM ( initPer, reinitGame, gameReset, applyDebug #ifdef EXPOSE_INTERNAL -- * Internal operations , sampleTrunks, sampleItems , mapFromFuns, resetFactions, populateDungeon, findEntryPoss #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Control.Monad.Trans.State.Strict as St import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Key (mapWithKeyM_) import qualified Data.Map.Strict as M import qualified Data.Set as S import qualified Data.Text as T import qualified NLP.Miniutter.English as MU import qualified System.Random.SplitMix32 as SM import Game.LambdaHack.Atomic import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Analytics import Game.LambdaHack.Common.Area import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item 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.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.Content.ModeKind import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Core.Frequency import Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability import qualified Game.LambdaHack.Definition.Color as Color import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.Flavour import Game.LambdaHack.Server.CommonM import qualified Game.LambdaHack.Server.DungeonGen as DungeonGen 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 initPer :: MonadServer m => m () initPer = do ( sfovLitLid, sfovClearLid, sfovLucidLid ,sperValidFid, sperCacheFid, sperFid ) <- getsState perFidInDungeon modifyServer $ \ser -> ser { sfovLitLid, sfovClearLid, sfovLucidLid , sperValidFid, sperCacheFid, sperFid } reinitGame :: MonadServerAtomic m => FactionDict -> m () reinitGame factionDold = do COps{coitem} <- getsState scops pers <- getsServer sperFid ServerOptions{scurChalSer, sknowMap, sshowItemSamples, sclientOptions} <- getsServer soptions -- This state is quite small, fit for transmition to the client. -- The biggest part is content, which needs to be updated in clients -- at this point to keep them in sync with changes on the server. s <- getState discoS <- getsState sdiscoKind -- Thanks to the following, for any item with not hidden identity, -- the client has its kind from the start. The client needs to know this -- to have a fast way (faster that looking for @PresentAs@ flag on a list) -- of determining whether an item kind is already identified -- or needs identification. let discoKindFiltered = let f kindId = isNothing $ IK.getMandatoryPresentAsFromKind $ okind coitem kindId in EM.filter f discoS defL | sknowMap = s | otherwise = localFromGlobal s defLocal = updateDiscoKind (const discoKindFiltered) defL factionD <- getsState sfactionD clientStatesOld <- getsServer sclientStates metaBackupOld <- getsServer smetaBackup -- Some item kinds preserve their identity and flavour throughout -- the whole meta-game, until the savefiles are removed. -- These are usually not common man-made items, because these can be made -- in many flavours so it may be hard to recognize them. -- Character backstories and rare artifacts are uncommon enough -- to requiring learning their identify only once. -- However, the exact properties of even natural items may vary, -- so the random aspects of items, stored in @sdiscoAspect@ -- are not preserved (a lot of other state components would need -- to be partially preserved, too, both on server and clients). let inMetaGame kindId = IK.SetFlag Ability.MetaGame `elem` IK.iaspects (okind coitem kindId) metaDiscoOldFid = EM.map (EM.filter inMetaGame . sdiscoKind) clientStatesOld fidToTeam :: FactionId -> TeamContinuity fidToTeam fid = fteam $ gkind $ factionDold EM.! fid metaDiscoOldTeam = EM.fromList $ map (first fidToTeam) $ EM.assocs metaDiscoOldFid exclusiveUnion = EM.unionWith $ \_ _ -> error "forbidden duplicate" metaDiscoAll = metaDiscoOldTeam `exclusiveUnion` metaBackupOld currentTeams = ES.fromList $ map (fteam . gkind) $ EM.elems factionD metaBackupNew = EM.withoutKeys metaDiscoAll currentTeams stateNew fact = case EM.lookup (fteam $ gkind fact) metaDiscoAll of Nothing -> defLocal Just disco -> updateDiscoKind (disco `EM.union`) defLocal clientStatesNew = EM.map stateNew factionD modifyServer $ \ser -> ser { sclientStates = clientStatesNew , smetaBackup = metaBackupNew } let updRestart fid = UpdRestart fid (pers EM.! fid) (clientStatesNew EM.! fid) scurChalSer sclientOptions mapWithKeyM_ (\fid _ -> do -- Different seed for each client, to make sure behaviour is varied. gen1 <- getsServer srandom let (clientRandomSeed, gen2) = SM.splitSMGen gen1 modifyServer $ \ser -> ser {srandom = gen2} execUpdAtomic $ updRestart fid clientRandomSeed) factionD dungeon <- getsState sdungeon let sactorTime = EM.map (const (EM.map (const EM.empty) dungeon)) factionD strajTime = EM.map (const (EM.map (const EM.empty) dungeon)) factionD modifyServer $ \ser -> ser {sactorTime, strajTime} when sshowItemSamples $ do genOrig <- getsServer srandom uniqueSetOrig <- getsServer suniqueSet genOld <- getsServer sgenerationAn genSampleTrunks <- sampleTrunks dungeon genSampleItems <- sampleItems dungeon let sgenerationAn = EM.unions [genSampleTrunks, genSampleItems, genOld] modifyServer $ \ser -> ser {sgenerationAn} -- Make sure the debug generations don't affect future RNG behaviour. -- However, in the long run, AI behaviour is affected anyway, -- because the items randomly chosen for AI actions are ordered by their -- @ItemId@, which is affected by the sample item generation. modifyServer $ \ser -> ser {srandom = genOrig, suniqueSet = uniqueSetOrig} populateDungeon mapM_ (\fid -> mapM_ (updatePer fid) (EM.keys dungeon)) (EM.keys factionD) -- For simplicity only spawnable actors are taken into account, not starting -- actors of any faction nor summonable actors. sampleTrunks :: MonadServerAtomic m => Dungeon -> m GenerationAnalytics sampleTrunks dungeon = do COps{cocave, coitem} <- getsState scops factionD <- getsState sfactionD let getGroups Level{lkind} = map fst $ CK.cactorFreq $ okind cocave lkind groups = S.elems $ S.fromList $ concatMap getGroups $ EM.elems dungeon addGroupToSet !s0 !grp = ofoldlGroup' coitem grp (\s _ ik _ -> ES.insert ik s) s0 trunkKindIds = ES.elems $ foldl' addGroupToSet ES.empty groups minLid = fst $ minimumBy (comparing (ldepth . snd)) $ EM.assocs dungeon Level{ldepth} <- getLevel minLid let regItem itemKindId = do let itemKind = okind coitem itemKindId freq = pure (IK.HORROR, itemKindId, itemKind) case runFrequency $ possibleActorFactions [] itemKind factionD of [] -> error "sampleTrunks: null faction frequency" (_, (fid, _)) : _ -> do let c = CTrunk fid minLid originPoint jfid = Just fid m2 <- rollItemAspect freq ldepth case m2 of NoNewItem -> error "sampleTrunks: can't create actor trunk" NewItem _ (ItemKnown kindIx ar _) itemFullRaw itemQuant -> do let itemKnown = ItemKnown kindIx ar jfid itemFull = itemFullRaw {itemBase = (itemBase itemFullRaw) {jfid}} Just <$> registerItem False (itemFull, itemQuant) itemKnown c miids <- mapM regItem trunkKindIds return $! EM.singleton STrunk $ EM.fromDistinctAscList $ zip (catMaybes miids) $ repeat 0 -- For simplicity, only actors generated on the ground are taken into account. -- not starting items of any actors nor items that can be create by effects -- occuring in the game. sampleItems :: MonadServerAtomic m => Dungeon -> m GenerationAnalytics sampleItems dungeon = do COps{cocave, coitem} <- getsState scops let getGroups Level{lkind} = map fst $ CK.citemFreq $ okind cocave lkind groups = S.elems $ S.fromList $ concatMap getGroups $ EM.elems dungeon addGroupToSet !s0 !grp = ofoldlGroup' coitem grp (\s _ ik _ -> ES.insert ik s) s0 itemKindIds = ES.elems $ foldl' addGroupToSet ES.empty groups minLid = fst $ minimumBy (comparing (ldepth . snd)) $ EM.assocs dungeon Level{ldepth} <- getLevel minLid let regItem itemKindId = do let itemKind = okind coitem itemKindId freq = pure (IK.HORROR, itemKindId, itemKind) c = CFloor minLid originPoint m2 <- rollItemAspect freq ldepth case m2 of NoNewItem -> error "sampleItems: can't create sample item" NewItem _ itemKnown itemFull _ -> Just <$> registerItem False (itemFull, (0, [])) itemKnown c miids <- mapM regItem itemKindIds return $! EM.singleton SItem $ EM.fromDistinctAscList $ zip (catMaybes miids) $ repeat 0 mapFromFuns :: Ord b => [a] -> [a -> b] -> M.Map b a mapFromFuns domain = let fromFun f m1 = let invAssocs = map (\c -> (f c, c)) domain m2 = M.fromList invAssocs in m2 `M.union` m1 in foldr fromFun M.empty resetFactions :: ContentData FactionKind -> Dice.AbsDepth -> ModeKind -> Bool -> Rnd FactionDict resetFactions cofact totalDepth mode automateAll = do let rawCreate (fid, (fkGroup, initialActors)) = do -- Validation of content guarantess the existence of such faction kind. gkindId <- fromJust <$> opick cofact fkGroup (const True) let gkind@FactionKind{..} = okind cofact gkindId castInitialActors (ln, d, actorGroup) = do n <- castDice (Dice.AbsDepth $ abs ln) totalDepth d return (ln, n, actorGroup) ginitial <- mapM castInitialActors initialActors let cmap = mapFromFuns Color.legalFgCol [colorToTeamName, colorToPlainName, colorToFancyName] colorName = T.toLower $ head $ T.words fname prefix = case (fhasPointman, finitUnderAI) of (False, False) -> "Uncoordinated" (False, True) -> "Loose" (True, False) -> "Autonomous" (True, True) -> "Controlled" gnameNew = prefix <+> if fhasGender then makePhrase [MU.Ws $ MU.Text fname] else fname gcolor = M.findWithDefault Color.BrWhite colorName cmap let gname = gnameNew gdoctrine = finitDoctrine gunderAI = finitUnderAI || mattract mode || automateAll gdipl = EM.empty -- fixed below gquit = Nothing _gleader = Nothing gvictims = EM.empty gstash = Nothing return (fid, Faction{..}) lFs <- mapM rawCreate $ zip [toEnum 1 ..] $ mroster mode let mkDipl diplMode = let f (ix1, ix2) = let adj1 fact = fact {gdipl = EM.insert ix2 diplMode (gdipl fact)} in EM.adjust adj1 ix1 in foldr f -- Only symmetry is ensured, everything else is permitted, -- e.g., a faction in alliance with two others that are at war. pairsFromFaction :: (FactionKind -> [TeamContinuity]) -> (FactionId, Faction) -> [(FactionId, FactionId)] pairsFromFaction selector (fid, fact) = let teams = selector $ gkind fact hasTeam team (_, fact2) = team == fteam (gkind fact2) pairsFromTeam team = case find (hasTeam team) lFs of Just (fid2, _) -> [(fid, fid2), (fid2, fid)] Nothing -> [] in concatMap pairsFromTeam teams rawFs = EM.fromList lFs -- War overrides alliance, so 'warFs' second. Consequently, if a faction -- is allied with a faction that is at war with them, they will be -- symmetrically at war. allianceFs = mkDipl Alliance rawFs $ concatMap (pairsFromFaction falliedTeams) $ EM.assocs rawFs warFs = mkDipl War allianceFs $ concatMap (pairsFromFaction fenemyTeams) $ EM.assocs allianceFs return $! warFs gameReset :: MonadServer m => ServerOptions -> Maybe (GroupName ModeKind) -> Maybe SM.SMGen -> m State gameReset serverOptions mGameMode mrandom = do -- Dungeon seed generation has to come first, to ensure item boosting -- is determined by the dungeon RNG. cops@COps{cofact, comode} <- getsState scops dungeonSeed <- getSetGen $ sdungeonRng serverOptions `mplus` mrandom srandom <- getSetGen $ smainRng serverOptions `mplus` mrandom let srngs = RNGs (Just dungeonSeed) (Just srandom) when (sdumpInitRngs serverOptions) $ dumpRngs srngs scoreTable <- restoreScore cops teamGearOld <- getsServer steamGear flavourOld <- getsServer sflavour discoKindRevOld <- getsServer sdiscoKindRev clientStatesOld <- getsServer sclientStates let gameMode = fromMaybe INSERT_COIN $ mGameMode `mplus` sgameMode serverOptions rnd :: Rnd (FactionDict, FlavourMap, DiscoveryKind, DiscoveryKindRev, DungeonGen.FreshDungeon, ContentId ModeKind) rnd = do modeKindId <- fromMaybe (error $ "Unknown game mode:" `showFailure` gameMode) <$> opick comode gameMode (const True) let mode = okind comode modeKindId flavour <- dungeonFlavourMap cops flavourOld (discoKind, sdiscoKindRev) <- serverDiscos cops discoKindRevOld freshDng <- DungeonGen.dungeonGen cops serverOptions $ mcaves mode factionD <- resetFactions cofact (DungeonGen.freshTotalDepth freshDng) mode (sautomateAll serverOptions) return ( factionD, flavour, discoKind , sdiscoKindRev, freshDng, modeKindId ) let ( factionD, sflavour, discoKind ,sdiscoKindRev, DungeonGen.FreshDungeon{..}, modeKindId ) = St.evalState rnd dungeonSeed defState = defStateGlobal freshDungeon freshTotalDepth factionD cops scoreTable modeKindId discoKind defSer = emptyStateServer { srandom , srngs } putServer defSer modifyServer $ \ser -> ser { steamGear = teamGearOld , steamGearCur = teamGearOld , sclientStates = clientStatesOld -- reset later , sdiscoKindRev , sflavour } return $! defState -- Spawn initial actors. Clients should notice this, to set their leaders. populateDungeon :: forall m. MonadServerAtomic m => m () populateDungeon = do cops@COps{coTileSpeedup} <- getsState scops factionD <- getsState sfactionD curChalSer <- getsServer $ scurChalSer . soptions let nGt0 (_, n, _) = n > 0 ginitialWolf fact1 = if cwolf curChalSer && fhasUI (gkind fact1) then case filter nGt0 $ ginitial fact1 of [] -> [] (ln, _, grp) : _ -> [(ln, 1, grp)] else ginitial fact1 -- Keep the same order of factions as in roster. needInitialCrew = sortBy (comparing fst) $ filter (not . null . ginitialWolf . snd) $ EM.assocs factionD getEntryLevels (_, fact) = map (\(ln, _, _) -> toEnum ln) $ ginitialWolf fact arenas = ES.elems $ ES.fromList $ concatMap getEntryLevels needInitialCrew hasActorsOnArena lid (_, fact) = any (\(ln, _, _) -> toEnum ln == lid) $ ginitialWolf fact initialActorPositions :: LevelId -> m (LevelId, EM.EnumMap FactionId Point) initialActorPositions lid = do lvl <- getLevel lid let arenaFactions = map fst $ filter (hasActorsOnArena lid) needInitialCrew entryPoss <- rndToAction $ findEntryPoss cops lvl (length arenaFactions) when (length entryPoss < length arenaFactions) $ debugPossiblyPrint "Server: populateDungeon: failed to find enough distinct faction starting positions; some factions share positions" let usedPoss = EM.fromList $ zip arenaFactions $ cycle entryPoss return (lid, usedPoss) factionPositions <- EM.fromDistinctAscList <$> mapM initialActorPositions arenas let initialActors :: (FactionId, Faction) -> m () initialActors (fid3, fact3) = mapM_ (placeActors fid3) $ ginitialWolf fact3 placeActors :: FactionId -> (Int, Int, GroupName ItemKind) -> m () placeActors fid3 (ln, n, actorGroup) = do let lid = toEnum ln lvl <- getLevel lid let ppos = factionPositions EM.! lid EM.! fid3 validTile t = not $ Tile.isNoActor coTileSpeedup t -- This takes into account already spawned actors of this -- and other factions. If not enough space, some are skipped. psFree = nearbyFreePoints cops lvl validTile ppos ps = take n psFree when (length ps < n) $ debugPossiblyPrint "Server: populateDungeon: failed to find enough initial actor positions; some actors are not generated" localTime <- getsState $ getLocalTime lid forM_ ps $ \p -> do rndDelay <- rndToAction $ randomR (1, clipsInTurn - 1) let delta = timeDeltaScale (Delta timeClip) rndDelay rndTime = timeShift localTime delta maid <- addActorFromGroup actorGroup fid3 p lid rndTime case maid of Nothing -> error $ "can't spawn initial actors" `showFailure` (lid, fid3) Just aid -> do mleader <- getsState $ gleader . (EM.! fid3) . sfactionD -- Sleeping actor may become a leader, but it's quickly corrected. when (isNothing mleader) $ setFreshLeader fid3 aid placeItemsInDungeon factionPositions embedItemsInDungeon mapM_ initialActors needInitialCrew -- | Find starting postions for all factions. Try to make them distant -- from each other. Place as many of the factions, as possible, -- over stairs. Place the first faction(s) over escape(s) -- (we assume they are guardians of the escapes). -- This implies the inital factions (if any) start far from escapes. findEntryPoss :: COps -> Level -> Int -> Rnd [Point] findEntryPoss COps{cocave, coTileSpeedup} lvl@Level{lkind, larea, lstair, lescape} kRaw = do let lskip = CK.cskip $ okind cocave lkind k = kRaw + length lskip -- if @lskip@ is bogus, will be too large; OK (_, xspan, yspan) = spanArea larea factionDist = max xspan yspan - 10 dist !poss !cmin !l _ = all (\ !pos -> chessDist l pos > cmin) poss tryFind _ 0 = return [] tryFind !ps !n = do let ds = [ dist ps factionDist , dist ps $ factionDist `div` 2 , dist ps $ factionDist `div` 3 , dist ps $ max 5 $ factionDist `div` 5 , dist ps $ max 2 $ factionDist `div` 10 ] mp <- findPosTry2 500 lvl -- try really hard, for skirmish fairness (\_ !t -> Tile.isWalkable coTileSpeedup t && not (Tile.isNoActor coTileSpeedup t)) (take 2 ds) -- don't pick too close @isOftenActor@ locations (\_ !t -> Tile.isOftenActor coTileSpeedup t) ds case mp of Just np -> do nps <- tryFind (np : ps) (n - 1) return $! np : nps Nothing -> return [] sameStaircase :: [Point] -> Point -> Bool sameStaircase upStairs Point{..} = any (\(Point ux uy) -> uy == py && ux + 2 == px) upStairs upAndSomeDownStairs = fst lstair ++ filter (not . sameStaircase (fst lstair)) (snd lstair) skipIndexes ixs l = map snd $ filter (\(ix, _) -> ix `notElem` ixs) $ zip [0..] l let !_A = assert (k > 0 && factionDist > 0) () onEscapes = take k lescape onStairs = take (k - length onEscapes) upAndSomeDownStairs nk = k - length onEscapes - length onStairs -- Starting in the middle is too easy. found <- tryFind (middlePoint larea : onEscapes ++ onStairs) nk return $! skipIndexes lskip $ onEscapes ++ onStairs ++ found -- | Apply options that don't need a new game. applyDebug :: MonadServer m => m () applyDebug = do ServerOptions{..} <- getsServer soptionsNxt modifyServer $ \ser -> ser {soptions = (soptions ser) { sniff , sallClear , sdbgMsgSer , snewGameSer , sassertExplored , sdumpInitRngs , sclientOptions }}