diff --git a/DunGenPlus/DunGenPlus/Components/DoorwayCleanup.cs b/DunGenPlus/DunGenPlus/Components/DoorwayCleanup.cs index a357ec2..a7696d8 100644 --- a/DunGenPlus/DunGenPlus/Components/DoorwayCleanup.cs +++ b/DunGenPlus/DunGenPlus/Components/DoorwayCleanup.cs @@ -11,6 +11,7 @@ using UnityEngine; namespace DunGenPlus.Components { public class DoorwayCleanup : MonoBehaviour, IDungeonCompleteReceiver { + [Header("OBSOLUTE. Please use DoorwayScriptingParent")] [Header("Doorway References")] [Tooltip("The doorway reference.")] public Doorway doorway; diff --git a/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DCSConnectorBlockerSpawnedPrefab.cs b/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DCSConnectorBlockerSpawnedPrefab.cs index 55a47e1..80f383e 100644 --- a/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DCSConnectorBlockerSpawnedPrefab.cs +++ b/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DCSConnectorBlockerSpawnedPrefab.cs @@ -6,6 +6,8 @@ using System.Threading.Tasks; using UnityEngine; namespace DunGenPlus.Components.DoorwayCleanupScripting { + + [Obsolete("Please use DoorwayScriptingParent")] public class DCSConnectorBlockerSpawnedPrefab : DoorwayCleanupScript { public enum Action { SwitchToConnector, SwitchToBlocker }; diff --git a/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DCSRemoveDoorwayConnectedDoorway.cs b/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DCSRemoveDoorwayConnectedDoorway.cs index 02b1058..8c3e37b 100644 --- a/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DCSRemoveDoorwayConnectedDoorway.cs +++ b/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DCSRemoveDoorwayConnectedDoorway.cs @@ -6,17 +6,20 @@ using System.Threading.Tasks; using UnityEngine; namespace DunGenPlus.Components.DoorwayCleanupScripting { + + [Obsolete("Please use DoorwayScriptingParent")] public class DCSRemoveDoorwayConnectedDoorway : DoorwayCleanupScriptDoorwayCompare { [Header("Removes Doorway Gameobject\nif the neighboring doorway's priority matches the operation comparison")] [Header("Operation Comparison")] public int doorwayPriority; + public int doorwayPriorityB; public Operation operation = Operation.Equal; public override void Cleanup(DoorwayCleanup parent) { var doorway = parent.doorway; if (doorway.connectedDoorway == null) return; - var result = GetOperation(operation).Invoke(doorway.connectedDoorway, doorwayPriority); + var result = GetOperation(operation).Invoke(doorway.connectedDoorway, new Arguments(doorwayPriority, doorwayPriorityB)); if (result) { parent.SwitchDoorwayGameObject(false); diff --git a/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DCSRemoveDoorwaySpawnedPrefab.cs b/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DCSRemoveDoorwaySpawnedPrefab.cs index 2f2dbbe..98a3bae 100644 --- a/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DCSRemoveDoorwaySpawnedPrefab.cs +++ b/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DCSRemoveDoorwaySpawnedPrefab.cs @@ -6,6 +6,8 @@ using System.Threading.Tasks; using UnityEngine; namespace DunGenPlus.Components.DoorwayCleanupScripting { + + [Obsolete("Please use DoorwayScriptingParent")] public class DCSRemoveDoorwaySpawnedPrefab : DoorwayCleanupScript { [Header("Removes Doorway Gameobject\nif Doorway instantiates a Connector/Blocker prefab with the target's name")] diff --git a/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DCSRemoveGameObjectsConnectedDoorway.cs b/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DCSRemoveGameObjectsConnectedDoorway.cs index 82a0f1b..58f6699 100644 --- a/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DCSRemoveGameObjectsConnectedDoorway.cs +++ b/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DCSRemoveGameObjectsConnectedDoorway.cs @@ -6,12 +6,14 @@ using System.Threading.Tasks; using UnityEngine; namespace DunGenPlus.Components.DoorwayCleanupScripting { + [Obsolete("Please use DoorwayScriptingParent")] public class DCSRemoveGameObjectsConnectedDoorway : DoorwayCleanupScriptDoorwayCompare { [Header("Removes target GameObjects\nif the neighboring doorway's priority matches the operation comparison")] [Header("Operation Comparison")] public int doorwayPriority; + public int doorwayPriorityB; public Operation operation = Operation.Equal; [Header("Targets")] @@ -20,7 +22,7 @@ namespace DunGenPlus.Components.DoorwayCleanupScripting { public override void Cleanup(DoorwayCleanup parent) { var doorway = parent.doorway; if (doorway.connectedDoorway == null) return; - var result = GetOperation(operation).Invoke(doorway.connectedDoorway, doorwayPriority); + var result = GetOperation(operation).Invoke(doorway.connectedDoorway, new Arguments(doorwayPriority, doorwayPriorityB)); if (result) { foreach(var t in targets) t.SetActive(false); } diff --git a/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DoorwayCleanupScript.cs b/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DoorwayCleanupScript.cs index edd1b49..15374ea 100644 --- a/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DoorwayCleanupScript.cs +++ b/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DoorwayCleanupScript.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using UnityEngine; namespace DunGenPlus.Components.DoorwayCleanupScripting { + [Obsolete("Please use DoorwayScriptingParent")] public abstract class DoorwayCleanupScript : MonoBehaviour { public abstract void Cleanup(DoorwayCleanup parent); diff --git a/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DoorwayCleanupScriptDoorwayCompare.cs b/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DoorwayCleanupScriptDoorwayCompare.cs index 8fef87c..beee7b2 100644 --- a/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DoorwayCleanupScriptDoorwayCompare.cs +++ b/DunGenPlus/DunGenPlus/Components/DoorwayCleanupScripting/DoorwayCleanupScriptDoorwayCompare.cs @@ -4,13 +4,44 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using UnityEngine; namespace DunGenPlus.Components.DoorwayCleanupScripting { + [Obsolete("Please use DoorwayScriptingParent")] public abstract class DoorwayCleanupScriptDoorwayCompare : DoorwayCleanupScript { - public enum Operation { Equal, NotEqual, LessThan, GreaterThan } + public enum Operation { + [InspectorName("Equal (target == value)")] + Equal, + [InspectorName("NotEqual (target != value)")] + NotEqual, + [InspectorName("LessThan (target < value)")] + LessThan, + [InspectorName("GreaterThan (target > value)")] + GreaterThan, + [InspectorName("LessThanEq (target <= value)")] + LessThanEq, + [InspectorName("GreaterThanEw (target >= value)")] + GreaterThanEq, + [InspectorName("Between (value < target < valueB)")] + Between, + [InspectorName("BetweenEq (value <= target <= valueB)")] + BetweenEq + } - public Func GetOperation(Operation operation){ + public struct Arguments{ + public int parameterA; + public int parameterB; + + public Arguments(int parameterA, int parameterB){ + this.parameterA = parameterA; + this.parameterB = parameterB; + } + + public static explicit operator Arguments((int a, int b) pair) => new Arguments(pair.a, pair.b); + } + + public Func GetOperation(Operation operation){ switch(operation){ case Operation.Equal: return EqualOperation; @@ -20,24 +51,48 @@ namespace DunGenPlus.Components.DoorwayCleanupScripting { return LessThanOperation; case Operation.GreaterThan: return GreaterThanOperation; + case Operation.LessThanEq: + return LessThanEqualOperation; + case Operation.GreaterThanEq: + return GreaterThanEqualOperation; + case Operation.Between: + return BetweenOperation; + case Operation.BetweenEq: + return BetweenEqualOperation; } return null; } - public bool EqualOperation(Doorway other, int doorwayPriority){ - return other.DoorPrefabPriority == doorwayPriority; + public bool EqualOperation(Doorway other, Arguments arguments){ + return other.DoorPrefabPriority == arguments.parameterA; } - public bool NotEqualOperation(Doorway other, int doorwayPriority){ - return other.DoorPrefabPriority != doorwayPriority; + public bool NotEqualOperation(Doorway other, Arguments arguments){ + return other.DoorPrefabPriority != arguments.parameterA; } - public bool LessThanOperation(Doorway other, int doorwayPriority){ - return other.DoorPrefabPriority < doorwayPriority; + public bool LessThanOperation(Doorway other, Arguments arguments){ + return other.DoorPrefabPriority < arguments.parameterA; } - public bool GreaterThanOperation(Doorway other, int doorwayPriority){ - return other.DoorPrefabPriority > doorwayPriority; + public bool GreaterThanOperation(Doorway other, Arguments arguments){ + return other.DoorPrefabPriority > arguments.parameterA; + } + + public bool LessThanEqualOperation(Doorway other, Arguments arguments){ + return other.DoorPrefabPriority <= arguments.parameterA; + } + + public bool GreaterThanEqualOperation(Doorway other, Arguments arguments){ + return other.DoorPrefabPriority >= arguments.parameterA; + } + + public bool BetweenOperation(Doorway other, Arguments arguments){ + return arguments.parameterA < other.DoorPrefabPriority && other.DoorPrefabPriority < arguments.parameterB; + } + + public bool BetweenEqualOperation(Doorway other, Arguments arguments){ + return arguments.parameterA <= other.DoorPrefabPriority && other.DoorPrefabPriority <= arguments.parameterB; } } diff --git a/DunGenPlus/DunGenPlus/Components/Scripting/DoorwayScriptingParent.cs b/DunGenPlus/DunGenPlus/Components/Scripting/DoorwayScriptingParent.cs new file mode 100644 index 0000000..46b48c6 --- /dev/null +++ b/DunGenPlus/DunGenPlus/Components/Scripting/DoorwayScriptingParent.cs @@ -0,0 +1,103 @@ +using DunGen; +using DunGenPlus.Managers; +using Soukoku.ExpressionParser; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace DunGenPlus.Components.Scripting { + + public class DoorwayScriptingParent : DunGenPlusScriptingParent { + + [Header("Scripting Debug")] + public Doorway connectedDoorwayDebug; + + public override void Awake(){ + base.Awake(); + + if (targetReference == null) return; + + // steal the scene objects from the doorway and clear them + // before the doorway messes with them before us + // psycho energy + AddNamedReference("connectors", targetReference.ConnectorSceneObjects); + targetReference.ConnectorSceneObjects = new List(); + + AddNamedReference("blockers", targetReference.BlockerSceneObjects); + targetReference.BlockerSceneObjects = new List(); + } + + public override void Call(){ + if (targetReference == null) return; + + // start up like in original + var isConnected = targetReference.connectedDoorway != null; + SetNamedGameObjectState("connectors", isConnected); + SetNamedGameObjectState("blockers", !isConnected); + + base.Call(); + } + + Doorway GetDoorway(string name){ + switch(name) { + case "self": + return targetReference; + case "other": + return InDebugMode ? connectedDoorwayDebug : targetReference.ConnectedDoorway; + default: + Utils.Utility.PrintLog($"{name} is not valid doorway expression. Please use 'self' or 'other'", BepInEx.Logging.LogLevel.Error); + return null; + + } + } + + public override EvaluationContext CreateContext() { + var context = new EvaluationContext(GetFields); + context.RegisterFunction("doorwaySpawnedGameObject", new FunctionRoutine(2, doorwaySpawnedGameObjectFunction)); + + return context; + } + + ExpressionToken doorwaySpawnedGameObjectFunction(EvaluationContext context, ExpressionToken[] parameters) { + var targetName = parameters[0].Value; + var target = GetDoorway(targetName); + if (target != null) { + var name = parameters[1].Value; + foreach(Transform child in target.transform) { + if (child.gameObject.activeSelf && child.name.Contains(name)) return ExpressionToken.True; + } + } + return ExpressionToken.False; + } + + (object, ValueTypeHint) GetFields(string field) { + var split = field.Split('.'); + + if (split.Length <= 1) { + Utils.Utility.PrintLog($"{field} is not a valid field", BepInEx.Logging.LogLevel.Error); + return (0, ValueTypeHint.Auto); + } + + var targetName = split[0]; + var target = GetDoorway(targetName); + var getter = split[1]; + + switch(getter) { + case "priority": + if (target != null){ + return (target.DoorPrefabPriority, ValueTypeHint.Auto); + } + return (0, ValueTypeHint.Auto); + case "exists": + return (target != null, ValueTypeHint.Auto); + default: + Utils.Utility.PrintLog($"{getter} is not a valid getter", BepInEx.Logging.LogLevel.Error); + return (0, ValueTypeHint.Auto); + } + } + + } +} diff --git a/DunGenPlus/DunGenPlus/Components/Scripting/DunGenPlusScript.cs b/DunGenPlus/DunGenPlus/Components/Scripting/DunGenPlusScript.cs new file mode 100644 index 0000000..889e024 --- /dev/null +++ b/DunGenPlus/DunGenPlus/Components/Scripting/DunGenPlusScript.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using Soukoku.ExpressionParser; +using DunGen; + +namespace DunGenPlus.Components.Scripting { + + public enum ScriptActionType { + SwitchToConnector, + SwitchToBlocker, + SetNamedReferenceState + } + + [System.Serializable] + public struct ScriptAction { + public ScriptActionType type; + public string namedReference; + public bool boolValue; + + public void CallAction(IDunGenScriptingParent parent){ + switch(type){ + case ScriptActionType.SwitchToConnector: + parent.SetNamedGameObjectState("connectors", true); + parent.SetNamedGameObjectState("blockers", false); + break; + case ScriptActionType.SwitchToBlocker: + parent.SetNamedGameObjectState("connectors", false); + parent.SetNamedGameObjectState("blockers", true); + break; + case ScriptActionType.SetNamedReferenceState: + parent.SetNamedGameObjectState(namedReference, boolValue); + break; + } + } + } + + public class DunGenPlusScript : MonoBehaviour { + + public static bool InDebugMode = false; + + public string expression; + public List actions; + + public bool EvaluateExpression(IDunGenScriptingParent parent){ + var context = parent.CreateContext(); + var evaluator = new Evaluator(context); + try { + InDebugMode = false; + var results = evaluator.Evaluate(expression, true); + return results.ToDouble(context) > 0; + } catch (Exception e) { + Plugin.logger.LogError($"Expression [{expression}] could not be parsed. Returning false"); + Plugin.logger.LogError(e.ToString()); + } + + return false; + } + + [ContextMenu("Verify")] + public void VerifyExpression(){ + var context = GetComponent().CreateContext(); + var evaluator = new Evaluator(context); + try { + InDebugMode = true; + var results = evaluator.Evaluate(expression, false); + Debug.Log($"Expression parsed successfully: {results.ToString()} ({evaluator.ConvertTokenToFalseTrue(results).ToString()})"); + } catch (Exception e) { + Debug.LogError($"Expression [{expression}] could not be parsed"); + Debug.LogError(e.ToString()); + } + } + + public void Call(IDunGenScriptingParent parent){ + if (EvaluateExpression(parent)){ + foreach(var action in actions) action.CallAction(parent); + } + } + + } +} diff --git a/DunGenPlus/DunGenPlus/Components/Scripting/DunGenPlusScriptingParent.cs b/DunGenPlus/DunGenPlus/Components/Scripting/DunGenPlusScriptingParent.cs new file mode 100644 index 0000000..93d33ad --- /dev/null +++ b/DunGenPlus/DunGenPlus/Components/Scripting/DunGenPlusScriptingParent.cs @@ -0,0 +1,152 @@ +using DunGen; +using DunGenPlus.Managers; +using Soukoku.ExpressionParser; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace DunGenPlus.Components.Scripting { + + public enum OverrideGameObjectState { + None, + Active, + Disabled + } + + [System.Serializable] + public class NamedGameObjectReference { + public string name; + public List gameObjects; + public OverrideGameObjectState overrideState; + + public NamedGameObjectReference(string name, List gameObjects){ + this.name = name; + this.gameObjects = gameObjects; + } + + public void SetState(bool state){ + foreach(var g in gameObjects){ + g?.SetActive(state); + } + } + + public void FixOverrideState(){ + if (overrideState == OverrideGameObjectState.None) return; + SetState(overrideState == OverrideGameObjectState.Active); + } + + public void DestroyInactiveGameObjects(){ + foreach(var g in gameObjects){ + if (g && !g.activeSelf) UnityEngine.Object.DestroyImmediate(g, false); + } + } + } + + public enum DunGenScriptingHook { + SetLevelObjectVariables, + OnMainEntranceTeleportSpawned + } + + + public interface IDunGenScriptingParent { + + DunGenScriptingHook GetScriptingHook { get; } + + void Call(); + + List GetNamedReferences { get; } + + void AddNamedReference(string name, List gameObjects); + + void SetNamedGameObjectState(string name, bool state); + void SetNamedGameObjectOverrideState(string name, OverrideGameObjectState state); + + EvaluationContext CreateContext(); + + } + + public abstract class DunGenPlusScriptingParent : MonoBehaviour, IDunGenScriptingParent, IDungeonCompleteReceiver where T: Component { + + public static bool InDebugMode => DunGenPlusScript.InDebugMode; + + [Header("REQUIRED")] + [Tooltip("The target reference.")] + public T targetReference; + public DunGenScriptingHook callHook = DunGenScriptingHook.OnMainEntranceTeleportSpawned; + + [Header("Named References")] + [Tooltip("Provide a variable name for a list of gameObjects. Used in DunGenScripting.")] + public List namedReferences = new List(); + public Dictionary namedDictionary = new Dictionary(); + + public DunGenScriptingHook GetScriptingHook => callHook; + public List GetNamedReferences => namedReferences; + + public void OnDungeonComplete(Dungeon dungeon) { + //SetBlockers(true); + //Debug.Log("ONDUNGEONCOMPLETE"); + DoorwayManager.AddDunGenScriptHook(this); + } + + public virtual void Awake(){ + foreach(var r in namedReferences){ + namedDictionary.Add(r.name, r); + } + } + + public virtual void Call() { + // call scripts + var scripts = GetComponentsInChildren(); + foreach(var c in scripts) c.Call(this); + + // apply any overrides + foreach(var n in namedReferences) n.FixOverrideState(); + + // clean up like in original + foreach(var n in namedReferences) DestroyInactiveGameObjects(n.gameObjects); + } + + public void AddNamedReference(string name, List gameObjects) { + var item = new NamedGameObjectReference(name, gameObjects); + namedReferences.Add(item); + namedDictionary.Add(name, item); + } + + public void SetNamedGameObjectState(string name, bool state){ + if (namedDictionary.TryGetValue(name, out var obj)){ + obj.SetState(state); + } else { + Plugin.logger.LogError($"Named reference: {name} does not exist"); + } + } + + public void SetNamedGameObjectOverrideState(string name, OverrideGameObjectState state){ + if (namedDictionary.TryGetValue(name, out var obj)){ + obj.overrideState = state; + } + } + + public void DestroyInactiveGameObjects(IEnumerable gameObjects){ + foreach(var g in gameObjects) { + if (g && !g.activeSelf) { + UnityEngine.Object.DestroyImmediate(g, false); + } + } + } + + protected bool CheckIfNotNull(object target, string name){ + if (target == null) { + Utils.Utility.PrintLog($"{name} was null", BepInEx.Logging.LogLevel.Error); + return false; + } + return true; + } + + public abstract EvaluationContext CreateContext(); + + } +} diff --git a/DunGenPlus/DunGenPlus/DevTools/Panels/DunGenPlusPanel.cs b/DunGenPlus/DunGenPlus/DevTools/Panels/DunGenPlusPanel.cs index 60ce84c..5782d40 100644 --- a/DunGenPlus/DunGenPlus/DevTools/Panels/DunGenPlusPanel.cs +++ b/DunGenPlus/DunGenPlus/DevTools/Panels/DunGenPlusPanel.cs @@ -32,6 +32,7 @@ namespace DunGenPlus.DevTools.Panels { private GameObject forcedTilesParentGameobject; private GameObject branchLoopBoostParentGameobject; private GameObject maxShadowsParentGameobject; + public bool eventCallbackValue = true; public override void AwakeCall() { Instance = this; @@ -70,6 +71,7 @@ namespace DunGenPlus.DevTools.Panels { } internal const string ActivateDunGenPlusTooltip = "If disabled, the dungeon generation will ignore this DunGenPlusExtender asset and simply create a vanilla dungeon instead when generating."; + internal const string EventCallbackScenarioTooltip = "Sets the EventCallbackScenario.IsDevDebug value"; public void SetupPanel() { selectedExtenderer = API.GetDunGenExtender(selectedDungeonFlow); @@ -77,6 +79,7 @@ namespace DunGenPlus.DevTools.Panels { var parentTransform = selectedListGameObject.transform; var properties = selectedExtenderer.Properties; manager.CreateBoolInputField(parentTransform, ("Activate DunGenPlus", ActivateDunGenPlusTooltip), selectedExtenderer.Active, SetActivateDunGenPlus); + manager.CreateBoolInputField(parentTransform, ("EventCallbackScenario state", EventCallbackScenarioTooltip), eventCallbackValue, SetDebugCallbackState); manager.CreateSpaceUIField(parentTransform); var mainPathTransform = manager.CreateVerticalLayoutUIField(parentTransform); @@ -168,6 +171,10 @@ namespace DunGenPlus.DevTools.Panels { selectedExtenderer.Active = state; } + public void SetDebugCallbackState(bool state){ + eventCallbackValue = state; + } + public void SetMainPathCount(int value) { selectedExtenderer.Properties.MainPathProperties.MainPathCount = value; mainPathParentGameobject.SetActive(value > 1); diff --git a/DunGenPlus/DunGenPlus/DunGenPlus.csproj b/DunGenPlus/DunGenPlus/DunGenPlus.csproj index afe53a0..cbf03b3 100644 --- a/DunGenPlus/DunGenPlus/DunGenPlus.csproj +++ b/DunGenPlus/DunGenPlus/DunGenPlus.csproj @@ -56,6 +56,9 @@ False ..\..\..\Libraries\MonoMod.Utils.dll + + ..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll + @@ -157,7 +160,24 @@ + + + + + + + + + + + + + + + + + diff --git a/DunGenPlus/DunGenPlus/DunGenPlus/DunGenPlus.dll b/DunGenPlus/DunGenPlus/DunGenPlus/DunGenPlus.dll index f168764..9c9ce47 100644 Binary files a/DunGenPlus/DunGenPlus/DunGenPlus/DunGenPlus.dll and b/DunGenPlus/DunGenPlus/DunGenPlus/DunGenPlus.dll differ diff --git a/DunGenPlus/DunGenPlus/DunGenPlus/DunGenPlusEditor.dll b/DunGenPlus/DunGenPlus/DunGenPlus/DunGenPlusEditor.dll index 6bf3722..95bfd19 100644 Binary files a/DunGenPlus/DunGenPlus/DunGenPlus/DunGenPlusEditor.dll and b/DunGenPlus/DunGenPlus/DunGenPlus/DunGenPlusEditor.dll differ diff --git a/DunGenPlus/DunGenPlus/ExpressionParser/EvaluationContext.cs b/DunGenPlus/DunGenPlus/ExpressionParser/EvaluationContext.cs new file mode 100644 index 0000000..9faba28 --- /dev/null +++ b/DunGenPlus/DunGenPlus/ExpressionParser/EvaluationContext.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Soukoku.ExpressionParser +{ + /// + /// Context for storing and returning values during an expression evaluation. + /// + public class EvaluationContext + { + static Dictionary BuiltInFunctions = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "pow", new FunctionRoutine(2, (ctx, args)=> + new ExpressionToken( Math.Pow(args[0].ToDouble(ctx), args[1].ToDouble(ctx)).ToString(ctx.FormatCulture))) }, + { "sin", new FunctionRoutine(1, (ctx, args)=> + new ExpressionToken( Math.Sin(args[0].ToDouble(ctx)).ToString(ctx.FormatCulture)))}, + { "cos", new FunctionRoutine(1, (ctx, args)=> + new ExpressionToken( Math.Cos(args[0].ToDouble(ctx)).ToString(ctx.FormatCulture)))}, + { "tan", new FunctionRoutine(1, (ctx, args)=> + new ExpressionToken( Math.Tan(args[0].ToDouble(ctx)).ToString(ctx.FormatCulture)))} + }; + + static readonly Dictionary __staticFuncs = new Dictionary(StringComparer.OrdinalIgnoreCase); + readonly Dictionary _instanceFuncs = new Dictionary(StringComparer.OrdinalIgnoreCase); + + Func _fieldLookup; + + /// + /// Initializes a new instance of the class. + /// + public EvaluationContext() { } + + /// + /// Initializes a new instance of the class. + /// + /// The field value lookup routine. + public EvaluationContext(Func fieldLookupRoutine) + { + _fieldLookup = fieldLookupRoutine; + } + + /// + /// Resolves the field value. + /// + /// The field. + /// + public (object Value, ValueTypeHint TypeHint) ResolveFieldValue(string field) + { + if (_fieldLookup != null) { return _fieldLookup(field); } + return OnResolveFieldValue(field); + } + + readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private CultureInfo _formatCulture = null; + + /// + /// Gets/sets the culture used to parse/format expressions. + /// Defaults to en-US for certain reasons. + /// + public CultureInfo FormatCulture + { + get { return _formatCulture ?? _usCulture; } + set { _formatCulture = value; } + } + + + /// + /// Gets the field value. + /// + /// The field. + /// + protected virtual (object Value, ValueTypeHint TypeHint) OnResolveFieldValue(string field) + { + return (string.Empty, ValueTypeHint.Auto); + } + + /// + /// Registers a custom function globally. + /// + /// Name of the function. + /// The information. + public static void RegisterGlobalFunction(string functionName, FunctionRoutine info) + { + __staticFuncs[functionName] = info; + } + + /// + /// Registers a custom function with this context instance. + /// + /// Name of the function. + /// The information. + public void RegisterFunction(string functionName, FunctionRoutine info) + { + _instanceFuncs[functionName] = info; + } + + /// + /// Gets the function registered with this context. + /// + /// Name of the function. + /// + /// + public FunctionRoutine GetFunction(string functionName) + { + if (_instanceFuncs.ContainsKey(functionName)) + { + return _instanceFuncs[functionName]; + } + if (__staticFuncs.ContainsKey(functionName)) + { + return __staticFuncs[functionName]; + } + if (BuiltInFunctions.ContainsKey(functionName)) + { + return BuiltInFunctions[functionName]; + } + return OnGetFunction(functionName) ?? + throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Function \"{0}\" is not supported.", functionName)); + } + + + /// + /// Gets the function registered with this context. + /// + /// Name of the function. + /// + /// + protected virtual FunctionRoutine OnGetFunction(string functionName) + { + return null; + } + } +} diff --git a/DunGenPlus/DunGenPlus/ExpressionParser/Evaluator.cs b/DunGenPlus/DunGenPlus/ExpressionParser/Evaluator.cs new file mode 100644 index 0000000..7b0b53b --- /dev/null +++ b/DunGenPlus/DunGenPlus/ExpressionParser/Evaluator.cs @@ -0,0 +1,438 @@ +using Soukoku.ExpressionParser.Parsing; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Text; +using UnityEngine.Events; + +namespace Soukoku.ExpressionParser +{ + /// + /// An expression evaluator. + /// + public class Evaluator + { + EvaluationContext _context; + Stack _stack; + + /// + /// Initializes a new instance of the class. + /// + /// The context. + /// context + public Evaluator(EvaluationContext context) + { + _context = context ?? throw new ArgumentNullException("context"); + } + + /// + /// Evaluates the specified input expression. + /// + /// The input expression (infix). + /// if set to true then the result will be coersed to boolean true/false if possible. + /// Anything not "false", "0", or "" is considered true. + /// + /// Unbalanced expression. + /// + public ExpressionToken Evaluate(string input, bool coerseToBoolean = false) + { + if (string.IsNullOrWhiteSpace(input)) + { + return coerseToBoolean ? ExpressionToken.False : new ExpressionToken(input); + } + + var tokens = new InfixToPostfixTokenizer().Tokenize(input); + // resolve field value and type hints here + foreach (var token in tokens.Where(tk => tk.TokenType == ExpressionTokenType.Field)) + { + token.FieldValue = _context.ResolveFieldValue(token.Value); + } + + var reader = new ListReader(tokens); + + // from https://en.wikipedia.org/wiki/Reverse_Polish_notation + _stack = new Stack(); + while (!reader.IsEnd) + { + var tk = reader.Read(); + switch (tk.TokenType) + { + case ExpressionTokenType.Value: + case ExpressionTokenType.DoubleQuoted: + case ExpressionTokenType.SingleQuoted: + case ExpressionTokenType.Field: + _stack.Push(tk); + break; + case ExpressionTokenType.Operator: + HandleOperator(tk.OperatorType); + break; + case ExpressionTokenType.Function: + HandleFunction(tk.Value); + break; + } + } + + if (_stack.Count == 1) + { + var res = _stack.Pop(); + if (coerseToBoolean) + { + return ConvertTokenToFalseTrue(res); + } + return res; + + //if (res.IsNumeric()) + //{ + // return res; + //} + //else if (IsTrue(res.Value)) + //{ + // return ExpressionToken.True; + //} + } + throw new NotSupportedException("Unbalanced expression."); + } + + public ExpressionToken ConvertTokenToFalseTrue(ExpressionToken token){ + // changed form Value to ToString() so fields by themselves evalute properly + if (IsFalse(token.ToString())) + { + return ExpressionToken.False; + } + return ExpressionToken.True; + } + + private void HandleFunction(string functionName) + { + var fun = _context.GetFunction(functionName); + var args = new Stack(fun.ArgumentCount); + + while (args.Count < fun.ArgumentCount) + { + args.Push(_stack.Pop()); + } + + _stack.Push(fun.Evaluate(_context, args.ToArray())); + } + + #region operator handling + + static bool IsDate(string lhs, string rhs, out DateTime lhsDate, out DateTime rhsDate) + { + lhsDate = default(DateTime); + rhsDate = default(DateTime); + + if (DateTime.TryParse(lhs, out lhsDate)) + { + DateTime.TryParse(rhs, out rhsDate); + return true; + } + else if (DateTime.TryParse(rhs, out rhsDate)) + { + DateTime.TryParse(lhs, out lhsDate); + return true; + } + return false; + } + bool IsNumber(string lhs, string rhs, out decimal lhsNumber, out decimal rhsNumber) + { + lhsNumber = 0; + rhsNumber = 0; + + var islNum = decimal.TryParse(lhs, ExpressionToken.NumberParseStyle, _context.FormatCulture, out lhsNumber); + var isrNum = decimal.TryParse(rhs, ExpressionToken.NumberParseStyle, _context.FormatCulture, out rhsNumber); + + return islNum && isrNum; + } + static bool IsBoolean(string lhs, string rhs, out bool lhsBool, out bool rhsBool) + { + bool lIsBool = false; + bool rIsBool = false; + lhsBool = false; + rhsBool = false; + + if (!string.IsNullOrEmpty(lhs)) + { + if (string.Equals(lhs, "true", StringComparison.OrdinalIgnoreCase) || lhs == "1") + { + lhsBool = true; + lIsBool = true; + } + else if (string.Equals(lhs, "false", StringComparison.OrdinalIgnoreCase) || lhs == "0") + { + lIsBool = true; + } + } + + if (lIsBool && !string.IsNullOrEmpty(rhs)) + { + if (string.Equals(rhs, "true", StringComparison.OrdinalIgnoreCase) || rhs == "1") + { + rhsBool = true; + rIsBool = true; + } + else if (string.Equals(rhs, "false", StringComparison.OrdinalIgnoreCase) || rhs == "0") + { + rIsBool = true; + } + } + return lIsBool && rIsBool; + + //lhsBool = false; + //rhsBool = false; + + //if (string.Equals(lhs, "true", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(rhs)) + //{ + // lhsBool = true; + // rhsBool = IsTrue(rhs); + // return true; + //} + //else if (string.Equals(lhs, "false", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(rhs)) + //{ + // rhsBool = IsTrue(rhs); + // return true; + //} + //else if (string.Equals(rhs, "true", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(lhs)) + //{ + // rhsBool = true; + // lhsBool = IsTrue(lhs); + // return true; + //} + //else if (string.Equals(rhs, "false", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(lhs)) + //{ + // lhsBool = IsTrue(lhs); + // return true; + //} + //return false; + } + + private void HandleOperator(OperatorType op) + { + switch (op) + { + case OperatorType.Addition: + BinaryNumberOperation((a, b) => a + b); + break; + case OperatorType.Subtraction: + BinaryNumberOperation((a, b) => a - b); + break; + case OperatorType.Multiplication: + BinaryNumberOperation((a, b) => a * b); + break; + case OperatorType.Division: + BinaryNumberOperation((a, b) => a / b); + break; + case OperatorType.Modulus: + BinaryNumberOperation((a, b) => a % b); + break; + // these logical comparision can be date/num/string! + case OperatorType.LessThan: + var rhsToken = _stack.Pop(); + var lhsToken = _stack.Pop(); + var rhs = rhsToken.ToString(); + var lhs = lhsToken.ToString(); + + if (IsNumber(lhs, rhs, out decimal lhsNum, out decimal rhsNum)) + { + _stack.Push(lhsNum < rhsNum ? ExpressionToken.True : ExpressionToken.False); + } + else if (IsDate(lhs, rhs, out DateTime lhsDate, out DateTime rhsDate)) + { + _stack.Push(lhsDate < rhsDate ? ExpressionToken.True : ExpressionToken.False); + } + else + { + _stack.Push(string.Compare(lhs, rhs, StringComparison.OrdinalIgnoreCase) < 0 ? ExpressionToken.True : ExpressionToken.False); + } + break; + case OperatorType.LessThanOrEqual: + rhsToken = _stack.Pop(); + lhsToken = _stack.Pop(); + rhs = rhsToken.ToString(); + lhs = lhsToken.ToString(); + + if (IsNumber(lhs, rhs, out lhsNum, out rhsNum)) + { + _stack.Push(lhsNum <= rhsNum ? ExpressionToken.True : ExpressionToken.False); + } + else if (IsDate(lhs, rhs, out DateTime lhsDate, out DateTime rhsDate)) + { + _stack.Push(lhsDate <= rhsDate ? ExpressionToken.True : ExpressionToken.False); + } + else + { + _stack.Push(string.Compare(lhs, rhs, StringComparison.OrdinalIgnoreCase) <= 0 ? ExpressionToken.True : ExpressionToken.False); + } + break; + case OperatorType.GreaterThan: + rhsToken = _stack.Pop(); + lhsToken = _stack.Pop(); + rhs = rhsToken.ToString(); + lhs = lhsToken.ToString(); + + if (IsNumber(lhs, rhs, out lhsNum, out rhsNum)) + { + _stack.Push(lhsNum > rhsNum ? ExpressionToken.True : ExpressionToken.False); + } + else if (IsDate(lhs, rhs, out DateTime lhsDate, out DateTime rhsDate)) + { + _stack.Push(lhsDate > rhsDate ? ExpressionToken.True : ExpressionToken.False); + } + else + { + _stack.Push(string.Compare(lhs, rhs, StringComparison.OrdinalIgnoreCase) > 0 ? ExpressionToken.True : ExpressionToken.False); + } + break; + case OperatorType.GreaterThanOrEqual: + rhsToken = _stack.Pop(); + lhsToken = _stack.Pop(); + rhs = rhsToken.ToString(); + lhs = lhsToken.ToString(); + + if (IsNumber(lhs, rhs, out lhsNum, out rhsNum)) + { + _stack.Push(lhsNum >= rhsNum ? ExpressionToken.True : ExpressionToken.False); + } + else if (IsDate(lhs, rhs, out DateTime lhsDate, out DateTime rhsDate)) + { + _stack.Push(lhsDate >= rhsDate ? ExpressionToken.True : ExpressionToken.False); + } + else + { + _stack.Push(string.Compare(lhs, rhs, StringComparison.OrdinalIgnoreCase) >= 0 ? ExpressionToken.True : ExpressionToken.False); + } + break; + case OperatorType.Equal: + rhsToken = _stack.Pop(); + lhsToken = _stack.Pop(); + rhs = rhsToken.ToString(); + lhs = lhsToken.ToString(); + + if (IsBoolean(lhs, rhs, out bool lhsBool, out bool rhsBool)) + { + _stack.Push(lhsBool == rhsBool ? ExpressionToken.True : ExpressionToken.False); + } + else if ((AllowAutoFormat(lhsToken) || AllowAutoFormat(rhsToken)) && + IsNumber(lhs, rhs, out lhsNum, out rhsNum)) + { + _stack.Push(lhsNum == rhsNum ? ExpressionToken.True : ExpressionToken.False); + } + else if (IsDate(lhs, rhs, out DateTime lhsDate, out DateTime rhsDate)) + { + _stack.Push(lhsDate == rhsDate ? ExpressionToken.True : ExpressionToken.False); + } + else + { + _stack.Push(string.Compare(lhs, rhs, StringComparison.OrdinalIgnoreCase) == 0 ? ExpressionToken.True : ExpressionToken.False); + } + break; + case OperatorType.NotEqual: + rhsToken = _stack.Pop(); + lhsToken = _stack.Pop(); + rhs = rhsToken.ToString(); + lhs = lhsToken.ToString(); + + if (IsBoolean(lhs, rhs, out lhsBool, out rhsBool)) + { + _stack.Push(lhsBool != rhsBool ? ExpressionToken.True : ExpressionToken.False); + } + else if ((AllowAutoFormat(lhsToken) || AllowAutoFormat(rhsToken)) && + IsNumber(lhs, rhs, out lhsNum, out rhsNum)) + { + _stack.Push(lhsNum != rhsNum ? ExpressionToken.True : ExpressionToken.False); + } + else if (IsDate(lhs, rhs, out DateTime lhsDate, out DateTime rhsDate)) + { + _stack.Push(lhsDate != rhsDate ? ExpressionToken.True : ExpressionToken.False); + } + else + { + _stack.Push(string.Compare(lhs, rhs, StringComparison.OrdinalIgnoreCase) != 0 ? ExpressionToken.True : ExpressionToken.False); + } + break; + case OperatorType.BitwiseAnd: + BinaryNumberOperation((a, b) => (int)a & (int)b); + break; + case OperatorType.BitwiseOr: + BinaryNumberOperation((a, b) => (int)a | (int)b); + break; + case OperatorType.LogicalAnd: + BinaryLogicOperation((a, b) => IsTrue(a) && IsTrue(b)); + break; + case OperatorType.LogicalOr: + BinaryLogicOperation((a, b) => IsTrue(a) || IsTrue(b)); + break; + case OperatorType.UnaryMinus: + UnaryNumberOperation(a => -1 * a); + break; + case OperatorType.UnaryPlus: + // no action + break; + case OperatorType.LogicalNegation: + UnaryLogicOperation(a => !IsTrue(a)); + break; + case OperatorType.PreIncrement: + UnaryNumberOperation(a => a + 1); + break; + case OperatorType.PreDecrement: + UnaryNumberOperation(a => a - 1); + break; + // TODO: handle assignments & post increments + default: + throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "The {0} operation is not currently supported.", op)); + } + } + + static bool AllowAutoFormat(ExpressionToken token) + { + return token.TokenType != ExpressionTokenType.Field || token.FieldValue.TypeHint != ValueTypeHint.Text; + } + + static bool IsTrue(string value) + { + return string.Equals("true", value, StringComparison.OrdinalIgnoreCase) || value == "1"; + } + + static bool IsFalse(string value) + { + return string.Equals("false", value, StringComparison.OrdinalIgnoreCase) || value == "0" || string.IsNullOrWhiteSpace(value); + } + + void UnaryNumberOperation(Func operation) + { + var op1 = _stack.Pop().ToDecimal(_context); + var res = operation(op1); + + _stack.Push(new ExpressionToken(res.ToString(_context.FormatCulture))); + } + void UnaryLogicOperation(Func operation) + { + var op1 = _stack.Pop(); + var res = operation(op1.ToString()) ? "1" : "0"; + + _stack.Push(new ExpressionToken(res)); + } + void BinaryLogicOperation(Func operation) + { + var op2 = _stack.Pop(); + var op1 = _stack.Pop(); + + var res = operation(op1.ToString(), op2.ToString()) ? "1" : "0"; + + _stack.Push(new ExpressionToken(res)); + } + void BinaryNumberOperation(Func operation) + { + var op2 = _stack.Pop().ToDecimal(_context); + var op1 = _stack.Pop().ToDecimal(_context); + + var res = operation(op1, op2); + + _stack.Push(new ExpressionToken(res.ToString(_context.FormatCulture))); + } + + #endregion + } +} diff --git a/DunGenPlus/DunGenPlus/ExpressionParser/ExpressionToken.cs b/DunGenPlus/DunGenPlus/ExpressionParser/ExpressionToken.cs new file mode 100644 index 0000000..963e5a3 --- /dev/null +++ b/DunGenPlus/DunGenPlus/ExpressionParser/ExpressionToken.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Globalization; +using Soukoku.ExpressionParser.Parsing; + +namespace Soukoku.ExpressionParser +{ + /// + /// A token split from the initial text input. + /// + public class ExpressionToken + { + /// + /// Canonical true value. Actual value is numerical "1". + /// + public static readonly ExpressionToken True = new ExpressionToken("1"); + /// + /// Canonical false value. Actual value is numerical "0". + /// + public static readonly ExpressionToken False = new ExpressionToken("0"); + + internal static readonly NumberStyles NumberParseStyle = NumberStyles.Integer | NumberStyles.AllowDecimalPoint | NumberStyles.AllowCurrencySymbol | NumberStyles.Number; + + /// + /// Initializes a new instance of the class. + /// + public ExpressionToken() { } + + /// + /// Initializes a new frozen instance of the class + /// with the specified value. + /// + /// The value. + public ExpressionToken(string value) + { + _type = ExpressionTokenType.Value; + _value = value; + } + + + RawToken _rawToken; // the raw token that makes this token + + /// + /// Gets the raw token that made this list. + /// + /// + public RawToken RawToken { get { return _rawToken; } } + + const string FrozenErrorMsg = "Cannot modify frozen token."; + + /// + /// Appends the specified token to this expression. + /// + /// The token. + /// + public void Append(RawToken token) + { + if (IsFrozen) { throw new InvalidOperationException(FrozenErrorMsg); } + + if (_rawToken == null) { _rawToken = token; } + else { _rawToken.Append(token); } + } + + /// + /// Gets a value indicating whether this instance is frozen from append. + /// + /// + /// true if this instance is frozen; otherwise, false. + /// + public bool IsFrozen { get { return _value != null; } } + + /// + /// Freezes this instance from being appended. + /// + /// + public void Freeze() + { + if (IsFrozen) { throw new InvalidOperationException(FrozenErrorMsg); } + + _value = _rawToken?.ToString(); + } + + private ExpressionTokenType _type; + /// + /// Gets or sets the type of the token. + /// + /// + /// The type of the token. + /// + public ExpressionTokenType TokenType + { + get { return _type; } + set { if (_value == null) { _type = value; } } + } + + /// + /// Gets or sets the type of the operator. This is only used if the + /// is . + /// + /// + /// The type of the operator. + /// + public OperatorType OperatorType { get; set; } + + string _value; + + /// + /// Gets the raw token value. + /// + /// + /// The value. + /// + public string Value { get { return _value ?? _rawToken?.ToString(); } } + + /// + /// Gets the resolved field value and type hint if token is a field. + /// + public (object Value, ValueTypeHint TypeHint) FieldValue { get; internal set; } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + switch (TokenType) + { + case ExpressionTokenType.Field: + return FieldValue.Value?.ToString() ?? ""; + default: + return Value ?? ""; + } + } + + + #region conversion routines + + ///// + ///// Check if the value is considered numeric. + ///// + ///// + //public bool IsNumeric() + //{ + // if (TokenType == ExpressionTokenType.Field && FieldValue.TypeHint == ValueTypeHint.Text) return false; + + // return decimal.TryParse(Value, NumberParseStyle, CultureInfo.InvariantCulture, out decimal dummy); + //} + + ///// + ///// Check if the value is considered true. + ///// + ///// + //public bool IsTrue(string value) + //{ + // if (TokenType == ExpressionTokenType.Field && FieldValue.TypeHint == ValueTypeHint.Text) return false; + + // return string.Equals("true", Value, StringComparison.OrdinalIgnoreCase) || value == "1"; + //} + + ///// + ///// Check if the value is considered false. + ///// + ///// + //public bool IsFalse(string value) + //{ + // if (TokenType == ExpressionTokenType.Field && FieldValue.TypeHint == ValueTypeHint.Text) return false; + + // return string.Equals("false", Value, StringComparison.OrdinalIgnoreCase) || value == "0"; + //} + + /// + /// Converts to the double value. + /// + /// + /// + /// + public double ToDouble(EvaluationContext context) + { + switch (TokenType) + { + case ExpressionTokenType.Value: + case ExpressionTokenType.SingleQuoted: + case ExpressionTokenType.DoubleQuoted: + return double.Parse(Value, NumberParseStyle, context.FormatCulture); + case ExpressionTokenType.Field: + return double.Parse(FieldValue.Value?.ToString(), NumberParseStyle, context.FormatCulture); + default: + throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Cannot convert {0}({1}) to a numeric value.", TokenType, Value)); + } + } + + /// + /// Converts to the decimal value. + /// + /// + /// + /// + public decimal ToDecimal(EvaluationContext context) + { + switch (TokenType) + { + case ExpressionTokenType.Value: + case ExpressionTokenType.SingleQuoted: + case ExpressionTokenType.DoubleQuoted: + return decimal.Parse(Value, NumberParseStyle, context.FormatCulture); + case ExpressionTokenType.Field: + return decimal.Parse(FieldValue.Value?.ToString(), NumberParseStyle, context.FormatCulture); + default: + throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Cannot convert {0}({1}) to a numeric value.", TokenType, Value)); + } + } + + #endregion + } + + +} diff --git a/DunGenPlus/DunGenPlus/ExpressionParser/ExpressionTokenType.cs b/DunGenPlus/DunGenPlus/ExpressionParser/ExpressionTokenType.cs new file mode 100644 index 0000000..ff77a01 --- /dev/null +++ b/DunGenPlus/DunGenPlus/ExpressionParser/ExpressionTokenType.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Soukoku.ExpressionParser +{ + /// + /// Indicates the expression token type. + /// + public enum ExpressionTokenType + { + /// + /// Invalid token type. + /// + None, + /// + /// The token is an operator. + /// + Operator, + /// + /// The token is an open parenthesis. + /// + OpenParenthesis, + /// + /// The token is a close parenthesis. + /// + CloseParenthesis, + /// + /// The token is a function. + /// + Function, + /// + /// The token is a comma. + /// + Comma, + /// + /// The token is a field reference. + /// + Field, + /// + /// The token is from single quoted value. + /// + SingleQuoted, + /// + /// The token is from double quoted value. + /// + DoubleQuoted, + /// + /// The token is a yet-to-be-parsed value. + /// + Value, + } +} diff --git a/DunGenPlus/DunGenPlus/ExpressionParser/FunctionRoutine.cs b/DunGenPlus/DunGenPlus/ExpressionParser/FunctionRoutine.cs new file mode 100644 index 0000000..3da2116 --- /dev/null +++ b/DunGenPlus/DunGenPlus/ExpressionParser/FunctionRoutine.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Soukoku.ExpressionParser +{ + /// + /// Defines a basic function routine. + /// + public class FunctionRoutine + { + Func _routine; + + /// + /// Initializes a new instance of the class. + /// + /// The argument count. + /// The routine. + /// routine + public FunctionRoutine(int argCount, Func routine) + { + if (routine == null) { throw new ArgumentNullException("routine"); } + ArgumentCount = argCount; + _routine = routine; + } + + /// + /// Gets the expected argument count. + /// + /// + /// The argument count. + /// + public int ArgumentCount { get; private set; } + + /// + /// Evaluates using the function routine. + /// + /// The context. + /// The arguments. + /// + public ExpressionToken Evaluate(EvaluationContext context, ExpressionToken[] args) { return _routine(context, args); } + } +} diff --git a/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/IExpressionTokenizer.cs b/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/IExpressionTokenizer.cs new file mode 100644 index 0000000..a2a967c --- /dev/null +++ b/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/IExpressionTokenizer.cs @@ -0,0 +1,16 @@ +namespace Soukoku.ExpressionParser.Parsing +{ + /// + /// Interface for something that can tokenize an expression. + /// + public interface IExpressionTokenizer + { + /// + /// Splits the specified input expression into a list of values. + /// + /// The input. + /// + /// + ExpressionToken[] Tokenize(string input); + } +} \ No newline at end of file diff --git a/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/InfixToPostfixTokenizer.cs b/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/InfixToPostfixTokenizer.cs new file mode 100644 index 0000000..d85cd5e --- /dev/null +++ b/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/InfixToPostfixTokenizer.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Soukoku.ExpressionParser.Parsing +{ + /// + /// A tokenizer that parses an input expression string in infix notation into tokens without white spaces + /// in the orders of postfix expressions. + /// + public class InfixToPostfixTokenizer : IExpressionTokenizer + { + const string UnbalancedParenMsg = "Unbalanced parenthesis in expression."; + + List _output; + Stack _stack; + + /// + /// Splits the specified input into a list of values + /// in postfix order. + /// + /// The input. + /// + /// + public ExpressionToken[] Tokenize(string input) + { + var infixTokens = new InfixTokenizer().Tokenize(input); + _output = new List(); + _stack = new Stack(); + + // this is the shunting-yard algorithm + // https://en.wikipedia.org/wiki/Shunting-yard_algorithm + + foreach (var inToken in infixTokens) + { + switch (inToken.TokenType) + { + case ExpressionTokenType.Value: + case ExpressionTokenType.DoubleQuoted: + case ExpressionTokenType.SingleQuoted: + case ExpressionTokenType.Field: + _output.Add(inToken); + break; + case ExpressionTokenType.Function: + _stack.Push(inToken); + break; + case ExpressionTokenType.Comma: + HandleComma(); + break; + case ExpressionTokenType.Operator: + HandleOperatorToken(inToken); + break; + case ExpressionTokenType.OpenParenthesis: + _stack.Push(inToken); + break; + case ExpressionTokenType.CloseParenthesis: + HandleCloseParenthesis(); + break; + } + } + + while (_stack.Count > 0) + { + var op = _stack.Pop(); + if (op.TokenType == ExpressionTokenType.OpenParenthesis) + { + throw new NotSupportedException(UnbalancedParenMsg); + } + _output.Add(op); + } + + return _output.ToArray(); + } + + private void HandleComma() + { + bool closed = false; + while (_stack.Count > 1) + { + var peek = _stack.Peek(); + if (peek.TokenType == ExpressionTokenType.OpenParenthesis) + { + closed = true; + break; + } + _output.Add(_stack.Pop()); + } + + if (!closed) + { + throw new NotSupportedException(UnbalancedParenMsg); + } + } + + private void HandleOperatorToken(ExpressionToken inToken) + { + while (_stack.Count > 0) + { + var op2 = _stack.Peek(); + if (op2.TokenType == ExpressionTokenType.Operator) + { + var op1Prec = KnownOperators.GetPrecedence(inToken.OperatorType); + var op2Prec = KnownOperators.GetPrecedence(op2.OperatorType); + var op1IsLeft = KnownOperators.IsLeftAssociative(inToken.OperatorType); + + if ((op1IsLeft && op1Prec <= op2Prec) || + (!op1IsLeft && op1Prec < op2Prec)) + { + _output.Add(_stack.Pop()); + continue; + } + } + break; + } + _stack.Push(inToken); + } + + private void HandleCloseParenthesis() + { + bool closed = false; + while (_stack.Count > 0) + { + var pop = _stack.Pop(); + if (pop.TokenType == ExpressionTokenType.OpenParenthesis) + { + closed = true; + break; + } + _output.Add(pop); + } + + if (!closed) + { + throw new NotSupportedException(UnbalancedParenMsg); + } + else if (_stack.Count > 0) + { + var next = _stack.Peek(); + if (next != null && next.TokenType == ExpressionTokenType.Function) + { + _output.Add(_stack.Pop()); + } + } + } + } +} diff --git a/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/InfixTokenizer.cs b/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/InfixTokenizer.cs new file mode 100644 index 0000000..fb01ee1 --- /dev/null +++ b/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/InfixTokenizer.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Soukoku.ExpressionParser.Parsing +{ + /// + /// A tokenizer that parses an input expression string in infix notation into tokens without white spaces. + /// + public class InfixTokenizer : IExpressionTokenizer + { + List _currentTokens; + + /// + /// Splits the specified input into a list of values. + /// + /// The input. + /// + /// + public ExpressionToken[] Tokenize(string input) + { + _currentTokens = new List(); + ExpressionToken lastExpToken = null; + + var reader = new ListReader(new RawTokenizer().Tokenize(input)); + + while (!reader.IsEnd) + { + var curRawToken = reader.Read(); + switch (curRawToken.TokenType) + { + case RawTokenType.WhiteSpace: + // generially ends previous token outside other special scopes + lastExpToken = null; + break; + case RawTokenType.Literal: + if (lastExpToken == null || lastExpToken.TokenType != ExpressionTokenType.Value) + { + lastExpToken = new ExpressionToken { TokenType = ExpressionTokenType.Value }; + _currentTokens.Add(lastExpToken); + } + lastExpToken.Append(curRawToken); + break; + case RawTokenType.Symbol: + // first do operator match by checking the prev op + // and see if combined with current token would still match a known operator + if (KnownOperators.IsKnown(curRawToken.Value)) + { + if (lastExpToken != null && lastExpToken.TokenType == ExpressionTokenType.Operator) + { + var testOpValue = lastExpToken.Value + curRawToken.Value; + if (KnownOperators.IsKnown(testOpValue)) + { + // just append it + lastExpToken.Append(curRawToken); + continue; + } + } + // start new one + lastExpToken = new ExpressionToken { TokenType = ExpressionTokenType.Operator }; + _currentTokens.Add(lastExpToken); + lastExpToken.Append(curRawToken); + } + else + { + lastExpToken = HandleNonOperatorSymbolToken(reader, lastExpToken, curRawToken); + } + break; + default: + // should never happen + throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Unsupported token type {0} at position {1}.", curRawToken.TokenType, curRawToken.Position)); + } + } + + MassageTokens(_currentTokens); + + return _currentTokens.ToArray(); + } + + ExpressionToken HandleNonOperatorSymbolToken(ListReader reader, ExpressionToken lastExpToken, RawToken curRawToken) + { + switch (curRawToken.Value) + { + case ",": + lastExpToken = new ExpressionToken { TokenType = ExpressionTokenType.Comma }; + _currentTokens.Add(lastExpToken); + lastExpToken.Append(curRawToken); + break; + case "(": + // if last one is string make it a function + if (lastExpToken != null && lastExpToken.TokenType == ExpressionTokenType.Value) + { + lastExpToken.TokenType = ExpressionTokenType.Function; + } + + lastExpToken = new ExpressionToken { TokenType = ExpressionTokenType.OpenParenthesis }; + _currentTokens.Add(lastExpToken); + lastExpToken.Append(curRawToken); + break; + case ")": + lastExpToken = new ExpressionToken { TokenType = ExpressionTokenType.CloseParenthesis }; + _currentTokens.Add(lastExpToken); + lastExpToken.Append(curRawToken); + break; + case "{": + // read until end of } + lastExpToken = ReadToLiteralAs(reader, "}", ExpressionTokenType.Field); + break; + case "\"": + // read until end of " + lastExpToken = ReadToLiteralAs(reader, "\"", ExpressionTokenType.DoubleQuoted); + break; + case "'": + // read until end of ' + lastExpToken = ReadToLiteralAs(reader, "'", ExpressionTokenType.SingleQuoted); + break; + } + + return lastExpToken; + } + + ExpressionToken ReadToLiteralAs(ListReader reader, string literalValue, ExpressionTokenType tokenType) + { + ExpressionToken lastExpToken = new ExpressionToken { TokenType = tokenType }; + _currentTokens.Add(lastExpToken); + while (!reader.IsEnd) + { + var next = reader.Read(); + if (next.TokenType == RawTokenType.Symbol && next.Value == literalValue) + { + break; + } + lastExpToken.Append(next); + } + + return lastExpToken; + } + + static void MassageTokens(List tokens) + { + // do final token parsing based on contexts and cleanup + + var reader = new ListReader(tokens); + while (!reader.IsEnd) + { + var tk = reader.Read(); + + if (tk.TokenType == ExpressionTokenType.Operator) + { + // special detection for operators depending on where it is :( + DetermineOperatorType(reader, tk); + } + + tk.Freeze(); + } + } + + private static void DetermineOperatorType(ListReader reader, ExpressionToken tk) + { + tk.OperatorType = KnownOperators.TryMap(tk.Value); + switch (tk.OperatorType) + { + case OperatorType.PreDecrement: + case OperatorType.PreIncrement: + // detect if it's really post ++ -- versions + var prev = reader.Position > 1 ? reader.Peek(-2) : null; + if (prev != null && prev.TokenType == ExpressionTokenType.Value) + { + if (tk.OperatorType == OperatorType.PreIncrement) + { + tk.OperatorType = OperatorType.PostIncrement; + } + else + { + tk.OperatorType = OperatorType.PostDecrement; + } + } + break; + case OperatorType.Addition: + case OperatorType.Subtraction: + // detect if unary + - + prev = reader.Position > 1 ? reader.Peek(-2) : null; + if (prev == null || + (prev.TokenType == ExpressionTokenType.Operator && + prev.OperatorType != OperatorType.PostDecrement && + prev.OperatorType != OperatorType.PostIncrement)) + { + if (tk.OperatorType == OperatorType.Addition) + { + tk.OperatorType = OperatorType.UnaryPlus; + } + else + { + tk.OperatorType = OperatorType.UnaryMinus; + } + } + break; + case OperatorType.None: + throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Operator {0} is not supported.", tk.Value)); + } + } + } + +} diff --git a/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/KnownOperators.cs b/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/KnownOperators.cs new file mode 100644 index 0000000..44bd770 --- /dev/null +++ b/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/KnownOperators.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Soukoku.ExpressionParser.Parsing +{ + /// + /// Contains recognized operator info. + /// + public static class KnownOperators + { + static readonly Dictionary DefaultMap = new Dictionary + { + // double char + {"++", OperatorType.PreIncrement }, + {"--", OperatorType.PreDecrement }, + {"+=", OperatorType.AdditionAssignment }, + {"-=", OperatorType.SubtractionAssignment }, + {"*=", OperatorType.MultiplicationAssignment }, + {"/=", OperatorType.DivisionAssignment }, + {"%=", OperatorType.ModulusAssignment }, + {"==", OperatorType.Equal }, + {"!=", OperatorType.NotEqual}, + {"<=", OperatorType.LessThanOrEqual }, + {">=", OperatorType.GreaterThanOrEqual }, + {"&&", OperatorType.LogicalAnd }, + {"||", OperatorType.LogicalOr }, + + // single char + {"+", OperatorType.Addition }, + {"-", OperatorType.Subtraction }, + {"*", OperatorType.Multiplication }, + {"/", OperatorType.Division }, + {"=", OperatorType.Assignment }, + {"%", OperatorType.Modulus }, + //"^", + {"<", OperatorType.LessThan }, + {">", OperatorType.GreaterThan }, + //"~", + {"&", OperatorType.BitwiseAnd }, + {"|", OperatorType.BitwiseOr }, + {"!", OperatorType.LogicalNegation }, + }; + + /// + /// Determines whether the specified operator value is recognized. + /// + /// The operator value. + /// + public static bool IsKnown(string operatorValue) + { + return DefaultMap.ContainsKey(operatorValue); + } + + /// + /// Try to get the enum version of the operator string value. + /// + /// The operator value. + /// + public static OperatorType TryMap(string operatorValue) + { + if (DefaultMap.ContainsKey(operatorValue)) + { + return DefaultMap[operatorValue]; + } + return OperatorType.None; + } + + /// + /// Gets the precedence of an operator. + /// + /// The type. + /// + public static int GetPrecedence(OperatorType type) + { + switch (type) + { + case OperatorType.PostDecrement: + case OperatorType.PostIncrement: + return 100; + case OperatorType.PreDecrement: + case OperatorType.PreIncrement: + case OperatorType.UnaryMinus: + case OperatorType.UnaryPlus: + case OperatorType.LogicalNegation: + return 90; + case OperatorType.Multiplication: + case OperatorType.Division: + case OperatorType.Modulus: + return 85; + case OperatorType.Addition: + case OperatorType.Subtraction: + return 80; + case OperatorType.LessThan: + case OperatorType.LessThanOrEqual: + case OperatorType.GreaterThan: + case OperatorType.GreaterThanOrEqual: + return 75; + case OperatorType.Equal: + case OperatorType.NotEqual: + return 70; + case OperatorType.BitwiseAnd: + case OperatorType.BitwiseOr: + return 65; + case OperatorType.LogicalAnd: + case OperatorType.LogicalOr: + return 60; + case OperatorType.Assignment: + case OperatorType.AdditionAssignment: + case OperatorType.DivisionAssignment: + case OperatorType.ModulusAssignment: + case OperatorType.MultiplicationAssignment: + case OperatorType.SubtractionAssignment: + return 20; + } + return 0; + } + + /// + /// Determines whether the operator is left-to-right associative (true) or right-to-left (false). + /// + /// The type. + /// + public static bool IsLeftAssociative(OperatorType type) + { + switch (type) + { + case OperatorType.PreDecrement: + case OperatorType.PreIncrement: + case OperatorType.UnaryMinus: + case OperatorType.UnaryPlus: + case OperatorType.LogicalNegation: + case OperatorType.Assignment: + case OperatorType.AdditionAssignment: + case OperatorType.DivisionAssignment: + case OperatorType.ModulusAssignment: + case OperatorType.MultiplicationAssignment: + case OperatorType.SubtractionAssignment: + return false; + } + return true; + } + } + +} diff --git a/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/ListReader.cs b/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/ListReader.cs new file mode 100644 index 0000000..b3c0834 --- /dev/null +++ b/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/ListReader.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Soukoku.ExpressionParser.Parsing +{ + /// + /// A simple reader for an IList. + /// + /// The type of the item in the list. + public class ListReader + { + IList _list; + + /// + /// Initializes a new instance of the class. + /// + /// The list to read. + /// list + public ListReader(IList list) + { + if (list == null) { throw new ArgumentNullException("list"); } + + _list = list; + } + + private int _position; + /// + /// Gets or sets the position of the reader. This is the 0-based index. + /// + /// + /// The position. + /// + /// + public int Position + { + get { return _position; } + set + { + if (value < 0 || value > _list.Count) + { + throw new ArgumentOutOfRangeException("value"); + } + _position = value; + } + } + + /// + /// Gets a value indicating whether the reader has reached the end of list. + /// + /// + /// true if this instance is eol; otherwise, false. + /// + public bool IsEnd { get { return _position >= _list.Count; } } + + /// + /// Reads the current item in the list and moves the forward. + /// + /// + /// + public TItem Read() + { + return _list[Position++]; + } + + /// + /// Peeks the current item in the list without moving the . + /// + /// + /// + public TItem Peek() + { + return Peek(0); + } + + /// + /// Peeks the item in the list without moving the . + /// + /// The offset from current position. + /// + /// + public TItem Peek(int offset) + { + // let list throw the exception. + return _list[Position + offset]; + } + } +} diff --git a/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/OperatorType.cs b/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/OperatorType.cs new file mode 100644 index 0000000..d6c5075 --- /dev/null +++ b/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/OperatorType.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Soukoku.ExpressionParser.Parsing +{ + /// + /// Indicates the recognized operator types. + /// + public enum OperatorType + { + /// + /// Unspecified default value. + /// + None, + /// + /// ++ after a value. + /// + PostIncrement, + /// + /// -- after a value. + /// + PostDecrement, + /// + /// ++ before a value. + /// + PreIncrement, + /// + /// -- before a value. + /// + PreDecrement, + /// + /// + before a value. + /// + UnaryPlus, + /// + /// - before a value. + /// + UnaryMinus, + /// + /// ! before a value. + /// + LogicalNegation, + /// + /// * between values. + /// + Multiplication, + /// + /// / between values. + /// + Division, + /// + /// % between values. + /// + Modulus, + /// + /// + between values. + /// + Addition, + /// + /// - between values. + /// + Subtraction, + /// + /// < between values. + /// + LessThan, + /// + /// <= between values. + /// + LessThanOrEqual, + /// + /// > between values. + /// + GreaterThan, + /// + /// >= between values. + /// + GreaterThanOrEqual, + /// + /// == between values. + /// + Equal, + /// + /// != between values. + /// + NotEqual, + /// + /// & between values. + /// + BitwiseAnd, + /// + /// | between values. + /// + BitwiseOr, + /// + /// && between values. + /// + LogicalAnd, + /// + /// || between values. + /// + LogicalOr, + /// + /// = between values. + /// + Assignment, + /// + /// += between values. + /// + AdditionAssignment, + /// + /// -= between values. + /// + SubtractionAssignment, + /// + /// *= between values. + /// + MultiplicationAssignment, + /// + /// /= between values. + /// + DivisionAssignment, + /// + /// %= between values. + /// + ModulusAssignment, + + } +} diff --git a/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/RawToken.cs b/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/RawToken.cs new file mode 100644 index 0000000..60277d2 --- /dev/null +++ b/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/RawToken.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Soukoku.ExpressionParser.Parsing +{ + + /// + /// A low-level token split from the initial text input. + /// + public class RawToken + { + /// + /// Initializes a new instance of the class. + /// + /// The type. + /// The position. + internal RawToken(RawTokenType type, int position) + { + TokenType = type; + Position = position; + ValueBuilder = new StringBuilder(); + } + + /// + /// Gets the token type. + /// + /// + /// The type. + /// + public RawTokenType TokenType { get; private set; } + + /// + /// Gets the starting position of this token in the original input. + /// + /// + /// The position. + /// + public int Position { get; private set; } + + // TODO: test pef on using builder or using string directly + internal StringBuilder ValueBuilder { get; private set; } + + internal void Append(RawToken token) + { + if (token != null) + { + ValueBuilder.Append(token.ValueBuilder); + } + } + + /// + /// Gets the token value. + /// + /// + /// The value. + /// + public string Value { get { return ValueBuilder.ToString(); } } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + return Value; + } + } + + /// + /// Indicates the low-level token type. + /// + public enum RawTokenType + { + /// + /// Invalid token type. + /// + None, + /// + /// Token is white space. + /// + WhiteSpace, + /// + /// Token is a symbol. + /// + Symbol, + /// + /// Token is not symbol or white space. + /// + Literal, + } +} diff --git a/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/RawTokenizer.cs b/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/RawTokenizer.cs new file mode 100644 index 0000000..b31fdd2 --- /dev/null +++ b/DunGenPlus/DunGenPlus/ExpressionParser/Parsing/RawTokenizer.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace Soukoku.ExpressionParser.Parsing +{ + /// + /// A low-level tokenizer that parses an input expression string into tokens. + /// + public class RawTokenizer + { + static readonly char[] DefaultSymbols = new[] + { + '+', '-', '*', '/', '=', '%', '^', + ',', '<', '>', '&', '|', '!', + '(', ')', '{', '}', '[', ']', + '"', '\'', '~' + }; + + + /// + /// Initializes a new instance of the class. + /// + public RawTokenizer() : this(null) { } + + /// + /// Initializes a new instance of the class. + /// + /// The char values to count as symbols. If null the will be used. + public RawTokenizer(params char[] symbols) + { + _symbols = symbols ?? DefaultSymbols; + } + + char[] _symbols; + + /// + /// Gets the char values that count as symbols for this tokenizer. + /// + /// + /// The symbols. + /// + public char[] GetSymbols() { return (char[])_symbols.Clone(); } + + /// + /// Splits the specified input into a list of values using white space and symbols. + /// The tokens can be recombined to rebuild the original input exactly. + /// + /// The input. + /// + public RawToken[] Tokenize(string input) + { + var tokens = new List(); + + if (input != null) + { + RawToken lastToken = null; + for (int i = 0; i < input.Length; i++) + { + var ch = input[i]; + if (char.IsWhiteSpace(ch)) + { + lastToken = NewTokenIfNecessary(tokens, lastToken, RawTokenType.WhiteSpace, i); + } + else if (_symbols.Contains(ch)) + { + lastToken = NewTokenIfNecessary(tokens, lastToken, RawTokenType.Symbol, i); + } + else + { + lastToken = NewTokenIfNecessary(tokens, lastToken, RawTokenType.Literal, i); + } + + if (ch == '\\' && ++i < input.Length) + { + // assume escape and just append next char as-is + var next = input[i]; + lastToken.ValueBuilder.Append(next); + } + else + { + lastToken.ValueBuilder.Append(ch); + } + } + } + return tokens.ToArray(); + } + + static RawToken NewTokenIfNecessary(List tokens, RawToken lastToken, RawTokenType curTokenType, int position) + { + if (lastToken == null || lastToken.TokenType != curTokenType || + curTokenType == RawTokenType.Symbol) // for symbol always let it be by itself + { + lastToken = new RawToken(curTokenType, position); + tokens.Add(lastToken); + } + return lastToken; + } + } +} diff --git a/DunGenPlus/DunGenPlus/ExpressionParser/ValueTypeHint.cs b/DunGenPlus/DunGenPlus/ExpressionParser/ValueTypeHint.cs new file mode 100644 index 0000000..9b95d38 --- /dev/null +++ b/DunGenPlus/DunGenPlus/ExpressionParser/ValueTypeHint.cs @@ -0,0 +1,17 @@ +namespace Soukoku.ExpressionParser +{ + /// + /// Used to indicate how to handle resolve field value. + /// + public enum ValueTypeHint + { + /// + /// Value is converted to suitable type for comparison purposes. + /// + Auto, + /// + /// Value is forced to be text for comparison purposes. + /// + Text, + } +} diff --git a/DunGenPlus/DunGenPlus/Generation/DunGenPlusGenerator.cs b/DunGenPlus/DunGenPlus/Generation/DunGenPlusGenerator.cs index 713cea5..fae2e1e 100644 --- a/DunGenPlus/DunGenPlus/Generation/DunGenPlusGenerator.cs +++ b/DunGenPlus/DunGenPlus/Generation/DunGenPlusGenerator.cs @@ -17,6 +17,7 @@ using UnityEngine.Rendering.HighDefinition; using BepInEx.Logging; using DunGenPlus.DevTools; using DunGenPlus.Patches; +using DunGenPlus.DevTools.Panels; [assembly: SecurityPermission( SecurityAction.RequestMinimum, SkipVerification = true )] namespace DunGenPlus.Generation { @@ -36,7 +37,7 @@ namespace DunGenPlus.Generation { ActiveAlternative = true; var props = extender.Properties.Copy(extender.Version); - var callback = new EventCallbackScenario(DevDebugManager.Instance); + var callback = new EventCallbackScenario(DunGenPlusPanel.Instance && DunGenPlusPanel.Instance.eventCallbackValue); Instance.Events.OnModifyDunGenExtenderProperties.Invoke(props, callback); props.NormalNodeArchetypesProperties.SetupProperties(generator); Properties = props; diff --git a/DunGenPlus/DunGenPlus/Managers/DoorwayManager.cs b/DunGenPlus/DunGenPlus/Managers/DoorwayManager.cs index dd1c3e9..36ccd99 100644 --- a/DunGenPlus/DunGenPlus/Managers/DoorwayManager.cs +++ b/DunGenPlus/DunGenPlus/Managers/DoorwayManager.cs @@ -1,36 +1,92 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; +using DunGen; using DunGen.Adapters; using DunGenPlus.Components; +using DunGenPlus.Components.Scripting; using DunGenPlus.Generation; using DunGenPlus.Utils; +using static DunGenPlus.Managers.DoorwayManager; namespace DunGenPlus.Managers { public static class DoorwayManager { public static ActionList onMainEntranceTeleportSpawnedEvent = new ActionList("onMainEntranceTeleportSpawned"); - public static List doorwayCleanupList; + //public static List doorwayCleanupList; + + public class Scripts { + public List scriptList; + public List actionList; + + public Scripts(){ + scriptList = new List(); + actionList = new List(); + } + + public void Add(IDunGenScriptingParent script) { + scriptList.Add(script); + } + + public void Add(Action action) { + actionList.Add(action); + } + + public bool Call(){ + foreach(var s in scriptList){ + s.Call(); + } + + foreach(var a in actionList){ + a.Invoke(); + } + + return scriptList.Count + actionList.Count > 0; + } + + } + + public static Dictionary scriptingLists; public static void ResetList(){ - doorwayCleanupList = new List(); + //doorwayCleanupList = new List(); + scriptingLists = new Dictionary(); + foreach(DunGenScriptingHook e in Enum.GetValues(typeof(DunGenScriptingHook))){ + scriptingLists.Add(e, new Scripts()); + } } public static void AddDoorwayCleanup(DoorwayCleanup cleanup){ - doorwayCleanupList.Add(cleanup); + //doorwayCleanupList.Add(cleanup); } - public static void onMainEntranceTeleportSpawnedFunction(){ + public static void AddDunGenScriptHook(IDunGenScriptingParent script){ + scriptingLists[script.GetScriptingHook].Add(script); + } + + public static void AddActionHook(DunGenScriptingHook hook, Action action){ + scriptingLists[hook].Add(action); + } + + public static void OnMainEntranceTeleportSpawnedFunction(){ if (DunGenPlusGenerator.Active) { - foreach(var d in doorwayCleanupList){ - d.SetBlockers(false); - d.Cleanup(); + + //foreach(var d in doorwayCleanupList){ + // d.SetBlockers(false); + // d.Cleanup(); + // Plugin.logger.LogWarning(d.GetComponentInParent().gameObject.name); + //} + + var anyFunctionCalled = false; + foreach(var d in scriptingLists.Values){ + anyFunctionCalled = anyFunctionCalled | d.Call(); } // we can leave early if doorway cleanup is not used (most likely for most dungeons anyway) - if (doorwayCleanupList.Count == 0) return; + if (!anyFunctionCalled) return; try{ var dungeonGen = RoundManager.Instance.dungeonGenerator; @@ -45,5 +101,11 @@ namespace DunGenPlus.Managers { } } + public static void SetLevelObjectVariablesFunction(){ + if (DunGenPlusGenerator.Active) { + scriptingLists[DunGenScriptingHook.SetLevelObjectVariables ].Call(); + } + } + } } diff --git a/DunGenPlus/DunGenPlus/Patches/RoundManagerPatch.cs b/DunGenPlus/DunGenPlus/Patches/RoundManagerPatch.cs index ea1cd17..651cb56 100644 --- a/DunGenPlus/DunGenPlus/Patches/RoundManagerPatch.cs +++ b/DunGenPlus/DunGenPlus/Patches/RoundManagerPatch.cs @@ -69,5 +69,12 @@ namespace DunGenPlus.Patches { } } + [HarmonyPrefix] + [HarmonyPriority(Priority.First)] + [HarmonyPatch(typeof(RoundManager), "SetLevelObjectVariables")] + public static void SetLevelObjectVariablesPatch (ref RoundManager __instance) { + DoorwayManager.SetLevelObjectVariablesFunction(); + } + } } diff --git a/DunGenPlus/DunGenPlus/Plugin.cs b/DunGenPlus/DunGenPlus/Plugin.cs index 62f81b0..cd968da 100644 --- a/DunGenPlus/DunGenPlus/Plugin.cs +++ b/DunGenPlus/DunGenPlus/Plugin.cs @@ -3,6 +3,7 @@ using BepInEx.Logging; using DunGen; using DunGen.Graph; using DunGenPlus.Collections; +using DunGenPlus.Components.Scripting; using DunGenPlus.Generation; using DunGenPlus.Managers; using DunGenPlus.Patches; @@ -26,7 +27,7 @@ namespace DunGenPlus { internal const string modGUID = "dev.ladyalice.dungenplus"; private const string modName = "Dungeon Generation Plus"; - private const string modVersion = "1.3.4"; + private const string modVersion = "1.4.0"; internal readonly Harmony Harmony = new Harmony(modGUID); @@ -62,7 +63,7 @@ namespace DunGenPlus { Assets.LoadAssets(); Assets.LoadAssetBundle(); - DoorwayManager.onMainEntranceTeleportSpawnedEvent.AddEvent("DoorwayCleanup", DoorwayManager.onMainEntranceTeleportSpawnedFunction); + DoorwayManager.onMainEntranceTeleportSpawnedEvent.AddEvent("DoorwayCleanup", DoorwayManager.OnMainEntranceTeleportSpawnedFunction); } } diff --git a/DunGenPlus/DunGenPlus/Utils/Utility.cs b/DunGenPlus/DunGenPlus/Utils/Utility.cs index 7cca9db..0fef72d 100644 --- a/DunGenPlus/DunGenPlus/Utils/Utility.cs +++ b/DunGenPlus/DunGenPlus/Utils/Utility.cs @@ -1,8 +1,11 @@ -using System; +using BepInEx.Logging; +using DunGenPlus.Components.Scripting; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using UnityEngine; using UnityEngine.Events; namespace DunGenPlus.Utils { @@ -52,4 +55,26 @@ namespace DunGenPlus.Utils { } } + public static class Utility { + + public static void PrintLog(string message, LogLevel logLevel){ + if (DunGenPlusScript.InDebugMode){ + switch(logLevel){ + case LogLevel.Error: + case LogLevel.Fatal: + Debug.LogError(message); + break; + case LogLevel.Warning: + Debug.LogWarning(message); + break; + default: + Debug.Log(message); + break; + } + } else { + Plugin.logger.Log(logLevel, message); + } + } + } + } diff --git a/DunGenPlus/DunGenPlusEditor/DunGenPlusEditor.csproj b/DunGenPlus/DunGenPlusEditor/DunGenPlusEditor.csproj index ef5f06b..b1bd4f9 100644 --- a/DunGenPlus/DunGenPlusEditor/DunGenPlusEditor.csproj +++ b/DunGenPlus/DunGenPlusEditor/DunGenPlusEditor.csproj @@ -73,10 +73,12 @@ + + diff --git a/DunGenPlus/DunGenPlusEditor/NamedGameObjectReferencePropertyDrawer.cs b/DunGenPlus/DunGenPlusEditor/NamedGameObjectReferencePropertyDrawer.cs new file mode 100644 index 0000000..26dbb31 --- /dev/null +++ b/DunGenPlus/DunGenPlusEditor/NamedGameObjectReferencePropertyDrawer.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine.UIElements; +using DunGenPlus; +using DunGenPlus.Collections; +using DunGenPlus.Components.Scripting; +using UnityEditor.UIElements; + +namespace DunGenPlusEditor { + + [CustomPropertyDrawer(typeof(NamedGameObjectReference))] + public class NamedGameObjectReferencePropertyDrawer : PropertyDrawer { + public override VisualElement CreatePropertyGUI(SerializedProperty property) { + + var container = new VisualElement(); + container.Add(new PropertyField(property.FindPropertyRelative("name"))); + container.Add(new PropertyField(property.FindPropertyRelative("gameObjects"))); + container.Add(new PropertyField(property.FindPropertyRelative("overrideState"))); + + return container; + } + } +} diff --git a/DunGenPlus/DunGenPlusEditor/ScriptActionPropertyDrawer.cs b/DunGenPlus/DunGenPlusEditor/ScriptActionPropertyDrawer.cs new file mode 100644 index 0000000..32fb9b2 --- /dev/null +++ b/DunGenPlus/DunGenPlusEditor/ScriptActionPropertyDrawer.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEditor; +using UnityEngine.UIElements; +using DunGenPlus; +using DunGenPlus.Collections; +using DunGenPlus.Components.Scripting; +using UnityEditor.UIElements; + +namespace DunGenPlusEditor { + + [CustomPropertyDrawer(typeof(ScriptAction))] + public class ScriptActionPropertyDrawer : PropertyDrawer { + public override VisualElement CreatePropertyGUI(SerializedProperty property) { + + var container = new VisualElement(); + var typeProperty = property.FindPropertyRelative("type"); + container.Add(new PropertyField(typeProperty)); + + switch((ScriptActionType)typeProperty.intValue){ + case ScriptActionType.SetNamedReferenceState: + AddPropertyFields(container, property, ("namedReference", "Named Reference"), ("boolValue", "State")); + break; + default: + break; + } + + + container.Add(new PropertyField(property.FindPropertyRelative("overrideState"))); + + return container; + } + + private void AddPropertyFields(VisualElement container, SerializedProperty property, params (string field, string label)[] pairs){ + foreach(var pair in pairs){ + container.Add(new PropertyField(property.FindPropertyRelative(pair.field), pair.label)); + } + } + } +}