mirror of
https://github.com/worldforge/cyphesis
synced 2026-08-13 12:26:04 -04:00
Restructure rule classes.
Classes under "rulesets" have been moved to "rules", and split up into futher subdirectories matching their library. As a result we can now better separate the python bindings, so that things that belongs to the simulation are separeted from things that belongs to the ai.
This commit is contained in:
parent
800d6f3d2c
commit
e85f0831b2
463 changed files with 1794 additions and 1515 deletions
345
rules/python/Python_API.cpp
Normal file
345
rules/python/Python_API.cpp
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
// Cyphesis Online RPG Server and AI Engine
|
||||
// Copyright (C) 2000-2005 Alistair Riddoch
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation; either version 2 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software Foundation,
|
||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
|
||||
#include "Python.h"
|
||||
|
||||
#include "Python_API.h"
|
||||
#include "Python_Script_Utils.h"
|
||||
|
||||
#include "rules/ai/BaseMind.h"
|
||||
|
||||
#include "common/globals.h"
|
||||
#include "common/const.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/AssetsManager.h"
|
||||
|
||||
#include "rules/entityfilter/python/CyPy_EntityFilter.h"
|
||||
#include "rules/python/CyPy_Common.h"
|
||||
#include "rules/python/CyPy_Atlas.h"
|
||||
#include "rules/python/CyPy_Physics.h"
|
||||
#include "rules/simulation/python/CyPy_Server.h"
|
||||
#include "rules/python/WrapperBase.h"
|
||||
|
||||
#include <Atlas/Objects/Operation.h>
|
||||
#include <Atlas/Objects/Anonymous.h>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <basedir.h>
|
||||
|
||||
using Atlas::Message::Element;
|
||||
using Atlas::Objects::Root;
|
||||
using Atlas::Objects::Operation::RootOperation;
|
||||
using Atlas::Objects::Entity::Anonymous;
|
||||
|
||||
static const bool debug_flag = false;
|
||||
|
||||
/// \defgroup PythonWrappers Python Wrapper Types
|
||||
///
|
||||
/// Structure types based on the PyObject header used to wrap C++ objects
|
||||
/// in Python.
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Logger replaces sys.stdout and sys.stderr so the nothing goes to output
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
/// \brief Python type to handle output from python scripts
|
||||
///
|
||||
/// In instance of this type is used to replace sys.stdout and sys.stderr
|
||||
/// in the Python interpreter so that all script output goes to the cyphesis
|
||||
/// log subsystem
|
||||
struct LogWriter : public WrapperBase<LogLevel, LogWriter>
|
||||
{
|
||||
std::string m_message;
|
||||
|
||||
LogWriter(Py::PythonClassInstance* self, Py::Tuple& args, Py::Dict& kwds)
|
||||
: WrapperBase(self, args, kwds)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
LogWriter(Py::PythonClassInstance* self, LogLevel value)
|
||||
: WrapperBase(self, value)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void python_log(LogLevel lvl, const std::string& msg)
|
||||
{
|
||||
|
||||
m_message += msg;
|
||||
std::string::size_type n = 0;
|
||||
std::string::size_type p;
|
||||
for (p = m_message.find_first_of('\n');
|
||||
p != std::string::npos;
|
||||
p = m_message.find_first_of('\n', n)) {
|
||||
log(lvl, m_message.substr(n, p - n));
|
||||
n = p + 1;
|
||||
}
|
||||
if (m_message.size() > n) {
|
||||
m_message = m_message.substr(n, m_message.size() - n);
|
||||
} else {
|
||||
m_message.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Py::Object write(const Py::Tuple& args)
|
||||
{
|
||||
if (args.length() > 0) {
|
||||
python_log(m_value, verifyString(args.front()));
|
||||
}
|
||||
|
||||
return Py::None();
|
||||
}
|
||||
|
||||
PYCXX_VARARGS_METHOD_DECL(LogWriter, write)
|
||||
|
||||
Py::Object flush()
|
||||
{
|
||||
return Py::None();
|
||||
}
|
||||
|
||||
PYCXX_NOARGS_METHOD_DECL(LogWriter, flush)
|
||||
|
||||
static void init_type()
|
||||
{
|
||||
behaviors().name("LogWriter");
|
||||
behaviors().doc("");
|
||||
|
||||
PYCXX_ADD_NOARGS_METHOD(flush, flush, "");
|
||||
PYCXX_ADD_VARARGS_METHOD(write, write, "");
|
||||
|
||||
behaviors().readyType();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/// \brief Find a class in a Python module
|
||||
///
|
||||
/// @param module the imported Python module
|
||||
/// @param package the name of the module for error reporting
|
||||
/// @param type the name of the class or type
|
||||
/// @return new reference
|
||||
Py::Object Get_PyClass(const Py::Module& module,
|
||||
const std::string& package,
|
||||
const std::string& type)
|
||||
{
|
||||
auto py_class = module.getAttr(type);
|
||||
if (py_class.isNull()) {
|
||||
log(ERROR, String::compose("Could not find python class \"%1.%2\"",
|
||||
package, type));
|
||||
PyErr_Print();
|
||||
return Py::Null();
|
||||
}
|
||||
if (!py_class.isCallable()) {
|
||||
log(ERROR, String::compose("Could not instance python class \"%1.%2\"",
|
||||
package, type));
|
||||
return Py::Null();
|
||||
}
|
||||
return py_class;
|
||||
}
|
||||
|
||||
/// \brief Import a Python module
|
||||
///
|
||||
/// @param package the name of the module
|
||||
/// @return new reference
|
||||
Py::Module Get_PyModule(const std::string& package)
|
||||
{
|
||||
Py::String package_name(package);
|
||||
PyObject* module = PyImport_Import(package_name.ptr());
|
||||
if (module == nullptr) {
|
||||
log(ERROR, String::compose("Missing python module \"%1\"", package));
|
||||
PyErr_Print();
|
||||
return Py::Module(nullptr);
|
||||
}
|
||||
return Py::Module(module);
|
||||
}
|
||||
|
||||
Py::Object Create_PyScript(const Py::Object& wrapper, const Py::Callable& py_class)
|
||||
{
|
||||
try {
|
||||
return py_class.apply(Py::TupleN(wrapper));
|
||||
} catch (...) {
|
||||
if (PyErr_Occurred() == nullptr) {
|
||||
log(ERROR, "Could not create python instance");
|
||||
} else {
|
||||
log(ERROR, "Reporting python error");
|
||||
PyErr_Print();
|
||||
}
|
||||
return Py::None();
|
||||
}
|
||||
}
|
||||
|
||||
sigc::signal<void> python_reload_scripts;
|
||||
std::vector<std::string> python_directories;
|
||||
|
||||
std::map<boost::filesystem::path, std::string> changedPaths;
|
||||
|
||||
void reloadChangedPaths()
|
||||
{
|
||||
if (!changedPaths.empty()) {
|
||||
|
||||
for (auto& entry : changedPaths) {
|
||||
auto& package = entry.second;
|
||||
auto& path = entry.first;
|
||||
auto module = Get_PyModule(package);
|
||||
if (!module.isNull()) {
|
||||
log(INFO, String::compose("Reloading module \"%1\" from file %2.", package, path));
|
||||
auto result = PyImport_ReloadModule(module.ptr());
|
||||
if (result != module.ptr()) {
|
||||
log(WARNING, String::compose("New pointer returned when reloading module \"%1\".", package));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
python_reload_scripts();
|
||||
changedPaths.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void observe_python_directories(boost::asio::io_service& io_service, AssetsManager& assetsManager)
|
||||
{
|
||||
|
||||
for (auto& directory : python_directories) {
|
||||
AssetsManager::instance().observeDirectory(directory, [=, &io_service](const boost::filesystem::path& path) {
|
||||
//Trim the ".py" extension
|
||||
if (boost::ends_with(path.string(), ".py")) {
|
||||
auto relative = path.string().substr(directory.length() + 1);
|
||||
relative = relative.substr(0, relative.size() - 3);
|
||||
static char separator[] = {boost::filesystem::path::preferred_separator, 0};
|
||||
auto package = boost::replace_all_copy(relative, separator, ".");
|
||||
changedPaths[path] = package;
|
||||
|
||||
auto timer = std::make_shared<boost::asio::steady_timer>(io_service);
|
||||
timer->expires_from_now(std::chrono::milliseconds(20));
|
||||
timer->async_wait([&, timer](const boost::system::error_code& ec) {
|
||||
if (!ec) {
|
||||
reloadChangedPaths();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void init_python_api(std::vector<std::function<std::string()>> initFunctions, const std::string& ruleset, bool log_stdout)
|
||||
{
|
||||
|
||||
std::vector<std::string> modules;
|
||||
for (auto& function : initFunctions) {
|
||||
modules.emplace_back(function());
|
||||
}
|
||||
|
||||
Py_InitializeEx(0);
|
||||
|
||||
//Make sure that all modules are imported, since this is needed to initialize all types.
|
||||
//Otherwise we risk that we try to invoke the types from the C++ code before they have
|
||||
//been populated.
|
||||
for (auto& moduleName : modules) {
|
||||
PyImport_ImportModule(moduleName.c_str());
|
||||
}
|
||||
|
||||
PyImport_ImportModule("sys");
|
||||
|
||||
Py::Module sys_module(PyImport_Import(Py::String("sys").ptr()));
|
||||
|
||||
if (sys_module.isNull()) {
|
||||
log(CRITICAL, "Python could not import sys module");
|
||||
return;
|
||||
}
|
||||
|
||||
if (log_stdout) {
|
||||
LogWriter::init_type();
|
||||
sys_module.setAttr("stdout", LogWriter::wrap(SCRIPT));
|
||||
sys_module.setAttr("stderr", LogWriter::wrap(SCRIPT_ERROR));
|
||||
}
|
||||
|
||||
auto sys_path = sys_module.getAttr("path");
|
||||
if (!sys_path.isNull()) {
|
||||
if (sys_path.isList()) {
|
||||
Py::List paths(sys_path);
|
||||
|
||||
// Add the path to the non-ruleset specific code.
|
||||
python_directories.push_back(share_directory + "/cyphesis/scripts");
|
||||
python_directories.push_back(share_directory + "/cyphesis/rulesets/basic/scripts");
|
||||
// Add the path to the ruleset specific code.
|
||||
python_directories.push_back(share_directory + "/cyphesis/rulesets/" + ruleset + "/scripts");
|
||||
|
||||
for (auto& path : python_directories) {
|
||||
paths.append(Py::String(path));
|
||||
}
|
||||
|
||||
} else {
|
||||
log(CRITICAL, "Python sys.path is not a list");
|
||||
}
|
||||
} else {
|
||||
log(CRITICAL, "Python could not import sys.path");
|
||||
}
|
||||
|
||||
debug(std::cout << Py_GetPath() << std::endl << std::flush;);
|
||||
}
|
||||
|
||||
void shutdown_python_api()
|
||||
{
|
||||
|
||||
Py_Finalize();
|
||||
}
|
||||
|
||||
void run_user_scripts(const std::string& prefix)
|
||||
{
|
||||
xdgHandle baseDirHandle{};
|
||||
boost::filesystem::path path;
|
||||
if (xdgInitHandle(&baseDirHandle)) {
|
||||
auto dataHome = xdgDataHome(&baseDirHandle);
|
||||
if (dataHome) {
|
||||
path = dataHome;
|
||||
}
|
||||
xdgWipeHandle(&baseDirHandle);
|
||||
}
|
||||
|
||||
if (!path.empty()) {
|
||||
path /= "cyphesis";
|
||||
path /= (prefix + ".d");
|
||||
log(INFO, String::compose("Looking for extra python scripts in %1.", path));
|
||||
if (boost::filesystem::is_directory(path)) {
|
||||
boost::filesystem::recursive_directory_iterator dir(path), end{};
|
||||
|
||||
while (dir != end) {
|
||||
if (boost::filesystem::is_regular_file(dir->status()) && dir->path().extension() == ".py") {
|
||||
auto fileHandle = fopen(dir->path().c_str(), "r");
|
||||
if (fileHandle) {
|
||||
log(INFO, String::compose("Running script '%1'.", dir->path().c_str()));
|
||||
PyRun_SimpleFileEx(fileHandle, dir->path().c_str(), true);
|
||||
if (PyErr_Occurred()) {
|
||||
PyErr_Print();
|
||||
}
|
||||
}
|
||||
}
|
||||
++dir;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue