namespace Ficdown.Parser.Player { using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Model.Parser; using Model.Player; using Model.Story; using Parser; internal class StateManager { private static Logger _logger = Logger.GetLogger(); private readonly Story _story; private readonly Dictionary _stateMatrix; private readonly int _sceneCount; private readonly int _actionCount; private BitArray _scenesSeenMask; private List _warnings { get; set; } public StateManager(Story story, List warnings) { _warnings = warnings; _story = story; var allScenes = _story.Scenes.SelectMany(s => s.Value); _sceneCount = allScenes.Max(s => s.Id); _scenesSeenMask = new BitArray(_sceneCount); // figure out which scenes can affect state var masked = 0; foreach(var scene in allScenes) { if(Utilities.GetInstance().ParseAnchors(scene.RawDescription).Any(a => a.Href.Toggles != null) || RegexLib.BlockQuotes.IsMatch(scene.RawDescription)) { _scenesSeenMask[scene.Id - 1] = true; masked++; } } _actionCount = _story.Actions.Count > 0 ? _story.Actions.Max(a => a.Value.Id) : 0; _stateMatrix = new Dictionary(); var state = 0; foreach ( var toggle in allScenes.SelectMany( sc => Utilities.GetInstance(_warnings, sc.Name, sc.LineNumber).ParseAnchors(sc.RawDescription) .SelectMany( a => a.Href != null && a.Href.Toggles != null ? a.Href.Toggles.Where(t => !_stateMatrix.ContainsKey(t)) : new string[] {}))) { _stateMatrix.Add(toggle, state++); } _logger.Debug($"{_sceneCount} scenes ({masked} can change state)."); _logger.Debug($"{_actionCount} actions."); _logger.Debug($"{_stateMatrix.Count()} states."); } public PageState InitialState { get { var scene = _story.Scenes[_story.FirstScene].Where(s => s.Conditions == null); if(scene.Count() == 0) throw new FicdownException(_story.Name, string.Format("Story links to undefined scene: {0}", _story.FirstScene)); if(scene.Count() > 1) _warnings.Add(new FicdownException(_story.Name, string.Format("Story links to scene that is defined more than once: {0}", _story.FirstScene))); return new PageState { Manager = this, Id = Guid.Empty, Links = new Dictionary(), State = new State { PlayerState = new BitArray(_stateMatrix.Keys.Count), ScenesSeen = new BitArray(_sceneCount), ActionsToShow = new BitArray(_actionCount), ActionFirstToggles = null }, AffectedState = new State { PlayerState = new BitArray(_stateMatrix.Keys.Count), ScenesSeen = new BitArray(_sceneCount), ActionsToShow = new BitArray(_actionCount), ActionFirstToggles = null }, Scene = scene.First(), StateMatrix = _stateMatrix }; } } public PageState ResolveNewState(Anchor anchor, PageState current) { var target = anchor.Href.Target ?? current.Scene.Key; var newState = ClonePage(current); newState.State.ScenesSeen[current.Scene.Id - 1] = true; List actionFirstToggles = null; if (anchor.Href.Toggles != null) { foreach (var toggle in anchor.Href.Toggles) { if (_story.Actions.ContainsKey(toggle)) { if(actionFirstToggles == null) actionFirstToggles = new List(); newState.State.ActionsToShow[_story.Actions[toggle].Id - 1] = true; if ( Utilities.GetInstance(_warnings, _story.Actions[toggle].Toggle, _story.Actions[toggle].LineNumber).ParseAnchors(_story.Actions[toggle].RawDescription) .Any(a => a.Href.Conditions != null && a.Href.Conditions.ContainsKey(toggle))) actionFirstToggles.Add(!current.State.PlayerState[_stateMatrix[toggle]]); } newState.State.PlayerState[_stateMatrix[toggle]] = true; } } newState.State.ActionFirstToggles = actionFirstToggles != null ? new BitArray(actionFirstToggles.ToArray()) : null; newState.Scene = GetScene(current.Scene.Name, anchor, target, newState.State.PlayerState); return newState; } public void ToggleStateOn(State state, string toggle, string blockName, int lineNumber, int colNumber) { if(_stateMatrix.ContainsKey(toggle)) state.PlayerState[_stateMatrix[toggle]] = true; else _warnings.Add(new FicdownException(blockName, string.Format("Conditional for undefined state: {0}", toggle), lineNumber, colNumber)); } public void ToggleSeenSceneOn(State state, int sceneId) { state.ScenesSeen[sceneId - 1] = true; } public string GetUniqueHash(State state, string sceneKey) { var combined = new bool[ state.PlayerState.Count + state.ScenesSeen.Count + state.ActionsToShow.Count + (state.ActionFirstToggles != null ? state.ActionFirstToggles.Count : 0)]; state.PlayerState.CopyTo(combined, 0); state.ScenesSeen.And(_scenesSeenMask).CopyTo(combined, state.PlayerState.Count); state.ActionsToShow.CopyTo(combined, state.PlayerState.Count + state.ScenesSeen.Count); if (state.ActionFirstToggles != null) state.ActionFirstToggles.CopyTo(combined, state.PlayerState.Count + state.ScenesSeen.Count + state.ActionsToShow.Count); var ba = new BitArray(combined); var byteSize = (int)Math.Ceiling(combined.Length / 8.0); var encoded = new byte[byteSize]; for(var i = 0; i < byteSize; i++) { encoded[i] = 0; } ba.CopyTo(encoded, 0); return string.Format("{0}=={1}", sceneKey, Convert.ToBase64String(encoded)); } public string GetCompressedHash(PageState page) { var compressed = new State { PlayerState = page.State.PlayerState.And(page.AffectedState.PlayerState), ScenesSeen = page.State.ScenesSeen.And(page.AffectedState.ScenesSeen), ActionsToShow = page.State.ActionsToShow, ActionFirstToggles = page.State.ActionFirstToggles }; return GetUniqueHash(compressed, page.Scene.Key); } private Scene GetScene(string blockName, Anchor anchor, string target, BitArray playerState) { if (_story.Scenes.ContainsKey(target)) { Scene newScene = null; foreach (var scene in _story.Scenes[target]) { if (ConditionsMatch(scene, playerState) && (newScene == null || newScene.Conditions == null || scene.Conditions.Count > newScene.Conditions.Count)) { newScene = scene; } } if (newScene == null) _warnings.Add(new FicdownException(blockName, string.Format("Link to scene that is undefined for conditionals: {0}", target), anchor.LineNumber, anchor.ColNumber)); return newScene; } _warnings.Add(new FicdownException(blockName, string.Format("Link to undefined scene: {0}", target), anchor.LineNumber, anchor.ColNumber)); return null; } private bool ConditionsMatch(Scene scene, BitArray playerState) { if (scene.Conditions == null) return true; scene.Conditions.ToList().ForEach(c => { if(!_stateMatrix.ContainsKey(c.Key)) _warnings.Add(new FicdownException(scene.Name, string.Format("Conditional for undefined state: {0}", c.Key), scene.LineNumber)); }); return scene.Conditions.Where(c => _stateMatrix.ContainsKey(c.Key)).All(c => playerState[_stateMatrix[c.Key]] == c.Value); } private PageState ClonePage(PageState page) { return new PageState { Manager = this, Id = Guid.NewGuid(), Links = new Dictionary(), State = new State { PlayerState = page.State.PlayerState.Clone() as BitArray, ScenesSeen = page.State.ScenesSeen.Clone() as BitArray, ActionsToShow = new BitArray(_actionCount) }, AffectedState = new State { PlayerState = new BitArray(_stateMatrix.Keys.Count), ScenesSeen = new BitArray(_sceneCount), ActionsToShow = new BitArray(_actionCount) }, StateMatrix = _stateMatrix }; } } }