using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Reflection; using Microsoft.Xna.Framework; namespace DataDrivenExample { class Scripting { public static Scripting GetInstance() { if (mInstance == null) { mInstance = new Scripting(); } return mInstance; } public void initialize(World w) { mWorld = w; } // Methods that scritps can call: /// /// Get a pointer to a WorldElement, given a string Identifier /// /// String identifier for the WorldElement we want /// Pointer to a WorldElement with the given ID public WorldElement GetWorldElement(string id) { return mWorld.GetWorldElement(id); } public void PrintDebugText(String txt) { DebugText.GetInstance().AddDebugText(txt); } /// /// Execute a script after a certain amount of time has gone by /// /// Script to execute /// Time to wait to execute script public void TimedScript(string script, float time) { mScriptTimers.Add(new ScriptTimingPair(script, time)); } /// /// Execute a script. The script is a string, representing a Method call. The first /// method invoked needs to be one of the methods in this Scripting class (such as /// GetWroldElement, or TimedScript). Example use: /// Assuming that the WorldElement with the ID "elemID" has a method SetPosition that /// takes two floating point numbers, we could invoke that method with the following script /// call: /// /// ExecuteScript("GetWorldElement(\"elemID\").SetPosition(3,4)") /// /// Both ' and " can be used as delimiters within the string, to allow calls like: /// ExecuteScript("TimedScript(\"GetWorldElement(\'pf1\').ResetPosition()\",5)" /// /// /// public void ExecuteScript(string scriptsString) { int offset = 0; while (offset < scriptsString.Length) { Eval(this, scriptsString, ref offset, false); SkipWhiteSpaces(scriptsString, ref offset); if (offset < scriptsString.Length && scriptsString[offset] == ';') { offset++; SkipWhiteSpaces(scriptsString, ref offset); } else if (offset < scriptsString.Length) { throw new ArgumentException("Poorly formed script: " + scriptsString); } } } public void Update(GameTime time) { List elemsToRemove = null; foreach (ScriptTimingPair stp in mScriptTimers) { stp.TimeRemaining -= (float) time.ElapsedGameTime.TotalSeconds; if (stp.TimeRemaining < 0) { if (elemsToRemove == null) elemsToRemove = new List(); elemsToRemove.Add(stp); ExecuteScript(stp.Script); } } if (elemsToRemove != null) { foreach (ScriptTimingPair stp in elemsToRemove) { mScriptTimers.Remove(stp); } } } public void RemoveWorldElement(string id) { mWorld.RemoveWorldElement(id); } // Methods to parse and execute scripts /// /// Parse and evaluate a script (or a piece of a script). The offset will be advanced over the /// portion of the script that is parsed /// /// Object to use for any references to methods / properties. Initially, /// this should be a reference to the global script object /// Script to parse /// Offset in scriptt to start parsing -- will be advanced to point /// to just past the section of the script that is evaluated /// True if literals are allowed /// Object representing the value of the evaluated script protected Object Eval(Object baseObject, string script, ref int offset, bool literalsOK) { SkipWhiteSpaces(script, ref offset); if (offset == script.Length) return null; if (literalsOK) { if (script[offset] == '\"' || script[offset] == '\'') { char stringDelimiter = script[offset]; offset++; int literalInitialOffset = offset; while (offset < script.Length && script[offset] != stringDelimiter) { offset++; } if (offset == script.Length) { throw new ArgumentException("Bad string literal in script:" + script); } string literalVal = script.Substring(literalInitialOffset, offset - literalInitialOffset); offset++; return literalVal; } else if (char.IsDigit(script[offset])) { int commaPos = script.IndexOf(',', offset); int closeParenPos = script.IndexOf(')', offset); if (commaPos > 0 && commaPos < closeParenPos) { float number = float.Parse(script.Substring(offset, commaPos - offset)); offset = commaPos; return number; } else if (closeParenPos > 0) { float number = float.Parse(script.Substring(offset, closeParenPos - offset)); offset = closeParenPos; return number; } else { throw new ArgumentException("Bad floating literal in script " + script + " at position " + offset.ToString()); } } else if (script.Length > offset + 4 && script.Substring(offset, 4) == "true" && (script[offset + 4] == ',' || script[offset + 4] == ')' || script[offset + 4] == ' ')) { offset += 4; return true; } else if (script.Length > offset + 5 && script.Substring(offset, 5) == "false" && (script[offset + 5] == ',' || script[offset + 5] == ')' || script[offset + 5] == ' ')) { offset += 5; return false; } } // TODO: Add support for other literals (int, float, etc) int nextDot = script.IndexOf('.', offset); int nextParen = script.IndexOf('(', offset); int nextSemi = script.IndexOf(';', offset); if ((nextParen > 0) && (nextParen < nextDot || nextDot < 0) && (nextParen < nextSemi || nextSemi < 0)) { String methodName = script.Substring(offset, nextParen - offset); MethodInfo method = baseObject.GetType().GetMethod(methodName); if (method == null) { throw new ArgumentException("Bad method name in script: " + methodName); } else { offset = nextParen + 1; Object[] parameters = ParseParamters(this, script, ref offset); Object methodReturn = method.Invoke(baseObject, parameters); if (offset < script.Length && script[offset] == '.') { offset++; return Eval(methodReturn, script, ref offset, false); } SkipWhiteSpaces(script, ref offset); return methodReturn; } } else if (nextDot > 0 && ((nextDot < nextSemi) || nextSemi < 0)) { PropertyInfo propInfo = baseObject.GetType().GetProperty(script.Substring(offset, nextDot - offset)); offset = nextDot + 1; return Eval(propInfo.GetValue(baseObject, null), script, ref offset, false); } else if (nextSemi > 0) { PropertyInfo propInfo = baseObject.GetType().GetProperty(script.Substring(offset, nextSemi - offset)); offset = nextSemi; if (propInfo != null) { return propInfo.GetValue(baseObject, null); } else { throw new ArgumentException("Bad property name in script: " + script.Substring(offset, nextSemi - offset)); } } else { PropertyInfo propInfo = baseObject.GetType().GetProperty(script.Substring(offset)); if (propInfo != null) { return propInfo.GetValue(baseObject, null); } else { throw new ArgumentException("Bad property name in script: " + script.Substring(offset)); } } } /// /// Given a string and an offset, increment the offset until the string at that /// index is not a whitespace character /// /// String to search for non-whitespace character /// Index to search from for non-whitespace characeter void SkipWhiteSpaces(string str, ref int offset) { while (offset < str.Length && char.IsWhiteSpace(str[offset])) offset++; } /// /// Parse all of the parameters of a method call imbedded within a script. /// The "offset" parameter should point to the very beginning of the parameter list /// [right after the opening paren (]. After this method is finished, the offset /// parameter should point just past the parameter list [just past ending param )] /// /// Object to use as base for properties / methods when evaluating parameters /// The entire script /// The offset in script of the beginning of the parameter list /// List of parsed parameters protected Object[] ParseParamters(Object baseObject, string script, ref int offset) { SkipWhiteSpaces(script, ref offset); if (script[offset] == ')') { offset++; return new Object[0]; } List paramList = new List(); while (offset < script.Length && script[offset] != ')') { paramList.Add(Eval(baseObject, script, ref offset, true)); SkipWhiteSpaces(script, ref offset); if (offset < script.Length && script[offset] == ',') offset++; } if (offset == script.Length) { throw new ArgumentException("Bad argument to method call in script:" + script + " at index " + offset.ToString()); } offset++; return paramList.ToArray(); } class ScriptTimingPair { public ScriptTimingPair(string scriptName, float time) { Script = scriptName; TimeRemaining = time; } public string Script { get; set; } public float TimeRemaining { get; set; } } private World mWorld; private List mScriptTimers; private static Scripting mInstance; private Scripting() { mScriptTimers = new List(); } } }