Wednesday, July 11, 2012

EPI #17: Battles, part 3

If you remember from EPI #15, all Spells have one or more effect that they convey to their target.  This is pretty much where all of the flavor of battles comes from.  It's where designers should be able to get super creative and have a lot of fun making up horrible things for Monsters and PlayerCharacters to do to each other.  I'm taking the same approach to defining and parsing these as I am with LAVs.

For the same reasons, then, if I wanted to do these right, I'd jump through the hoops of coming up with a grammar and writing a parser and generally treating Spell effects like a light scripting language.  However, I'm running a little low on time, so I'm gonna stick with regular expressions and if-elif-elif-... blocks.  (If you remember from EPI #1, I made it my goal to have some kind of playable release done by July 16th.  That's Monday. Ha ha ha.)

But Spell effects pose a problem for that approach.  How am I supposed to handle something like

<effect>MODIFY_STATISTIC -1*(1-("target.defense"/100))*"caster.attack_power"+5 TO "target.hit_points"</effect>

In what amounts to a switch statement?  Regular expressions and switches are definitely not suited to evaluating arbitrarily complicated mathematical expressions, so I need a different solution.

Enter Python's eval() function.  eval() takes a string of text and asks the Python interpreter to treat it as if it were Python code.  Using eval() on raw user input (for example, an expression written by a designer as part of an Spell effect) is an unspeakably bad idea.  This is because you leave yourself wide open to code injection attacks.  The Python interpreter does what it's told, and doesn't think twice about whether the code it's interpreting might be catastrophically harmful.  For example, what if a clever designer wrote a Spell that looked like this:

<effect>MODIFY_STATISTIC import os; os.system("rm -rf /") TO "target.hit_points"</effect>

If the player were running EPI as root on a Linux machine, casting that Spell would delete his entire hard drive.  Not good.  This example is a little dramatic, yes, and strictly speaking people should only use their machines as root or an administrative user for exactly as long as they need to, but the point is that I don't want to leave my code open to these kind attacks, but I don't want to write a top-down parser either.

So I choose to treat these expressions very carefully instead.  First, I match them against the regular expression /import/i, which makes sure that the expression I'm about to evaluate doesn't try to import any modules.  Then, using more regular expressions, I substitute occurrences of things like "target.defense" for self.target.statistics['defense'], replacing those quoted quantities with their numerical equivalents.  Then, I pass the new formula to eval( formula, {'__builtins__':None}, {} ). The two optional arguments, {'__builtins__':None} and {}, prevent eval() from using any built-in, global, or local variables.  This, combined with the /import/i filter, permit eval() to operate only on literal expressions.  It's not allowed to reference any variables, import any modules, or make any function calls.

It may be possible that a designer could obfuscate their module import somehow and slip it past my regular expression filter; I don't know.  But I'm not really too concerned about that.  I plan on releasing the engine open-source anyways, and anyone with enough experience with Python to know how to do such a thing would have a much easier time just modifying the engine to do something malicious in the first place.

So I guess you make sure you only get the engine from a trustworthy source (I'll probably throw it up on Sourceforge when it's ready), and definitely don't run it as root/Administrator.

No comments:

Post a Comment