More work on the safe python AIs:
- Using safe.py from the given SVN repository, which is much cleaner, no more notes or tests. - Added a preprocessor, as a way to allow a limited import again. - Made runtime docs work again, with a --python-api switch instead of the previous AI hack.
This commit is contained in:
parent
69d5e52e60
commit
37ec6e8aaf
6 changed files with 115 additions and 284 deletions
|
@ -80,16 +80,11 @@ def output(topics, level):
|
|||
(doc and doc.replace("\n", "\n\n") or "..."))
|
||||
|
||||
if __name__ == "__main__":
|
||||
# If we are run as script, start a scenario with this python AI and a single turn,
|
||||
# so the documentation will gets printed.
|
||||
import os, sys
|
||||
scenario = "multiplayer_Blitz" # doesn't matter, just needs to be valid
|
||||
controllers = "--controller1=ai --algorithm1=python_ai " +\
|
||||
"--parm1=python_script:documentation.py --controller2=ai"
|
||||
os.system("src/wesnoth --nogui --multiplayer --turns=1 --scenario=%s %s" %
|
||||
(scenario, controllers))
|
||||
import os
|
||||
# If we are run as script, run wesnoth with the --python-api switch.
|
||||
os.system("src/wesnoth --python-api")
|
||||
else:
|
||||
# If we are run as a python AI, simply output the documentation to stdout.
|
||||
# If we are run as a python script, output the documentation to stdout.
|
||||
import wesnoth
|
||||
topics = []
|
||||
myhelp("wesnoth", topics)
|
||||
|
|
39
data/ais/parse.py
Normal file
39
data/ais/parse.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
import re, os, safe
|
||||
|
||||
def include(matchob):
|
||||
"""
|
||||
Regular expression callback. Handles a single import statement, returning
|
||||
the included code.
|
||||
"""
|
||||
names = [x.strip() for x in matchob.group(1).split(",")]
|
||||
r = ""
|
||||
for name in names:
|
||||
for path in pathes:
|
||||
includefile = os.path.join(path, name)
|
||||
try:
|
||||
code = parse_file(includefile + ".py")
|
||||
break
|
||||
except IOError:
|
||||
pass
|
||||
else:
|
||||
raise safe.SafeException("Could not include %s." % name)
|
||||
return None
|
||||
r += code
|
||||
return r
|
||||
|
||||
def parse_file(name):
|
||||
"""
|
||||
Simple pre-parsing of scripts, all it does is allow importing other scripts.
|
||||
"""
|
||||
abspath = os.path.abspath(name)
|
||||
if abspath in already: return ""
|
||||
already[abspath] = 1
|
||||
code = file(abspath).read()
|
||||
code = re.sub(r"^import\s+(.*)", include, code, re.M)
|
||||
return code
|
||||
|
||||
def parse(name):
|
||||
global already
|
||||
already = {}
|
||||
return parse_file(name)
|
||||
|
248
data/ais/safe.py
248
data/ais/safe.py
|
@ -5,61 +5,10 @@ This code is not guaranteed to work. Use at your own risk!
|
|||
Beware! Trust no one!
|
||||
|
||||
Please e-mail philhassey@yahoo.com if you find any security holes.
|
||||
svn://www.imitationpickles.org/pysafe/trunk
|
||||
|
||||
Known limitations:
|
||||
- Safe doesn't have any testing for timeouts/DoS. One-liners
|
||||
like these will lock up the system: "while 1: pass", "234234**234234"
|
||||
- Lots of (likely) safe builtins and safe AST Nodes are not allowed.
|
||||
I suppose you can add them to the whitelist if you want them. I
|
||||
trimmed it down as much as I thought I could get away with and still
|
||||
have useful python code.
|
||||
- Might not work with future versions of python - this is made with
|
||||
python 2.4 in mind. _STR_NOT_BEGIN might have to be extended
|
||||
in the future with more magic variable prefixes. Or you can
|
||||
switch to conservative mode, but then even variables like "my_var"
|
||||
won't work, which is sort of a nuisance.
|
||||
- If you get data back from a safe_exec, don't call any functions
|
||||
or methods - they might not be safe with __builtin__ restored
|
||||
to its normal state. Work with them again via an additional safe_exec.
|
||||
- The "context" sent to the functions is not tested at all. If you
|
||||
pass in a dangerous function {'myfile':file} the code will be able
|
||||
to call it.
|
||||
See README.txt, NOTES.txt, CHANGES.txt for more details.
|
||||
"""
|
||||
|
||||
# Built-in Objects
|
||||
# http://docs.python.org/lib/builtin.html
|
||||
|
||||
# AST Nodes - compiler
|
||||
# http://docs.python.org/lib/module-compiler.ast.html
|
||||
|
||||
# Types and members - inspection
|
||||
# http://docs.python.org/lib/inspect-types.html
|
||||
# The standard type heirarchy
|
||||
# http://docs.python.org/ref/types.html
|
||||
|
||||
# Based loosely on - Restricted "safe" eval - by Babar K. Zafar
|
||||
# (it isn't very safe, but it got me started)
|
||||
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496746
|
||||
|
||||
# Securing Python: Controlling the abilities of the interpreter
|
||||
# (or - why even trying this is likely to end in tears)
|
||||
# http://us.pycon.org/common/talkdata/PyCon2007/062/PyCon_2007.pdf
|
||||
|
||||
# Changes
|
||||
# 2007-03-13: added test for unicode strings that contain __, etc
|
||||
# 2007-03-09: renamed safe_eval to safe_exec, since that's what it is.
|
||||
# 2007-03-09: use "exec code in context" , because of test_misc_recursive_fnc
|
||||
# 2007-03-09: Removed 'type' from _BUILTIN_OK - see test_misc_type_escape
|
||||
# 2007-03-08: Cleaned up the destroy / restore mechanism, added more tests
|
||||
# 2007-03-08: Fixed how contexts work.
|
||||
# 2007-03-07: Added test for global node
|
||||
# 2007-03-07: Added test for SyntaxError
|
||||
# 2007-03-07: Fixed an issue where the context wasn't being reset (added test)
|
||||
# 2007-03-07: Added unittest for dir()
|
||||
# 2007-03-07: Removed 'isinstance', 'issubclass' from builtins whitelist
|
||||
# 2007-03-07: Removed 'EmptyNode', 'Global' from AST whitelist
|
||||
# 2007-03-07: Added import __builtin__; s/__builtins__/__builtin__
|
||||
|
||||
import compiler
|
||||
import __builtin__
|
||||
|
||||
|
@ -118,23 +67,16 @@ def _check_ast(code):
|
|||
ast = compiler.parse(code)
|
||||
_check_node(ast)
|
||||
|
||||
# r = [v for v in dir(__builtin__) if v[0] != '_' and v[0] == v[0].upper()] ; r.sort() ; print r
|
||||
_BUILTIN_OK = [
|
||||
'__debug__','quit','exit',
|
||||
|
||||
'ArithmeticError', 'AssertionError', 'AttributeError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FloatingPointError', 'FutureWarning', 'IOError', 'ImportError', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'None', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'OverflowWarning', 'PendingDeprecationWarning', 'ReferenceError', 'RuntimeError', 'RuntimeWarning', 'StandardError', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError',
|
||||
|
||||
'abs', 'bool', 'cmp', 'complex', 'dict', 'divmod', 'filter', 'float', 'frozenset', 'hex', 'int', 'len', 'list', 'long', 'map', 'max', 'min', 'object', 'oct', 'pow', 'range', 'reduce', 'repr', 'round', 'set', 'slice', 'str', 'sum', 'tuple', 'xrange', 'zip',
|
||||
'isinstance', 'issubclass']
|
||||
|
||||
#this is zope's list...
|
||||
#in ['False', 'None', 'True', 'abs', 'basestring', 'bool', 'callable',
|
||||
#'chr', 'cmp', 'complex', 'divmod', 'float', 'hash',
|
||||
#'hex', 'id', 'int', 'isinstance', 'issubclass', 'len',
|
||||
#'long', 'oct', 'ord', 'pow', 'range', 'repr', 'round',
|
||||
#'str', 'tuple', 'unichr', 'unicode', 'xrange', 'zip']:
|
||||
|
||||
|
||||
'Warning',
|
||||
'None','True','False',
|
||||
'abs', 'bool', 'callable', 'cmp', 'complex', 'dict', 'divmod', 'filter',
|
||||
'float', 'frozenset', 'hex', 'int', 'isinstance', 'issubclass', 'len',
|
||||
'list', 'long', 'map', 'max', 'min', 'object', 'oct', 'pow', 'range',
|
||||
'repr', 'round', 'set', 'slice', 'str', 'sum', 'tuple', 'xrange', 'zip',
|
||||
]
|
||||
|
||||
_BUILTIN_STR = [
|
||||
'copyright','credits','license','__name__','__doc__',
|
||||
]
|
||||
|
@ -187,173 +129,3 @@ def safe_exec(code,context = None):
|
|||
safe_check(code)
|
||||
safe_run(code,context)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import unittest
|
||||
|
||||
class TestSafe(unittest.TestCase):
|
||||
def test_check_node_import(self):
|
||||
self.assertRaises(CheckNodeException,safe_exec,"import os")
|
||||
def test_check_node_from(self):
|
||||
self.assertRaises(CheckNodeException,safe_exec,"from os import *")
|
||||
def test_check_node_exec(self):
|
||||
self.assertRaises(CheckNodeException,safe_exec,"exec 'None'")
|
||||
def test_check_node_raise(self):
|
||||
self.assertRaises(CheckNodeException,safe_exec,"raise Exception")
|
||||
def test_check_node_global(self):
|
||||
self.assertRaises(CheckNodeException,safe_exec,"global abs")
|
||||
|
||||
def test_check_str_x(self):
|
||||
self.assertRaises(CheckStrException,safe_exec,"x__ = 1")
|
||||
def test_check_str_str(self):
|
||||
self.assertRaises(CheckStrException,safe_exec,"x = '__'")
|
||||
def test_check_str_class(self):
|
||||
self.assertRaises(CheckStrException,safe_exec,"None.__class__")
|
||||
def test_check_str_func_globals(self):
|
||||
self.assertRaises(CheckStrException,safe_exec,"def x(): pass; x.func_globals")
|
||||
def test_check_str_init(self):
|
||||
safe_exec("def __init__(self): pass")
|
||||
def test_check_str_subclasses(self):
|
||||
self.assertRaises(CheckStrException,safe_exec,"object.__subclasses__")
|
||||
def test_check_str_properties(self):
|
||||
code = """
|
||||
class X(object):
|
||||
def __get__(self,k,t=None):
|
||||
1/0
|
||||
"""
|
||||
self.assertRaises(CheckStrException,safe_exec,code)
|
||||
def test_check_str_unicode(self):
|
||||
self.assertRaises(CheckStrException,safe_exec,"u'__'")
|
||||
|
||||
def test_run_builtin_open(self):
|
||||
self.assertRaises(RunBuiltinException,safe_exec,"open('test.txt','w')")
|
||||
def test_run_builtin_getattr(self):
|
||||
self.assertRaises(RunBuiltinException,safe_exec,"getattr(None,'x')")
|
||||
def test_run_builtin_abs(self):
|
||||
safe_exec("abs(-1)")
|
||||
def test_run_builtin_open_fnc(self):
|
||||
def test():
|
||||
f = open('test.txt','w')
|
||||
self.assertRaises(RunBuiltinException,safe_exec,"test()",{'test':test})
|
||||
def test_run_builtin_open_context(self):
|
||||
#this demonstrates how python jumps into some mystical
|
||||
#restricted mode at this point .. causing this to throw
|
||||
#an IOError. a bit strange, if you ask me.
|
||||
self.assertRaises(IOError,safe_exec,"test('test.txt','w')",{'test':open})
|
||||
def test_run_builtin_type_context(self):
|
||||
#however, even though this is also a very dangerous function
|
||||
#python's mystical restricted mode doesn't throw anything.
|
||||
safe_exec("test(1)",{'test':type})
|
||||
def test_run_builtin_dir(self):
|
||||
self.assertRaises(RunBuiltinException,safe_exec,"dir(None)")
|
||||
|
||||
def test_run_exeception_div(self):
|
||||
self.assertRaises(ZeroDivisionError,safe_exec,"1/0")
|
||||
def test_run_exeception_i(self):
|
||||
self.assertRaises(ValueError,safe_exec,"(-1)**0.5")
|
||||
|
||||
def test_misc_callback(self):
|
||||
self.value = None
|
||||
def test(): self.value = 1
|
||||
safe_exec("test()", {'test':test})
|
||||
self.assertEqual(self.value, 1)
|
||||
def test_misc_safe(self):
|
||||
self.value = None
|
||||
def test(v): self.value = v
|
||||
code = """
|
||||
class Test:
|
||||
def __init__(self,value):
|
||||
self.x = value
|
||||
self.y = 4
|
||||
def run(self):
|
||||
for n in xrange(0,34):
|
||||
self.x += n
|
||||
self.y *= n
|
||||
return self.x+self.y
|
||||
b = Test(value)
|
||||
r = b.run()
|
||||
test(r)
|
||||
"""
|
||||
safe_exec(code,{'value':3,'test':test})
|
||||
self.assertEqual(self.value, 564)
|
||||
|
||||
def test_misc_context_reset(self):
|
||||
#test that local contact is reset
|
||||
safe_exec("abs = None")
|
||||
safe_exec("abs(-1)")
|
||||
safe_run("abs = None")
|
||||
safe_run("abs(-1)")
|
||||
|
||||
def test_misc_syntax_error(self):
|
||||
self.assertRaises(SyntaxError,safe_exec,"/")
|
||||
|
||||
def test_misc_context_switch(self):
|
||||
self.value = None
|
||||
def test(v): self.value = v
|
||||
safe_exec("""
|
||||
def test2():
|
||||
open('test.txt','w')
|
||||
test(test2)
|
||||
""",{'test':test})
|
||||
self.assertRaises(RunBuiltinException,safe_exec,"test()",{'test':self.value})
|
||||
|
||||
def test_misc_context_junk(self):
|
||||
#test that stuff isn't being added into *my* context
|
||||
#except what i want in it..
|
||||
c = {}
|
||||
safe_exec("b=1",c)
|
||||
self.assertEqual(c['b'],1)
|
||||
|
||||
def test_misc_context_later(self):
|
||||
#honestly, i'd rec that people don't do this, but
|
||||
#at least we've got it covered ...
|
||||
c = {}
|
||||
safe_exec("def test(): open('test.txt','w')",c)
|
||||
self.assertRaises(RunBuiltinException,c['test'])
|
||||
|
||||
#def test_misc_test(self):
|
||||
#code = "".join(open('test.py').readlines())
|
||||
#safe_check(code)
|
||||
|
||||
def test_misc_builtin_globals_write(self):
|
||||
#check that a user can't modify the special _builtin_globals stuff
|
||||
safe_exec("abs = None")
|
||||
self.assertNotEqual(_builtin_globals['abs'],None)
|
||||
|
||||
#def test_misc_builtin_globals_used(self):
|
||||
##check that the same builtin globals are always used
|
||||
#c1,c2 = {},{}
|
||||
#safe_exec("def test(): pass",c1)
|
||||
#safe_exec("def test(): pass",c2)
|
||||
#self.assertEqual(c1['test'].func_globals,c2['test'].func_globals)
|
||||
#self.assertEqual(c1['test'].func_globals,_builtin_globals)
|
||||
|
||||
def test_misc_builtin_globals_used(self):
|
||||
#check that the same builtin globals are always used
|
||||
c = {}
|
||||
safe_exec("def test1(): pass",c)
|
||||
safe_exec("def test2(): pass",c)
|
||||
self.assertEqual(c['test1'].func_globals,c['test2'].func_globals)
|
||||
self.assertEqual(c['test1'].func_globals['__builtins__'],_builtin_globals)
|
||||
self.assertEqual(c['__builtins__'],_builtin_globals)
|
||||
|
||||
def test_misc_type_escape(self):
|
||||
#tests that 'type' isn't allowed anymore
|
||||
#with type defined, you could create magical classes like this:
|
||||
code = """
|
||||
def delmethod(self): 1/0
|
||||
foo=type('Foo', (object,), {'_' + '_del_' + '_':delmethod})()
|
||||
foo.error
|
||||
"""
|
||||
try:
|
||||
self.assertRaises(RunBuiltinException,safe_exec,code)
|
||||
finally:
|
||||
pass
|
||||
|
||||
def test_misc_recursive_fnc(self):
|
||||
code = "def test():test()\ntest()"
|
||||
self.assertRaises(RuntimeError,safe_exec,code)
|
||||
|
||||
|
||||
unittest.main()
|
||||
|
||||
#safe_exec('print locals()')
|
||||
|
|
|
@ -47,7 +47,6 @@
|
|||
|
||||
static python_ai* running_instance;
|
||||
bool python_ai::init_ = false;
|
||||
PyObject* python_ai::python_error_ = NULL;
|
||||
|
||||
#define return_none do {Py_INCREF(Py_None); return Py_None;} while(false)
|
||||
|
||||
|
@ -1667,28 +1666,50 @@ static PyMethodDef wesnoth_python_methods[] = {
|
|||
PyObject* pyob = reinterpret_cast<PyObject *>(type); \
|
||||
PyModule_AddObject(module, const_cast<char *>(n), pyob); }
|
||||
|
||||
void python_ai::initialize_python()
|
||||
{
|
||||
if (init_) return;
|
||||
init_ = true;
|
||||
Py_Initialize( );
|
||||
PyObject* module = Py_InitModule3("wesnoth", wesnoth_python_methods,
|
||||
"This is the wesnoth AI module. "
|
||||
"The python script will be executed once for each turn of the side with the "
|
||||
"python AI using the script.");
|
||||
Py_Register(wesnoth_unit_type, "unit");
|
||||
Py_Register(wesnoth_location_type, "location");
|
||||
Py_Register(wesnoth_gamemap_type, "gamemap");
|
||||
Py_Register(wesnoth_unittype_type, "unittype");
|
||||
Py_Register(wesnoth_team_type, "team");
|
||||
Py_Register(wesnoth_attacktype_type, "attacktype");
|
||||
Py_Register(wesnoth_gamestatus_type, "gamestatus");
|
||||
}
|
||||
|
||||
void python_ai::invoke(std::string name)
|
||||
{
|
||||
initialize_python();
|
||||
PyErr_Clear();
|
||||
PyObject* globals = PyDict_New();
|
||||
PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins());
|
||||
std::string python_code;
|
||||
python_code +=
|
||||
"import sys\n"
|
||||
"backup = sys.path[:]\n"
|
||||
"sys.path.append(\"" + game_config::path + "/data/ais\")\n"
|
||||
"try:\n"
|
||||
" import " + name + "\n"
|
||||
"finally:\n"
|
||||
" sys.path = backup\n";
|
||||
PyObject *ret = PyRun_String(python_code.c_str(), Py_file_input, globals,
|
||||
globals);
|
||||
Py_XDECREF(ret);
|
||||
Py_DECREF(globals);
|
||||
}
|
||||
|
||||
python_ai::python_ai(ai_interface::info& info) : ai_interface(info), exception(QUIT)
|
||||
{
|
||||
LOG_AI << "Running Python instance.\n";
|
||||
running_instance = this;
|
||||
if ( !init_ )
|
||||
{
|
||||
Py_Initialize( );
|
||||
PyObject* module = Py_InitModule3("wesnoth", wesnoth_python_methods,
|
||||
"A script must call 'import wesnoth' to import all Wesnoth-related objects. "
|
||||
"The python script will be executed once for each turn of the side with the "
|
||||
"python AI using the script.");
|
||||
Py_Register(wesnoth_unit_type, "unit");
|
||||
Py_Register(wesnoth_location_type, "location");
|
||||
Py_Register(wesnoth_gamemap_type, "gamemap");
|
||||
Py_Register(wesnoth_unittype_type, "unittype");
|
||||
Py_Register(wesnoth_team_type, "team");
|
||||
Py_Register(wesnoth_attacktype_type, "attacktype");
|
||||
Py_Register(wesnoth_gamestatus_type, "gamestatus");
|
||||
python_error_ = PyErr_NewException("wesnoth.error",NULL,NULL);
|
||||
PyDict_SetItemString(PyModule_GetDict(module),"error",python_error_);
|
||||
init_ = true;
|
||||
}
|
||||
initialize_python();
|
||||
calculate_possible_moves(possible_moves_,src_dst_,dst_src_,false);
|
||||
calculate_possible_moves(enemy_possible_moves_,enemy_src_dst_,enemy_dst_src_,true);
|
||||
}
|
||||
|
@ -1721,21 +1742,20 @@ void python_ai::play_turn()
|
|||
|
||||
LOG_AI << "Executing Python script \"" << script << "\".\n";
|
||||
// Run the python script. We actually execute a short inline python
|
||||
// script, which sets up the module search path to the current binary
|
||||
// pathes, runs the script with execfile, and then resets the path.
|
||||
// script, which sets up the module search path to the data path,
|
||||
// runs the script, and then resets the path.
|
||||
std::string python_code;
|
||||
python_code += "import sys\n"
|
||||
"backup = sys.path[:]; sys.path.extend([";
|
||||
const std::vector<std::string>& paths = get_binary_paths("data");
|
||||
std::vector<std::string>::const_iterator i;
|
||||
for (i = paths.begin(); i != paths.end(); ++i) {
|
||||
python_code += "'" + *i + "ais', ";
|
||||
}
|
||||
python_code += "])\n"
|
||||
"import wesnoth, safe, heapq, random\n"
|
||||
python_code +=
|
||||
"import sys\n"
|
||||
"backup = sys.path[:]\n"
|
||||
"sys.path.append(\"" + game_config::path + "/data/ais\")\n"
|
||||
"try:\n"
|
||||
" code = file(\"" + script + "\").read()\n"
|
||||
" safe.safe_exec(code, {\"wesnoth\" : wesnoth, \"heapq\" : heapq, \"random\" : random})\n"
|
||||
" import wesnoth, parse, safe, heapq, random\n"
|
||||
" code = parse.parse(\"" + script + "\")\n"
|
||||
" safe.safe_exec(code, {\n"
|
||||
" \"wesnoth\" : wesnoth,\n"
|
||||
" \"heapq\" : heapq,\n"
|
||||
" \"random\" : random})\n"
|
||||
"finally:\n"
|
||||
" sys.path = backup\n";
|
||||
PyObject *ret = PyRun_String(python_code.c_str(), Py_file_input,
|
||||
|
|
|
@ -56,14 +56,14 @@ public:
|
|||
static PyObject* wrapper_unit_find_path( wesnoth_unit* unit, PyObject* args );
|
||||
static PyObject* wrapper_unit_attack_statistics(wesnoth_unit* unit, PyObject* args);
|
||||
|
||||
static void set_error(const char *fmt, ...);
|
||||
|
||||
static bool is_unit_valid(const unit* unit);
|
||||
std::vector<team>& get_teams() { return get_info().teams; }
|
||||
static std::vector<std::string> get_available_scripts();
|
||||
protected:
|
||||
static bool init_;
|
||||
static PyObject* python_error_;
|
||||
|
||||
static void initialize_python();
|
||||
static void invoke(std::string name);
|
||||
private:
|
||||
static bool init_;
|
||||
end_level_exception exception;
|
||||
ai_interface::move_map src_dst_;
|
||||
ai_interface::move_map dst_src_;
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
#include "serialization/binary_wml.hpp"
|
||||
#include "serialization/parser.hpp"
|
||||
#include "serialization/preprocessor.hpp"
|
||||
#include "ai_python.hpp"
|
||||
|
||||
#include "wesconfig.h"
|
||||
|
||||
|
@ -1655,6 +1656,7 @@ int play_game(int argc, char** argv)
|
|||
<< " --max-fps the maximum fps the game tries to run at the value\n"
|
||||
<< " should be between the 1 and 1000, the default is 50.\n"
|
||||
<< " --path prints the name of the game data directory and exits.\n"
|
||||
<< " --python-api prints the runtime documentation for the python API.\n"
|
||||
<< " -r, --resolution XxY sets the screen resolution. Example: -r 800x600\n"
|
||||
<< " -t, --test runs the game in a small test scenario.\n"
|
||||
<< " -v, --version prints the game's version number and exits.\n"
|
||||
|
@ -1685,6 +1687,9 @@ int play_game(int argc, char** argv)
|
|||
std::cout << game_config::path
|
||||
<< "\n";
|
||||
return 0;
|
||||
} else if(val == "--python-api") {
|
||||
python_ai::invoke("documentation.py");
|
||||
return 0;
|
||||
} else if (val.substr(0, 6) == "--log-") {
|
||||
size_t p = val.find('=');
|
||||
if (p == std::string::npos) {
|
||||
|
|
Loading…
Add table
Reference in a new issue