// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic; using System.Collections.Concurrent; using System.Diagnostics; /// /// A state machine which is dynamically built, based on a . /// For creation, use the factory method . /// public sealed class ComboStateMachine : StateMachine { private static readonly ConcurrentDictionary StateCache = new(); private readonly TimeSpan _maximumCompletionTime; private DateTime _lastStartedCombo; /// /// Initializes a new instance of the class. /// /// The initial state. /// The final state. /// The maximum completion time for combos. private ComboStateMachine(ComboState initial, ComboState final, TimeSpan maximumCompletionTime) : base(initial) { this._maximumCompletionTime = maximumCompletionTime; this.InitialState = initial; this.FinalState = final; } /// /// Gets or sets the final state. /// When this state is achieved, the state machine will proceed to the /// to handle the next combo attempt. /// public ComboState FinalState { get; set; } /// /// Gets or sets the initial state. /// public ComboState InitialState { get; set; } /// /// Creates the specified combo definition, based on the . /// /// The combo definition. /// The created .. public static ComboStateMachine Create(SkillComboDefinition comboDefinition) { var states = GetOrCreateStates(comboDefinition); return new ComboStateMachine(states.Initial, states.Final, comboDefinition.MaximumCompletionTime); } /// /// Registers the skill to trigger potential state advancements. /// /// The performed skill. /// , if the combo completed; otherwise, . public async ValueTask RegisterSkillAsync(Skill skill) { if (DateTime.UtcNow - this._lastStartedCombo > this._maximumCompletionTime) { // If it took to long, reset to initial state. await this.TryAdvanceToAsync(this.InitialState).ConfigureAwait(false); } var nextPossibleSkillState = this.CurrentState?.PossibleTransitions?.OfType().FirstOrDefault(t => t.RequiredSkill == skill.GetBaseSkill()); if (nextPossibleSkillState is null) { // If it's the wrong skill, reset to initial state. await this.TryAdvanceToAsync(this.InitialState).ConfigureAwait(false); return false; } if (this.CurrentState == this.InitialState) { this._lastStartedCombo = DateTime.UtcNow; } await this.TryAdvanceToAsync(nextPossibleSkillState).ConfigureAwait(false); var canComplete = this.CurrentState?.PossibleTransitions?.Contains(this.FinalState) ?? false; if (canComplete && await this.TryAdvanceToAsync(this.FinalState).ConfigureAwait(false)) { await this.TryAdvanceToAsync(this.InitialState).ConfigureAwait(false); return true; } return false; } private static (ComboState Initial, ComboState Final) GetOrCreateStates(SkillComboDefinition comboDefinition) { return StateCache.GetOrAdd(comboDefinition, BuildStates); } private static (ComboState Initial, ComboState Final) BuildStates(SkillComboDefinition comboDefinition) { var initialState = new ComboState(Guid.NewGuid(), null) { Name = "Initial", PossibleTransitions = new List() }; var finalState = new ComboState(Guid.NewGuid(), null) { Name = "Finished", PossibleTransitions = new List { initialState } }; var statesPerStep = new Dictionary>(); foreach (var groupedSteps in comboDefinition.Steps.GroupBy(s => s.Order).OrderByDescending(s => s.Key)) { foreach (var step in groupedSteps) { var stepState = new ComboState(Guid.NewGuid(), step.Skill); stepState.Name = $"Step {step.Order}: {step.Skill?.Name}"; stepState.PossibleTransitions = new List(); stepState.PossibleTransitions.Add(initialState); if (step.Order == 1) { initialState.PossibleTransitions.Add(stepState); } if (step.IsFinalStep) { stepState.PossibleTransitions.Add(finalState); } else { if (statesPerStep.TryGetValue(step.Order + 1, out var nextSteps)) { nextSteps .OfType() .Where(p => p.RequiredSkill != step.Skill) .ForEach(stepState.PossibleTransitions.Add); } else { Debug.Fail("Inconsistent combo data"); } } if (!statesPerStep.TryGetValue(step.Order, out var list)) { list = new List(); statesPerStep[step.Order] = list; } list.Add(stepState); } } return (initialState, finalState); } }