DoorwayCleanup is now obselete

Created new scripting system
Added toggle for EventCallbackScenario for DevDebug window
This commit is contained in:
LadyAliceMargatroid 2025-02-07 08:29:24 -08:00
parent 4af194e0f4
commit 25530ebbb3
36 changed files with 2467 additions and 24 deletions

View File

@ -11,6 +11,7 @@ using UnityEngine;
namespace DunGenPlus.Components { namespace DunGenPlus.Components {
public class DoorwayCleanup : MonoBehaviour, IDungeonCompleteReceiver { public class DoorwayCleanup : MonoBehaviour, IDungeonCompleteReceiver {
[Header("OBSOLUTE. Please use DoorwayScriptingParent")]
[Header("Doorway References")] [Header("Doorway References")]
[Tooltip("The doorway reference.")] [Tooltip("The doorway reference.")]
public Doorway doorway; public Doorway doorway;

View File

@ -6,6 +6,8 @@ using System.Threading.Tasks;
using UnityEngine; using UnityEngine;
namespace DunGenPlus.Components.DoorwayCleanupScripting { namespace DunGenPlus.Components.DoorwayCleanupScripting {
[Obsolete("Please use DoorwayScriptingParent")]
public class DCSConnectorBlockerSpawnedPrefab : DoorwayCleanupScript { public class DCSConnectorBlockerSpawnedPrefab : DoorwayCleanupScript {
public enum Action { SwitchToConnector, SwitchToBlocker }; public enum Action { SwitchToConnector, SwitchToBlocker };

View File

@ -6,17 +6,20 @@ using System.Threading.Tasks;
using UnityEngine; using UnityEngine;
namespace DunGenPlus.Components.DoorwayCleanupScripting { namespace DunGenPlus.Components.DoorwayCleanupScripting {
[Obsolete("Please use DoorwayScriptingParent")]
public class DCSRemoveDoorwayConnectedDoorway : DoorwayCleanupScriptDoorwayCompare { public class DCSRemoveDoorwayConnectedDoorway : DoorwayCleanupScriptDoorwayCompare {
[Header("Removes Doorway Gameobject\nif the neighboring doorway's priority matches the operation comparison")] [Header("Removes Doorway Gameobject\nif the neighboring doorway's priority matches the operation comparison")]
[Header("Operation Comparison")] [Header("Operation Comparison")]
public int doorwayPriority; public int doorwayPriority;
public int doorwayPriorityB;
public Operation operation = Operation.Equal; public Operation operation = Operation.Equal;
public override void Cleanup(DoorwayCleanup parent) { public override void Cleanup(DoorwayCleanup parent) {
var doorway = parent.doorway; var doorway = parent.doorway;
if (doorway.connectedDoorway == null) return; 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) { if (result) {
parent.SwitchDoorwayGameObject(false); parent.SwitchDoorwayGameObject(false);

View File

@ -6,6 +6,8 @@ using System.Threading.Tasks;
using UnityEngine; using UnityEngine;
namespace DunGenPlus.Components.DoorwayCleanupScripting { namespace DunGenPlus.Components.DoorwayCleanupScripting {
[Obsolete("Please use DoorwayScriptingParent")]
public class DCSRemoveDoorwaySpawnedPrefab : DoorwayCleanupScript { public class DCSRemoveDoorwaySpawnedPrefab : DoorwayCleanupScript {
[Header("Removes Doorway Gameobject\nif Doorway instantiates a Connector/Blocker prefab with the target's name")] [Header("Removes Doorway Gameobject\nif Doorway instantiates a Connector/Blocker prefab with the target's name")]

View File

@ -6,12 +6,14 @@ using System.Threading.Tasks;
using UnityEngine; using UnityEngine;
namespace DunGenPlus.Components.DoorwayCleanupScripting { namespace DunGenPlus.Components.DoorwayCleanupScripting {
[Obsolete("Please use DoorwayScriptingParent")]
public class DCSRemoveGameObjectsConnectedDoorway : DoorwayCleanupScriptDoorwayCompare { public class DCSRemoveGameObjectsConnectedDoorway : DoorwayCleanupScriptDoorwayCompare {
[Header("Removes target GameObjects\nif the neighboring doorway's priority matches the operation comparison")] [Header("Removes target GameObjects\nif the neighboring doorway's priority matches the operation comparison")]
[Header("Operation Comparison")] [Header("Operation Comparison")]
public int doorwayPriority; public int doorwayPriority;
public int doorwayPriorityB;
public Operation operation = Operation.Equal; public Operation operation = Operation.Equal;
[Header("Targets")] [Header("Targets")]
@ -20,7 +22,7 @@ namespace DunGenPlus.Components.DoorwayCleanupScripting {
public override void Cleanup(DoorwayCleanup parent) { public override void Cleanup(DoorwayCleanup parent) {
var doorway = parent.doorway; var doorway = parent.doorway;
if (doorway.connectedDoorway == null) return; 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) { if (result) {
foreach(var t in targets) t.SetActive(false); foreach(var t in targets) t.SetActive(false);
} }

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using UnityEngine; using UnityEngine;
namespace DunGenPlus.Components.DoorwayCleanupScripting { namespace DunGenPlus.Components.DoorwayCleanupScripting {
[Obsolete("Please use DoorwayScriptingParent")]
public abstract class DoorwayCleanupScript : MonoBehaviour { public abstract class DoorwayCleanupScript : MonoBehaviour {
public abstract void Cleanup(DoorwayCleanup parent); public abstract void Cleanup(DoorwayCleanup parent);

View File

@ -4,13 +4,44 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using UnityEngine;
namespace DunGenPlus.Components.DoorwayCleanupScripting { namespace DunGenPlus.Components.DoorwayCleanupScripting {
[Obsolete("Please use DoorwayScriptingParent")]
public abstract class DoorwayCleanupScriptDoorwayCompare : DoorwayCleanupScript { 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<Doorway, int, bool> 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<Doorway, Arguments, bool> GetOperation(Operation operation){
switch(operation){ switch(operation){
case Operation.Equal: case Operation.Equal:
return EqualOperation; return EqualOperation;
@ -20,24 +51,48 @@ namespace DunGenPlus.Components.DoorwayCleanupScripting {
return LessThanOperation; return LessThanOperation;
case Operation.GreaterThan: case Operation.GreaterThan:
return GreaterThanOperation; return GreaterThanOperation;
case Operation.LessThanEq:
return LessThanEqualOperation;
case Operation.GreaterThanEq:
return GreaterThanEqualOperation;
case Operation.Between:
return BetweenOperation;
case Operation.BetweenEq:
return BetweenEqualOperation;
} }
return null; return null;
} }
public bool EqualOperation(Doorway other, int doorwayPriority){ public bool EqualOperation(Doorway other, Arguments arguments){
return other.DoorPrefabPriority == doorwayPriority; return other.DoorPrefabPriority == arguments.parameterA;
} }
public bool NotEqualOperation(Doorway other, int doorwayPriority){ public bool NotEqualOperation(Doorway other, Arguments arguments){
return other.DoorPrefabPriority != doorwayPriority; return other.DoorPrefabPriority != arguments.parameterA;
} }
public bool LessThanOperation(Doorway other, int doorwayPriority){ public bool LessThanOperation(Doorway other, Arguments arguments){
return other.DoorPrefabPriority < doorwayPriority; return other.DoorPrefabPriority < arguments.parameterA;
} }
public bool GreaterThanOperation(Doorway other, int doorwayPriority){ public bool GreaterThanOperation(Doorway other, Arguments arguments){
return other.DoorPrefabPriority > doorwayPriority; 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;
} }
} }

View File

@ -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<Doorway> {
[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<GameObject>();
AddNamedReference("blockers", targetReference.BlockerSceneObjects);
targetReference.BlockerSceneObjects = new List<GameObject>();
}
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);
}
}
}
}

View File

@ -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<ScriptAction> 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<IDunGenScriptingParent>().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);
}
}
}
}

View File

@ -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<GameObject> gameObjects;
public OverrideGameObjectState overrideState;
public NamedGameObjectReference(string name, List<GameObject> 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<NamedGameObjectReference> GetNamedReferences { get; }
void AddNamedReference(string name, List<GameObject> gameObjects);
void SetNamedGameObjectState(string name, bool state);
void SetNamedGameObjectOverrideState(string name, OverrideGameObjectState state);
EvaluationContext CreateContext();
}
public abstract class DunGenPlusScriptingParent<T> : 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<NamedGameObjectReference> namedReferences = new List<NamedGameObjectReference>();
public Dictionary<string, NamedGameObjectReference> namedDictionary = new Dictionary<string, NamedGameObjectReference>();
public DunGenScriptingHook GetScriptingHook => callHook;
public List<NamedGameObjectReference> 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<DunGenPlusScript>();
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<GameObject> 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<GameObject> 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();
}
}

View File

@ -32,6 +32,7 @@ namespace DunGenPlus.DevTools.Panels {
private GameObject forcedTilesParentGameobject; private GameObject forcedTilesParentGameobject;
private GameObject branchLoopBoostParentGameobject; private GameObject branchLoopBoostParentGameobject;
private GameObject maxShadowsParentGameobject; private GameObject maxShadowsParentGameobject;
public bool eventCallbackValue = true;
public override void AwakeCall() { public override void AwakeCall() {
Instance = this; 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 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() { public void SetupPanel() {
selectedExtenderer = API.GetDunGenExtender(selectedDungeonFlow); selectedExtenderer = API.GetDunGenExtender(selectedDungeonFlow);
@ -77,6 +79,7 @@ namespace DunGenPlus.DevTools.Panels {
var parentTransform = selectedListGameObject.transform; var parentTransform = selectedListGameObject.transform;
var properties = selectedExtenderer.Properties; var properties = selectedExtenderer.Properties;
manager.CreateBoolInputField(parentTransform, ("Activate DunGenPlus", ActivateDunGenPlusTooltip), selectedExtenderer.Active, SetActivateDunGenPlus); manager.CreateBoolInputField(parentTransform, ("Activate DunGenPlus", ActivateDunGenPlusTooltip), selectedExtenderer.Active, SetActivateDunGenPlus);
manager.CreateBoolInputField(parentTransform, ("EventCallbackScenario state", EventCallbackScenarioTooltip), eventCallbackValue, SetDebugCallbackState);
manager.CreateSpaceUIField(parentTransform); manager.CreateSpaceUIField(parentTransform);
var mainPathTransform = manager.CreateVerticalLayoutUIField(parentTransform); var mainPathTransform = manager.CreateVerticalLayoutUIField(parentTransform);
@ -168,6 +171,10 @@ namespace DunGenPlus.DevTools.Panels {
selectedExtenderer.Active = state; selectedExtenderer.Active = state;
} }
public void SetDebugCallbackState(bool state){
eventCallbackValue = state;
}
public void SetMainPathCount(int value) { public void SetMainPathCount(int value) {
selectedExtenderer.Properties.MainPathProperties.MainPathCount = value; selectedExtenderer.Properties.MainPathProperties.MainPathCount = value;
mainPathParentGameobject.SetActive(value > 1); mainPathParentGameobject.SetActive(value > 1);

View File

@ -56,6 +56,9 @@
<SpecificVersion>False</SpecificVersion> <SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\Libraries\MonoMod.Utils.dll</HintPath> <HintPath>..\..\..\Libraries\MonoMod.Utils.dll</HintPath>
</Reference> </Reference>
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
@ -157,7 +160,24 @@
<Compile Include="Components\MainRoomDoorwayGroups.cs" /> <Compile Include="Components\MainRoomDoorwayGroups.cs" />
<Compile Include="Components\Props\SpawnSyncedObjectCycle.cs" /> <Compile Include="Components\Props\SpawnSyncedObjectCycle.cs" />
<Compile Include="Components\Scrap\RandomGuaranteedScrapSpawn.cs" /> <Compile Include="Components\Scrap\RandomGuaranteedScrapSpawn.cs" />
<Compile Include="Components\Scripting\DoorwayScriptingParent.cs" />
<Compile Include="Components\Scripting\DunGenPlusScript.cs" />
<Compile Include="Components\Scripting\DunGenPlusScriptingParent.cs" />
<Compile Include="DevTools\Panels\AssetsPanel.cs" /> <Compile Include="DevTools\Panels\AssetsPanel.cs" />
<Compile Include="ExpressionParser\EvaluationContext.cs" />
<Compile Include="ExpressionParser\Evaluator.cs" />
<Compile Include="ExpressionParser\ExpressionToken.cs" />
<Compile Include="ExpressionParser\ExpressionTokenType.cs" />
<Compile Include="ExpressionParser\FunctionRoutine.cs" />
<Compile Include="ExpressionParser\Parsing\IExpressionTokenizer.cs" />
<Compile Include="ExpressionParser\Parsing\InfixTokenizer.cs" />
<Compile Include="ExpressionParser\Parsing\InfixToPostfixTokenizer.cs" />
<Compile Include="ExpressionParser\Parsing\KnownOperators.cs" />
<Compile Include="ExpressionParser\Parsing\ListReader.cs" />
<Compile Include="ExpressionParser\Parsing\OperatorType.cs" />
<Compile Include="ExpressionParser\Parsing\RawToken.cs" />
<Compile Include="ExpressionParser\Parsing\RawTokenizer.cs" />
<Compile Include="ExpressionParser\ValueTypeHint.cs" />
<Compile Include="Generation\DunGenPlusGenerationPaths.cs" /> <Compile Include="Generation\DunGenPlusGenerationPaths.cs" />
<Compile Include="Generation\DunGenPlusGeneratorDebug.cs" /> <Compile Include="Generation\DunGenPlusGeneratorDebug.cs" />
<Compile Include="Generation\DunGenPlusGeneratorGlobalProps.cs" /> <Compile Include="Generation\DunGenPlusGeneratorGlobalProps.cs" />

View File

@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace Soukoku.ExpressionParser
{
/// <summary>
/// Context for storing and returning values during an expression evaluation.
/// </summary>
public class EvaluationContext
{
static Dictionary<string, FunctionRoutine> BuiltInFunctions = new Dictionary<string, FunctionRoutine>(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<string, FunctionRoutine> __staticFuncs = new Dictionary<string, FunctionRoutine>(StringComparer.OrdinalIgnoreCase);
readonly Dictionary<string, FunctionRoutine> _instanceFuncs = new Dictionary<string, FunctionRoutine>(StringComparer.OrdinalIgnoreCase);
Func<string, (object, ValueTypeHint)> _fieldLookup;
/// <summary>
/// Initializes a new instance of the <see cref="EvaluationContext"/> class.
/// </summary>
public EvaluationContext() { }
/// <summary>
/// Initializes a new instance of the <see cref="EvaluationContext"/> class.
/// </summary>
/// <param name="fieldLookupRoutine">The field value lookup routine.</param>
public EvaluationContext(Func<string, (object Value, ValueTypeHint TypeHint)> fieldLookupRoutine)
{
_fieldLookup = fieldLookupRoutine;
}
/// <summary>
/// Resolves the field value.
/// </summary>
/// <param name="field">The field.</param>
/// <returns></returns>
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;
/// <summary>
/// Gets/sets the culture used to parse/format expressions.
/// Defaults to en-US for certain reasons.
/// </summary>
public CultureInfo FormatCulture
{
get { return _formatCulture ?? _usCulture; }
set { _formatCulture = value; }
}
/// <summary>
/// Gets the field value.
/// </summary>
/// <param name="field">The field.</param>
/// <returns></returns>
protected virtual (object Value, ValueTypeHint TypeHint) OnResolveFieldValue(string field)
{
return (string.Empty, ValueTypeHint.Auto);
}
/// <summary>
/// Registers a custom function globally.
/// </summary>
/// <param name="functionName">Name of the function.</param>
/// <param name="info">The information.</param>
public static void RegisterGlobalFunction(string functionName, FunctionRoutine info)
{
__staticFuncs[functionName] = info;
}
/// <summary>
/// Registers a custom function with this context instance.
/// </summary>
/// <param name="functionName">Name of the function.</param>
/// <param name="info">The information.</param>
public void RegisterFunction(string functionName, FunctionRoutine info)
{
_instanceFuncs[functionName] = info;
}
/// <summary>
/// Gets the function registered with this context.
/// </summary>
/// <param name="functionName">Name of the function.</param>
/// <returns></returns>
/// <exception cref="System.NotSupportedException"></exception>
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));
}
/// <summary>
/// Gets the function registered with this context.
/// </summary>
/// <param name="functionName">Name of the function.</param>
/// <returns></returns>
/// <exception cref="System.NotSupportedException"></exception>
protected virtual FunctionRoutine OnGetFunction(string functionName)
{
return null;
}
}
}

View File

@ -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
{
/// <summary>
/// An expression evaluator.
/// </summary>
public class Evaluator
{
EvaluationContext _context;
Stack<ExpressionToken> _stack;
/// <summary>
/// Initializes a new instance of the <see cref="Evaluator"/> class.
/// </summary>
/// <param name="context">The context.</param>
/// <exception cref="System.ArgumentNullException">context</exception>
public Evaluator(EvaluationContext context)
{
_context = context ?? throw new ArgumentNullException("context");
}
/// <summary>
/// Evaluates the specified input expression.
/// </summary>
/// <param name="input">The input expression (infix).</param>
/// <param name="coerseToBoolean">if set to <c>true</c> then the result will be coersed to boolean true/false if possible.
/// Anything not "false", "0", or "" is considered true.</param>
/// <returns></returns>
/// <exception cref="NotSupportedException">Unbalanced expression.</exception>
/// <exception cref="System.NotSupportedException"></exception>
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<ExpressionToken>(tokens);
// from https://en.wikipedia.org/wiki/Reverse_Polish_notation
_stack = new Stack<ExpressionToken>();
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<ExpressionToken>(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<decimal, decimal> operation)
{
var op1 = _stack.Pop().ToDecimal(_context);
var res = operation(op1);
_stack.Push(new ExpressionToken(res.ToString(_context.FormatCulture)));
}
void UnaryLogicOperation(Func<string, bool> operation)
{
var op1 = _stack.Pop();
var res = operation(op1.ToString()) ? "1" : "0";
_stack.Push(new ExpressionToken(res));
}
void BinaryLogicOperation(Func<string, string, bool> 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<decimal, decimal, decimal> 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
}
}

View File

@ -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
{
/// <summary>
/// A token split from the initial text input.
/// </summary>
public class ExpressionToken
{
/// <summary>
/// Canonical true value. Actual value is numerical "1".
/// </summary>
public static readonly ExpressionToken True = new ExpressionToken("1");
/// <summary>
/// Canonical false value. Actual value is numerical "0".
/// </summary>
public static readonly ExpressionToken False = new ExpressionToken("0");
internal static readonly NumberStyles NumberParseStyle = NumberStyles.Integer | NumberStyles.AllowDecimalPoint | NumberStyles.AllowCurrencySymbol | NumberStyles.Number;
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionToken"/> class.
/// </summary>
public ExpressionToken() { }
/// <summary>
/// Initializes a new frozen instance of the <see cref="ExpressionToken"/> class
/// with the specified value.
/// </summary>
/// <param name="value">The value.</param>
public ExpressionToken(string value)
{
_type = ExpressionTokenType.Value;
_value = value;
}
RawToken _rawToken; // the raw token that makes this token
/// <summary>
/// Gets the raw token that made this list.
/// </summary>
/// <returns></returns>
public RawToken RawToken { get { return _rawToken; } }
const string FrozenErrorMsg = "Cannot modify frozen token.";
/// <summary>
/// Appends the specified token to this expression.
/// </summary>
/// <param name="token">The token.</param>
/// <exception cref="System.InvalidOperationException"></exception>
public void Append(RawToken token)
{
if (IsFrozen) { throw new InvalidOperationException(FrozenErrorMsg); }
if (_rawToken == null) { _rawToken = token; }
else { _rawToken.Append(token); }
}
/// <summary>
/// Gets a value indicating whether this instance is frozen from append.
/// </summary>
/// <value>
/// <c>true</c> if this instance is frozen; otherwise, <c>false</c>.
/// </value>
public bool IsFrozen { get { return _value != null; } }
/// <summary>
/// Freezes this instance from being appended.
/// </summary>
/// <exception cref="System.InvalidOperationException"></exception>
public void Freeze()
{
if (IsFrozen) { throw new InvalidOperationException(FrozenErrorMsg); }
_value = _rawToken?.ToString();
}
private ExpressionTokenType _type;
/// <summary>
/// Gets or sets the type of the token.
/// </summary>
/// <value>
/// The type of the token.
/// </value>
public ExpressionTokenType TokenType
{
get { return _type; }
set { if (_value == null) { _type = value; } }
}
/// <summary>
/// Gets or sets the type of the operator. This is only used if the <see cref="TokenType"/>
/// is <see cref="ExpressionTokenType.Operator"/>.
/// </summary>
/// <value>
/// The type of the operator.
/// </value>
public OperatorType OperatorType { get; set; }
string _value;
/// <summary>
/// Gets the raw token value.
/// </summary>
/// <value>
/// The value.
/// </value>
public string Value { get { return _value ?? _rawToken?.ToString(); } }
/// <summary>
/// Gets the resolved field value and type hint if token is a field.
/// </summary>
public (object Value, ValueTypeHint TypeHint) FieldValue { get; internal set; }
/// <summary>
/// Returns a <see cref="System.String" /> that represents this instance.
/// </summary>
/// <returns>
/// A <see cref="System.String" /> that represents this instance.
/// </returns>
public override string ToString()
{
switch (TokenType)
{
case ExpressionTokenType.Field:
return FieldValue.Value?.ToString() ?? "";
default:
return Value ?? "";
}
}
#region conversion routines
///// <summary>
///// Check if the value is considered numeric.
///// </summary>
///// <returns></returns>
//public bool IsNumeric()
//{
// if (TokenType == ExpressionTokenType.Field && FieldValue.TypeHint == ValueTypeHint.Text) return false;
// return decimal.TryParse(Value, NumberParseStyle, CultureInfo.InvariantCulture, out decimal dummy);
//}
///// <summary>
///// Check if the value is considered true.
///// </summary>
///// <returns></returns>
//public bool IsTrue(string value)
//{
// if (TokenType == ExpressionTokenType.Field && FieldValue.TypeHint == ValueTypeHint.Text) return false;
// return string.Equals("true", Value, StringComparison.OrdinalIgnoreCase) || value == "1";
//}
///// <summary>
///// Check if the value is considered false.
///// </summary>
///// <returns></returns>
//public bool IsFalse(string value)
//{
// if (TokenType == ExpressionTokenType.Field && FieldValue.TypeHint == ValueTypeHint.Text) return false;
// return string.Equals("false", Value, StringComparison.OrdinalIgnoreCase) || value == "0";
//}
/// <summary>
/// Converts to the double value.
/// </summary>
/// <returns></returns>
/// <exception cref="System.NotSupportedException"></exception>
/// <exception cref="FormatException"></exception>
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));
}
}
/// <summary>
/// Converts to the decimal value.
/// </summary>
/// <returns></returns>
/// <exception cref="System.NotSupportedException"></exception>
/// <exception cref="FormatException"></exception>
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
}
}

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Soukoku.ExpressionParser
{
/// <summary>
/// Indicates the expression token type.
/// </summary>
public enum ExpressionTokenType
{
/// <summary>
/// Invalid token type.
/// </summary>
None,
/// <summary>
/// The token is an operator.
/// </summary>
Operator,
/// <summary>
/// The token is an open parenthesis.
/// </summary>
OpenParenthesis,
/// <summary>
/// The token is a close parenthesis.
/// </summary>
CloseParenthesis,
/// <summary>
/// The token is a function.
/// </summary>
Function,
/// <summary>
/// The token is a comma.
/// </summary>
Comma,
/// <summary>
/// The token is a field reference.
/// </summary>
Field,
/// <summary>
/// The token is from single quoted value.
/// </summary>
SingleQuoted,
/// <summary>
/// The token is from double quoted value.
/// </summary>
DoubleQuoted,
/// <summary>
/// The token is a yet-to-be-parsed value.
/// </summary>
Value,
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Soukoku.ExpressionParser
{
/// <summary>
/// Defines a basic function routine.
/// </summary>
public class FunctionRoutine
{
Func<EvaluationContext, ExpressionToken[], ExpressionToken> _routine;
/// <summary>
/// Initializes a new instance of the <see cref="FunctionRoutine"/> class.
/// </summary>
/// <param name="argCount">The argument count.</param>
/// <param name="routine">The routine.</param>
/// <exception cref="System.ArgumentNullException">routine</exception>
public FunctionRoutine(int argCount, Func<EvaluationContext, ExpressionToken[], ExpressionToken> routine)
{
if (routine == null) { throw new ArgumentNullException("routine"); }
ArgumentCount = argCount;
_routine = routine;
}
/// <summary>
/// Gets the expected argument count.
/// </summary>
/// <value>
/// The argument count.
/// </value>
public int ArgumentCount { get; private set; }
/// <summary>
/// Evaluates using the function routine.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="args">The arguments.</param>
/// <returns></returns>
public ExpressionToken Evaluate(EvaluationContext context, ExpressionToken[] args) { return _routine(context, args); }
}
}

View File

@ -0,0 +1,16 @@
namespace Soukoku.ExpressionParser.Parsing
{
/// <summary>
/// Interface for something that can tokenize an expression.
/// </summary>
public interface IExpressionTokenizer
{
/// <summary>
/// Splits the specified input expression into a list of <see cref="ExpressionToken" /> values.
/// </summary>
/// <param name="input">The input.</param>
/// <returns></returns>
/// <exception cref="System.NotSupportedException"></exception>
ExpressionToken[] Tokenize(string input);
}
}

View File

@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace Soukoku.ExpressionParser.Parsing
{
/// <summary>
/// A tokenizer that parses an input expression string in infix notation into tokens without white spaces
/// in the orders of postfix expressions.
/// </summary>
public class InfixToPostfixTokenizer : IExpressionTokenizer
{
const string UnbalancedParenMsg = "Unbalanced parenthesis in expression.";
List<ExpressionToken> _output;
Stack<ExpressionToken> _stack;
/// <summary>
/// Splits the specified input into a list of <see cref="ExpressionToken" /> values
/// in postfix order.
/// </summary>
/// <param name="input">The input.</param>
/// <returns></returns>
/// <exception cref="System.NotSupportedException"></exception>
public ExpressionToken[] Tokenize(string input)
{
var infixTokens = new InfixTokenizer().Tokenize(input);
_output = new List<ExpressionToken>();
_stack = new Stack<ExpressionToken>();
// 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());
}
}
}
}
}

View File

@ -0,0 +1,206 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace Soukoku.ExpressionParser.Parsing
{
/// <summary>
/// A tokenizer that parses an input expression string in infix notation into tokens without white spaces.
/// </summary>
public class InfixTokenizer : IExpressionTokenizer
{
List<ExpressionToken> _currentTokens;
/// <summary>
/// Splits the specified input into a list of <see cref="ExpressionToken" /> values.
/// </summary>
/// <param name="input">The input.</param>
/// <returns></returns>
/// <exception cref="System.NotSupportedException"></exception>
public ExpressionToken[] Tokenize(string input)
{
_currentTokens = new List<ExpressionToken>();
ExpressionToken lastExpToken = null;
var reader = new ListReader<RawToken>(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<RawToken> 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<RawToken> 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<ExpressionToken> tokens)
{
// do final token parsing based on contexts and cleanup
var reader = new ListReader<ExpressionToken>(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<ExpressionToken> 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));
}
}
}
}

View File

@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Soukoku.ExpressionParser.Parsing
{
/// <summary>
/// Contains recognized operator info.
/// </summary>
public static class KnownOperators
{
static readonly Dictionary<string, OperatorType> DefaultMap = new Dictionary<string, OperatorType>
{
// 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 },
};
/// <summary>
/// Determines whether the specified operator value is recognized.
/// </summary>
/// <param name="operatorValue">The operator value.</param>
/// <returns></returns>
public static bool IsKnown(string operatorValue)
{
return DefaultMap.ContainsKey(operatorValue);
}
/// <summary>
/// Try to get the enum version of the operator string value.
/// </summary>
/// <param name="operatorValue">The operator value.</param>
/// <returns></returns>
public static OperatorType TryMap(string operatorValue)
{
if (DefaultMap.ContainsKey(operatorValue))
{
return DefaultMap[operatorValue];
}
return OperatorType.None;
}
/// <summary>
/// Gets the precedence of an operator.
/// </summary>
/// <param name="type">The type.</param>
/// <returns></returns>
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;
}
/// <summary>
/// Determines whether the operator is left-to-right associative (true) or right-to-left (false).
/// </summary>
/// <param name="type">The type.</param>
/// <returns></returns>
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;
}
}
}

View File

@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Soukoku.ExpressionParser.Parsing
{
/// <summary>
/// A simple reader for an IList.
/// </summary>
/// <typeparam name="TItem">The type of the item in the list.</typeparam>
public class ListReader<TItem>
{
IList<TItem> _list;
/// <summary>
/// Initializes a new instance of the <see cref="ListReader{TItem}"/> class.
/// </summary>
/// <param name="list">The list to read.</param>
/// <exception cref="System.ArgumentNullException">list</exception>
public ListReader(IList<TItem> list)
{
if (list == null) { throw new ArgumentNullException("list"); }
_list = list;
}
private int _position;
/// <summary>
/// Gets or sets the position of the reader. This is the 0-based index.
/// </summary>
/// <value>
/// The position.
/// </value>
/// <exception cref="System.ArgumentOutOfRangeException"></exception>
public int Position
{
get { return _position; }
set
{
if (value < 0 || value > _list.Count)
{
throw new ArgumentOutOfRangeException("value");
}
_position = value;
}
}
/// <summary>
/// Gets a value indicating whether the reader has reached the end of list.
/// </summary>
/// <value>
/// <c>true</c> if this instance is eol; otherwise, <c>false</c>.
/// </value>
public bool IsEnd { get { return _position >= _list.Count; } }
/// <summary>
/// Reads the current item in the list and moves the <see cref="Position" /> forward.
/// </summary>
/// <returns></returns>
/// <exception cref="System.ArgumentOutOfRangeException"></exception>
public TItem Read()
{
return _list[Position++];
}
/// <summary>
/// Peeks the current item in the list without moving the <see cref="Position"/>.
/// </summary>
/// <returns></returns>
/// <exception cref="System.ArgumentOutOfRangeException"></exception>
public TItem Peek()
{
return Peek(0);
}
/// <summary>
/// Peeks the item in the list without moving the <see cref="Position" />.
/// </summary>
/// <param name="offset">The offset from current position.</param>
/// <returns></returns>
/// <exception cref="System.ArgumentOutOfRangeException"></exception>
public TItem Peek(int offset)
{
// let list throw the exception.
return _list[Position + offset];
}
}
}

View File

@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Soukoku.ExpressionParser.Parsing
{
/// <summary>
/// Indicates the recognized operator types.
/// </summary>
public enum OperatorType
{
/// <summary>
/// Unspecified default value.
/// </summary>
None,
/// <summary>
/// ++ after a value.
/// </summary>
PostIncrement,
/// <summary>
/// -- after a value.
/// </summary>
PostDecrement,
/// <summary>
/// ++ before a value.
/// </summary>
PreIncrement,
/// <summary>
/// -- before a value.
/// </summary>
PreDecrement,
/// <summary>
/// + before a value.
/// </summary>
UnaryPlus,
/// <summary>
/// - before a value.
/// </summary>
UnaryMinus,
/// <summary>
/// ! before a value.
/// </summary>
LogicalNegation,
/// <summary>
/// * between values.
/// </summary>
Multiplication,
/// <summary>
/// / between values.
/// </summary>
Division,
/// <summary>
/// % between values.
/// </summary>
Modulus,
/// <summary>
/// + between values.
/// </summary>
Addition,
/// <summary>
/// - between values.
/// </summary>
Subtraction,
/// <summary>
/// &lt; between values.
/// </summary>
LessThan,
/// <summary>
/// &lt;= between values.
/// </summary>
LessThanOrEqual,
/// <summary>
/// &gt; between values.
/// </summary>
GreaterThan,
/// <summary>
/// &gt;= between values.
/// </summary>
GreaterThanOrEqual,
/// <summary>
/// == between values.
/// </summary>
Equal,
/// <summary>
/// != between values.
/// </summary>
NotEqual,
/// <summary>
/// &amp; between values.
/// </summary>
BitwiseAnd,
/// <summary>
/// | between values.
/// </summary>
BitwiseOr,
/// <summary>
/// &amp;&amp; between values.
/// </summary>
LogicalAnd,
/// <summary>
/// || between values.
/// </summary>
LogicalOr,
/// <summary>
/// = between values.
/// </summary>
Assignment,
/// <summary>
/// += between values.
/// </summary>
AdditionAssignment,
/// <summary>
/// -= between values.
/// </summary>
SubtractionAssignment,
/// <summary>
/// *= between values.
/// </summary>
MultiplicationAssignment,
/// <summary>
/// /= between values.
/// </summary>
DivisionAssignment,
/// <summary>
/// %= between values.
/// </summary>
ModulusAssignment,
}
}

View File

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Soukoku.ExpressionParser.Parsing
{
/// <summary>
/// A low-level token split from the initial text input.
/// </summary>
public class RawToken
{
/// <summary>
/// Initializes a new instance of the <see cref="RawToken"/> class.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="position">The position.</param>
internal RawToken(RawTokenType type, int position)
{
TokenType = type;
Position = position;
ValueBuilder = new StringBuilder();
}
/// <summary>
/// Gets the token type.
/// </summary>
/// <value>
/// The type.
/// </value>
public RawTokenType TokenType { get; private set; }
/// <summary>
/// Gets the starting position of this token in the original input.
/// </summary>
/// <value>
/// The position.
/// </value>
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);
}
}
/// <summary>
/// Gets the token value.
/// </summary>
/// <value>
/// The value.
/// </value>
public string Value { get { return ValueBuilder.ToString(); } }
/// <summary>
/// Returns a <see cref="System.String" /> that represents this instance.
/// </summary>
/// <returns>
/// A <see cref="System.String" /> that represents this instance.
/// </returns>
public override string ToString()
{
return Value;
}
}
/// <summary>
/// Indicates the low-level token type.
/// </summary>
public enum RawTokenType
{
/// <summary>
/// Invalid token type.
/// </summary>
None,
/// <summary>
/// Token is white space.
/// </summary>
WhiteSpace,
/// <summary>
/// Token is a symbol.
/// </summary>
Symbol,
/// <summary>
/// Token is not symbol or white space.
/// </summary>
Literal,
}
}

View File

@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace Soukoku.ExpressionParser.Parsing
{
/// <summary>
/// A low-level tokenizer that parses an input expression string into tokens.
/// </summary>
public class RawTokenizer
{
static readonly char[] DefaultSymbols = new[]
{
'+', '-', '*', '/', '=', '%', '^',
',', '<', '>', '&', '|', '!',
'(', ')', '{', '}', '[', ']',
'"', '\'', '~'
};
/// <summary>
/// Initializes a new instance of the <see cref="RawTokenizer"/> class.
/// </summary>
public RawTokenizer() : this(null) { }
/// <summary>
/// Initializes a new instance of the <see cref="RawTokenizer" /> class.
/// </summary>
/// <param name="symbols">The char values to count as symbols. If null the <see cref="DefaultSymbols"/> will be used.</param>
public RawTokenizer(params char[] symbols)
{
_symbols = symbols ?? DefaultSymbols;
}
char[] _symbols;
/// <summary>
/// Gets the char values that count as symbols for this tokenizer.
/// </summary>
/// <value>
/// The symbols.
/// </value>
public char[] GetSymbols() { return (char[])_symbols.Clone(); }
/// <summary>
/// Splits the specified input into a list of <see cref="RawToken"/> values using white space and symbols.
/// The tokens can be recombined to rebuild the original input exactly.
/// </summary>
/// <param name="input">The input.</param>
/// <returns></returns>
public RawToken[] Tokenize(string input)
{
var tokens = new List<RawToken>();
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<RawToken> 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;
}
}
}

View File

@ -0,0 +1,17 @@
namespace Soukoku.ExpressionParser
{
/// <summary>
/// Used to indicate how to handle resolve field value.
/// </summary>
public enum ValueTypeHint
{
/// <summary>
/// Value is converted to suitable type for comparison purposes.
/// </summary>
Auto,
/// <summary>
/// Value is forced to be text for comparison purposes.
/// </summary>
Text,
}
}

View File

@ -17,6 +17,7 @@ using UnityEngine.Rendering.HighDefinition;
using BepInEx.Logging; using BepInEx.Logging;
using DunGenPlus.DevTools; using DunGenPlus.DevTools;
using DunGenPlus.Patches; using DunGenPlus.Patches;
using DunGenPlus.DevTools.Panels;
[assembly: SecurityPermission( SecurityAction.RequestMinimum, SkipVerification = true )] [assembly: SecurityPermission( SecurityAction.RequestMinimum, SkipVerification = true )]
namespace DunGenPlus.Generation { namespace DunGenPlus.Generation {
@ -36,7 +37,7 @@ namespace DunGenPlus.Generation {
ActiveAlternative = true; ActiveAlternative = true;
var props = extender.Properties.Copy(extender.Version); 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); Instance.Events.OnModifyDunGenExtenderProperties.Invoke(props, callback);
props.NormalNodeArchetypesProperties.SetupProperties(generator); props.NormalNodeArchetypesProperties.SetupProperties(generator);
Properties = props; Properties = props;

View File

@ -1,36 +1,92 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using DunGen;
using DunGen.Adapters; using DunGen.Adapters;
using DunGenPlus.Components; using DunGenPlus.Components;
using DunGenPlus.Components.Scripting;
using DunGenPlus.Generation; using DunGenPlus.Generation;
using DunGenPlus.Utils; using DunGenPlus.Utils;
using static DunGenPlus.Managers.DoorwayManager;
namespace DunGenPlus.Managers { namespace DunGenPlus.Managers {
public static class DoorwayManager { public static class DoorwayManager {
public static ActionList onMainEntranceTeleportSpawnedEvent = new ActionList("onMainEntranceTeleportSpawned"); public static ActionList onMainEntranceTeleportSpawnedEvent = new ActionList("onMainEntranceTeleportSpawned");
public static List<DoorwayCleanup> doorwayCleanupList; //public static List<DoorwayCleanup> doorwayCleanupList;
public class Scripts {
public List<IDunGenScriptingParent> scriptList;
public List<Action> actionList;
public Scripts(){
scriptList = new List<IDunGenScriptingParent>();
actionList = new List<Action>();
}
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<DunGenScriptingHook, Scripts> scriptingLists;
public static void ResetList(){ public static void ResetList(){
doorwayCleanupList = new List<DoorwayCleanup>(); //doorwayCleanupList = new List<DoorwayCleanup>();
scriptingLists = new Dictionary<DunGenScriptingHook, Scripts>();
foreach(DunGenScriptingHook e in Enum.GetValues(typeof(DunGenScriptingHook))){
scriptingLists.Add(e, new Scripts());
}
} }
public static void AddDoorwayCleanup(DoorwayCleanup cleanup){ 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) { if (DunGenPlusGenerator.Active) {
foreach(var d in doorwayCleanupList){
d.SetBlockers(false); //foreach(var d in doorwayCleanupList){
d.Cleanup(); // d.SetBlockers(false);
// d.Cleanup();
// Plugin.logger.LogWarning(d.GetComponentInParent<Tile>().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) // 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{ try{
var dungeonGen = RoundManager.Instance.dungeonGenerator; var dungeonGen = RoundManager.Instance.dungeonGenerator;
@ -45,5 +101,11 @@ namespace DunGenPlus.Managers {
} }
} }
public static void SetLevelObjectVariablesFunction(){
if (DunGenPlusGenerator.Active) {
scriptingLists[DunGenScriptingHook.SetLevelObjectVariables ].Call();
}
}
} }
} }

View File

@ -69,5 +69,12 @@ namespace DunGenPlus.Patches {
} }
} }
[HarmonyPrefix]
[HarmonyPriority(Priority.First)]
[HarmonyPatch(typeof(RoundManager), "SetLevelObjectVariables")]
public static void SetLevelObjectVariablesPatch (ref RoundManager __instance) {
DoorwayManager.SetLevelObjectVariablesFunction();
}
} }
} }

View File

@ -3,6 +3,7 @@ using BepInEx.Logging;
using DunGen; using DunGen;
using DunGen.Graph; using DunGen.Graph;
using DunGenPlus.Collections; using DunGenPlus.Collections;
using DunGenPlus.Components.Scripting;
using DunGenPlus.Generation; using DunGenPlus.Generation;
using DunGenPlus.Managers; using DunGenPlus.Managers;
using DunGenPlus.Patches; using DunGenPlus.Patches;
@ -26,7 +27,7 @@ namespace DunGenPlus {
internal const string modGUID = "dev.ladyalice.dungenplus"; internal const string modGUID = "dev.ladyalice.dungenplus";
private const string modName = "Dungeon Generation Plus"; 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); internal readonly Harmony Harmony = new Harmony(modGUID);
@ -62,7 +63,7 @@ namespace DunGenPlus {
Assets.LoadAssets(); Assets.LoadAssets();
Assets.LoadAssetBundle(); Assets.LoadAssetBundle();
DoorwayManager.onMainEntranceTeleportSpawnedEvent.AddEvent("DoorwayCleanup", DoorwayManager.onMainEntranceTeleportSpawnedFunction); DoorwayManager.onMainEntranceTeleportSpawnedEvent.AddEvent("DoorwayCleanup", DoorwayManager.OnMainEntranceTeleportSpawnedFunction);
} }
} }

View File

@ -1,8 +1,11 @@
using System; using BepInEx.Logging;
using DunGenPlus.Components.Scripting;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events; using UnityEngine.Events;
namespace DunGenPlus.Utils { 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);
}
}
}
} }

View File

@ -73,10 +73,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="DunGenExtenderPropertyDrawer.cs" /> <Compile Include="DunGenExtenderPropertyDrawer.cs" />
<Compile Include="NamedGameObjectReferencePropertyDrawer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="PropertyDrawerUtility.cs" /> <Compile Include="PropertyDrawerUtility.cs" />
<Compile Include="PropertyOverridePropertyDrawer.cs" /> <Compile Include="PropertyOverridePropertyDrawer.cs" />
<Compile Include="ReadOnlyPropertyDrawer.cs" /> <Compile Include="ReadOnlyPropertyDrawer.cs" />
<Compile Include="ScriptActionPropertyDrawer.cs" />
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup> <PropertyGroup>

View File

@ -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;
}
}
}

View File

@ -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));
}
}
}
}