From fafd23b6734e49cfa0c1e519340f62b51efffe37 Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Tue, 13 Oct 2020 13:41:50 -0700 Subject: [PATCH] Exposing more C++ sources --- .efrocachemap | 40 +- Makefile | 15 +- src/ballistica/ballistica.h | 2 +- src/ballistica/python/python.cc | 2714 +++++++++++++++++++++ src/generated_src/Makefile | 14 +- src/generated_src/ballistica/binding.py | 126 + src/generated_src/ballistica/bootstrap.py | 147 ++ tools/batools/build.py | 60 +- tools/batools/codegen.py | 109 + tools/batools/pcommand.py | 72 +- tools/pcommand | 2 +- 11 files changed, 3218 insertions(+), 83 deletions(-) create mode 100644 src/ballistica/python/python.cc create mode 100644 src/generated_src/ballistica/binding.py create mode 100644 src/generated_src/ballistica/bootstrap.py create mode 100644 tools/batools/codegen.py diff --git a/.efrocachemap b/.efrocachemap index f71ffac6..b7e4a55b 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -3932,24 +3932,24 @@ "assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/b5/85/f8b6d0558ddb87267f34254b1450", "assets/build/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/1c/e1/4a1a2eddda2f4aebd5f8b64ab08e", "assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f", - "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/31/9f/f2aa794e452ae380c363dab7744e", - "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/cf/0d/24472a7bce6c6de12493d506ce64", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3b/64/e544f697481ca43b38b3568b8a1e", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/70/f0/5df34ce15aa534d8c42cd7235984", - "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/62/2c/4ddcde6c2483ab591646c2f49257", - "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/94/e8/f272b412ca176fde161346bf1363", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d3/b2/63d02079a0fb191abf8366f64814", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/44/67/c3d7dcb5e16ad5451ba7daf2f9bd", - "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/e0/2f/1665f879dbc7d9241b84990cffaf", - "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/fe/d6/77e6b6d65a3484e4c264be8dbfab", - "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/2a/01/3d42c36b2afd5a24d37edc2f6883", - "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/f5/89/558acefe3774686fc10f967e737b", - "build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ba/78/292390a40c1e4b9804d7572d7a04", - "build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4c/a9/568023651355fdd0ce7a865c2872", - "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/69/cc/a5555af8babdef9948380ede8431", - "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/42/be/79eec8bc7b2cc914cc6cb8ed0769", - "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/66/66/063065ddf0a99cb992e936373f53", - "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/30/6f/27022976202b3886ad4a395f1f06", - "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9e/55/2caa189c8674ef770c2ab3a7cd7d", - "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b2/14/f0e7a95e18c4484cf4a7894a3fbd" + "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/c3/64/d09fa017e267f7239b53ea15e60b", + "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/f2/0a/8923a5a2c428fe3c355eb3e9e024", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/8e/da/41e88ab335c7edfa162c4a0de4be", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a0/76/805f87ceafce55784d025a3e9977", + "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/90/38/615955c8501a8ee201166bf44e36", + "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/1d/2a/01fe857df7ef297a7f7ea3886d26", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/0a/f8/59aeb014837f2419d76ee66cb01d", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a2/31/0a4cc16d108f66c9c8f323799103", + "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/1d/4b/d1cdb7ad12c6a9a5ce6fef4e54af", + "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/69/7b/50ab311eea3401bdecedb322022e", + "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/b5/95/dee668a0080fd9d529bc4d7a6ce8", + "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/53/b2/0e959273e553034e2d7ff135c8b0", + "build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c4/c0/91eab0d803b753a0a2e1ba46c22f", + "build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cb/72/fcba4049353a8cdbfec9d7065a3b", + "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e8/25/986296217583b22aaa1652e67086", + "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/38/1e/6650391748d1410acf7c26f0eb72", + "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d0/bf/73561ee1722ddce9a98d9a8c9b4f", + "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fe/1f/13289c985eaefb448c8c9c8e2cfc", + "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a5/4b/c79bf59874bf83cba38485114851", + "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e7/3d/d1e07fd3d91655d11787e7b60958" } \ No newline at end of file diff --git a/Makefile b/Makefile index a064852f..ecccd6fd 100644 --- a/Makefile +++ b/Makefile @@ -817,14 +817,15 @@ CM_BT_LC = $(shell echo $(CMAKE_BUILD_TYPE) | tr A-Z a-z) ballisticacore-cmake/.clang-format: .clang-format @cd ballisticacore-cmake && ln -sf ../.clang-format . -# Simple target for CI to build a binary but no assets/etc. -_cmake-simple-ci-server-build: - rm -rf build/cmake_scsb - mkdir -p build/cmake_scsb - tools/pcommand update_cmake_prefab_lib server debug build/cmake_scsb - cd build/cmake_scsb && \ +# Simple target for CI to build a binary but not download/assemble assets/etc. +_cmake-simple-ci-server-build: code + rm -rf build/cmake_simple_ci_server_build + mkdir -p build/cmake_simple_ci_server_build + tools/pcommand update_cmake_prefab_lib \ + server debug build/cmake_simple_ci_server_build + cd build/cmake_simple_ci_server_build && \ cmake -DCMAKE_BUILD_TYPE=Debug -DHEADLESS=true ${PWD}/ballisticacore-cmake - cd build/cmake_scsb && ${MAKE} -j${CPUS} + cd build/cmake_simple_ci_server_build && ${MAKE} -j${CPUS} # Tell make which of these targets don't represent files. .PHONY: _cmake-simple-ci-server-build diff --git a/src/ballistica/ballistica.h b/src/ballistica/ballistica.h index f4c05fe0..e283af2a 100644 --- a/src/ballistica/ballistica.h +++ b/src/ballistica/ballistica.h @@ -154,8 +154,8 @@ auto IsBootstrapped() -> bool; /// Internal bits. auto CreateAppInternal() -> AppInternal*; -auto AppInternalPythonInit() -> PyObject*; auto AppInternalPythonInit2() -> void; +auto AppInternalInitModule() -> void; auto AppInternalHasBlessingHash() -> bool; auto AppInternalPutLog(bool fatal) -> bool; auto AppInternalAwardAdTickets() -> void; diff --git a/src/ballistica/python/python.cc b/src/ballistica/python/python.cc new file mode 100644 index 00000000..9b69d7f2 --- /dev/null +++ b/src/ballistica/python/python.cc @@ -0,0 +1,2714 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/python/python.h" + +#include +#include +#include +#include + +#include "ballistica/app/app_globals.h" +#include "ballistica/audio/audio.h" +#include "ballistica/dynamics/material/material.h" +#include "ballistica/game/account.h" +#include "ballistica/game/friend_score_set.h" +#include "ballistica/game/game_stream.h" +#include "ballistica/game/host_activity.h" +#include "ballistica/game/player.h" +#include "ballistica/game/score_to_beat.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/input/device/joystick.h" +#include "ballistica/input/device/keyboard_input.h" +#include "ballistica/media/component/collide_model.h" +#include "ballistica/media/component/model.h" +#include "ballistica/media/component/texture.h" +#include "ballistica/python/class/python_class_activity_data.h" +#include "ballistica/python/class/python_class_collide_model.h" +#include "ballistica/python/class/python_class_context.h" +#include "ballistica/python/class/python_class_data.h" +#include "ballistica/python/class/python_class_input_device.h" +#include "ballistica/python/class/python_class_material.h" +#include "ballistica/python/class/python_class_model.h" +#include "ballistica/python/class/python_class_node.h" +#include "ballistica/python/class/python_class_session_data.h" +#include "ballistica/python/class/python_class_session_player.h" +#include "ballistica/python/class/python_class_sound.h" +#include "ballistica/python/class/python_class_texture.h" +#include "ballistica/python/class/python_class_vec3.h" +#include "ballistica/python/class/python_class_widget.h" +#include "ballistica/python/python_command.h" +#include "ballistica/python/python_context_call_runnable.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/scene.h" +#include "ballistica/ui/ui.h" +#include "ballistica/ui/widget/text_widget.h" + +namespace ballistica { + +// Ignore signed bitwise stuff; python macros do it quite a bit. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +#pragma ide diagnostic ignored "RedundantCast" + +// Used by our built in exception type. +void Python::SetPythonException(PyExcType exctype, const char* description) { + PyObject* pytype{}; + switch (exctype) { + case PyExcType::kRuntime: + pytype = PyExc_RuntimeError; + break; + case PyExcType::kAttribute: + pytype = PyExc_AttributeError; + break; + case PyExcType::kIndex: + pytype = PyExc_IndexError; + break; + case PyExcType::kValue: + pytype = PyExc_ValueError; + break; + case PyExcType::kType: + pytype = PyExc_TypeError; + break; + case PyExcType::kContext: + pytype = g_python->obj(Python::ObjID::kContextError).get(); + break; + case PyExcType::kNotFound: + pytype = g_python->obj(Python::ObjID::kNotFoundError).get(); + break; + case PyExcType::kNodeNotFound: + pytype = g_python->obj(Python::ObjID::kNodeNotFoundError).get(); + break; + case PyExcType::kSessionPlayerNotFound: + pytype = g_python->obj(Python::ObjID::kSessionPlayerNotFoundError).get(); + break; + case PyExcType::kInputDeviceNotFound: + pytype = g_python->obj(Python::ObjID::kInputDeviceNotFoundError).get(); + break; + case PyExcType::kDelegateNotFound: + pytype = g_python->obj(Python::ObjID::kDelegateNotFoundError).get(); + break; + case PyExcType::kWidgetNotFound: + pytype = g_python->obj(Python::ObjID::kWidgetNotFoundError).get(); + break; + case PyExcType::kActivityNotFound: + pytype = g_python->obj(Python::ObjID::kActivityNotFoundError).get(); + break; + case PyExcType::kSessionNotFound: + pytype = g_python->obj(Python::ObjID::kSessionNotFoundError).get(); + break; + } + assert(pytype != nullptr && PyType_Check(pytype)); + PyErr_SetString(pytype, description); +} + +const char* Python::ScopedCallLabel::current_label_ = nullptr; + +auto Python::HaveGIL() -> bool { return static_cast(PyGILState_Check()); } + +void Python::PrintStackTrace() { + ScopedInterpreterLock lock; + auto objid{Python::ObjID::kPrintTraceCall}; + if (g_python->objexists(objid)) { + g_python->obj(objid).Call(); + } else { + Log("Warning: Python::PrintStackTrace() called before bootstrap complete; " + "not printing."); + } +} + +// Return whether GetPyString() will succeed for an object. +auto Python::IsPyString(PyObject* o) -> bool { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + return (PyUnicode_Check(o) + || PyObject_IsInstance( + o, g_python->obj(Python::ObjID::kLStrClass).get())); +} + +auto Python::GetPyString(PyObject* o) -> std::string { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + PyExcType exctype{PyExcType::kType}; + if (PyUnicode_Check(o)) { + return PyUnicode_AsUTF8(o); + } else { + // Check if its a Lstr. If so; we pull its json string representation. + int result = + PyObject_IsInstance(o, g_python->obj(Python::ObjID::kLStrClass).get()); + if (result == -1) { + PyErr_Clear(); + result = 0; + } + if (result == 1) { + // At this point its not a simple type error if something goes wonky. + // Perhaps we should try to preserve any error type raised by + // the _get_json() call... + exctype = PyExcType::kRuntime; + PythonRef get_json_call(PyObject_GetAttrString(o, "_get_json"), + PythonRef::kSteal); + if (get_json_call.CallableCheck()) { + PythonRef json = get_json_call.Call(); + if (PyUnicode_Check(json.get())) { + return PyUnicode_AsUTF8(json.get()); + } + } + } + } + + // Failed, we have. + // Clear any Python error that got us here; we're in C++ Exception land now. + PyErr_Clear(); + throw Exception( + "Can't get string from value: " + Python::ObjToString(o) + ".", exctype); +} + +template +auto GetPyIntT(PyObject* o) -> T { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (PyLong_Check(o)) { + return static_cast_check_fit(PyLong_AS_LONG(o)); + } + if (PyNumber_Check(o)) { + PyObject* f = PyNumber_Long(o); + if (f) { + auto val = static_cast_check_fit(PyLong_AS_LONG(f)); + Py_DECREF(f); + return val; + } + } + + // Failed, we have. + // Clear any Python error that got us here; we're in C++ Exception land now. + PyErr_Clear(); + + // Assuming any failure here was type related. + throw Exception("Can't get int from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto Python::GetPyInt64(PyObject* o) -> int64_t { + return GetPyIntT(o); +} + +auto Python::GetPyInt(PyObject* o) -> int { return GetPyIntT(o); } + +auto Python::GetPyBool(PyObject* o) -> bool { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (o == Py_True) { + return true; + } + if (o == Py_False) { + return false; + } + if (PyLong_Check(o)) { + return (PyLong_AS_LONG(o) != 0); + } + if (PyNumber_Check(o)) { + if (PyObject* o2 = PyNumber_Long(o)) { + auto val = PyLong_AS_LONG(o2); + Py_DECREF(o2); + return (val != 0); + } + } + + // Failed, we have. + // Clear any Python error that got us here; we're in C++ Exception land now. + PyErr_Clear(); + + // Assuming any failure here was type related. + throw Exception("Can't get bool from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto Python::IsPySession(PyObject* o) -> bool { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + int result = + PyObject_IsInstance(o, g_python->obj(ObjID::kSessionClass).get()); + if (result == -1) { + PyErr_Clear(); + result = 0; + } + return static_cast(result); +} + +auto Python::GetPySession(PyObject* o) -> Session* { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + PyExcType pyexctype{PyExcType::kType}; + if (IsPySession(o)) { + // Look for an _sessiondata attr on it. + if (PyObject* sessiondata = PyObject_GetAttrString(o, "_sessiondata")) { + // This will deallocate for us. + PythonRef ref(sessiondata, PythonRef::kSteal); + if (PythonClassSessionData::Check(sessiondata)) { + // This will succeed or throw its own Exception. + return (reinterpret_cast(sessiondata)) + ->GetSession(); + } + } else { + pyexctype = PyExcType::kRuntime; // Wonky session obj. + } + } + + // Failed, we have. + // Clear any Python error that got us here; we're in C++ Exception land now. + PyErr_Clear(); + throw Exception( + "Can't get Session from value: " + Python::ObjToString(o) + ".", + pyexctype); +} + +auto Python::IsPyPlayer(PyObject* o) -> bool { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + int result = PyObject_IsInstance(o, g_python->obj(ObjID::kPlayerClass).get()); + if (result == -1) { + result = 0; + PyErr_Clear(); + } + return static_cast(result); +} + +auto Python::GetPyPlayer(PyObject* o, bool allow_empty_ref, bool allow_none) + -> Player* { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + PyExcType pyexctype{PyExcType::kType}; + + if (allow_none && (o == Py_None)) { + return nullptr; + } + + // Make sure it's a subclass of ba.Player. + if (IsPyPlayer(o)) { + // Look for an sessionplayer attr on it. + if (PyObject* sessionplayer = PyObject_GetAttrString(o, "sessionplayer")) { + // This will deallocate for us. + PythonRef ref(sessionplayer, PythonRef::kSteal); + + if (PythonClassSessionPlayer::Check(sessionplayer)) { + // This will succeed or throw an exception itself. + return (reinterpret_cast(sessionplayer)) + ->GetPlayer(!allow_empty_ref); + } + } else { + pyexctype = PyExcType::kRuntime; // We've got a wonky object. + } + } + + // Failed, we have. + // Clear any Python error that got us here; we're in C++ Exception land now. + PyErr_Clear(); + throw Exception( + "Can't get player from value: " + Python::ObjToString(o) + ".", + pyexctype); +} + +auto Python::IsPyHostActivity(PyObject* o) -> bool { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + int result = + PyObject_IsInstance(o, g_python->obj(ObjID::kActivityClass).get()); + if (result == -1) { + result = 0; + PyErr_Clear(); + } + return static_cast(result); +} + +auto Python::GetPyHostActivity(PyObject* o) -> HostActivity* { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + PyExcType pyexctype{PyExcType::kType}; + + // Make sure it's a subclass of ba.Activity. + if (IsPyHostActivity(o)) { + // Look for an _activity_data attr on it. + if (PyObject* activity_data = PyObject_GetAttrString(o, "_activity_data")) { + // This will deallocate for us. + PythonRef ref(activity_data, PythonRef::kSteal); + if (PythonClassActivityData::Check(activity_data)) { + return (reinterpret_cast(activity_data)) + ->GetHostActivity(); + } + } else { + pyexctype = PyExcType::kRuntime; // activity obj is wonky. + } + } + + // Failed, we have. + // Clear any Python error that got us here; we're in C++ Exception land now. + PyErr_Clear(); + throw Exception( + "Can't get activity from value: " + Python::ObjToString(o) + ".", + pyexctype); +} + +auto Python::GetPyNode(PyObject* o, bool allow_empty_ref, bool allow_none) + -> Node* { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (allow_none && (o == Py_None)) { + return nullptr; + } + if (PythonClassNode::Check(o)) { + // This will succeed or throw its own Exception. + return (reinterpret_cast(o))->GetNode(!allow_empty_ref); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception("Can't get node from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto Python::GetPyInputDevice(PyObject* o) -> InputDevice* { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (PythonClassInputDevice::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o)->GetInputDevice(); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception( + "Can't get input-device from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto Python::GetPySessionPlayer(PyObject* o, bool allow_empty_ref, + bool allow_none) -> Player* { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (allow_none && (o == Py_None)) { + return nullptr; + } + if (PythonClassSessionPlayer::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o)->GetPlayer( + !allow_empty_ref); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception( + "Can't get ba.SessionPlayer from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto Python::GetPyTexture(PyObject* o, bool allow_empty_ref, bool allow_none) + -> Texture* { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (allow_none && (o == Py_None)) { + return nullptr; + } + if (PythonClassTexture::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o)->GetTexture( + !allow_empty_ref); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception( + "Can't get ba.Texture from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto Python::GetPyModel(PyObject* o, bool allow_empty_ref, bool allow_none) + -> Model* { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (allow_none && (o == Py_None)) { + return nullptr; + } + if (PythonClassModel::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o)->GetModel(!allow_empty_ref); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception( + "Can't get ba.Model from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto Python::GetPySound(PyObject* o, bool allow_empty_ref, bool allow_none) + -> Sound* { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (allow_none && (o == Py_None)) { + return nullptr; + } + if (PythonClassSound::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o)->GetSound(!allow_empty_ref); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception( + "Can't get ba.Sound from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto Python::GetPyData(PyObject* o, bool allow_empty_ref, bool allow_none) + -> Data* { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (allow_none && (o == Py_None)) { + return nullptr; + } + if (PythonClassData::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o)->GetData(!allow_empty_ref); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception( + "Can't get ba.Data from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto Python::GetPyCollideModel(PyObject* o, bool allow_empty_ref, + bool allow_none) -> CollideModel* { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (allow_none && (o == Py_None)) { + return nullptr; + } + if (PythonClassCollideModel::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o)->GetCollideModel( + !allow_empty_ref); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception( + "Can't get ba.CollideModel from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto Python::GetPyWidget(PyObject* o) -> Widget* { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (PythonClassWidget::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o)->GetWidget(); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception( + "Can't get widget from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto Python::GetPyMaterial(PyObject* o, bool allow_empty_ref, bool allow_none) + -> Material* { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (allow_none && (o == Py_None)) { + return nullptr; + } + if (PythonClassMaterial::Check(o)) { + // This will succeed or throw its own Exception. + return reinterpret_cast(o)->GetMaterial( + !allow_empty_ref); + } + + // Nothing here should have led to an unresolved Python error state. + assert(!PyErr_Occurred()); + + throw Exception( + "Can't get material from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto Python::CanGetPyDouble(PyObject* o) -> bool { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + return static_cast(PyNumber_Check(o)); +} + +auto Python::GetPyDouble(PyObject* o) -> double { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + // Try to take the fast path if its a float. + if (PyFloat_Check(o)) { + return PyFloat_AS_DOUBLE(o); + } + if (PyNumber_Check(o)) { + if (PyObject* f = PyNumber_Float(o)) { + double val = PyFloat_AS_DOUBLE(f); + Py_DECREF(f); + return val; + } + } + + // Failed, we have. + // Clear any Python error that got us here; we're in C++ Exception land now. + PyErr_Clear(); + throw Exception( + "Can't get double from value: " + Python::ObjToString(o) + ".", + PyExcType::kType); +} + +auto Python::GetPyFloats(PyObject* o) -> std::vector { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.exists()); + Py_ssize_t size = PySequence_Fast_GET_SIZE(sequence.get()); + PyObject** py_objects = PySequence_Fast_ITEMS(sequence.get()); + std::vector vals(static_cast(size)); + assert(vals.size() == size); + for (Py_ssize_t i = 0; i < size; i++) { + vals[i] = Python::GetPyFloat(py_objects[i]); + } + return vals; +} + +auto Python::GetPyStrings(PyObject* o) -> std::vector { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.exists()); + Py_ssize_t size = PySequence_Fast_GET_SIZE(sequence.get()); + PyObject** py_objects = PySequence_Fast_ITEMS(sequence.get()); + std::vector vals(static_cast(size)); + assert(vals.size() == size); + for (Py_ssize_t i = 0; i < size; i++) { + vals[i] = Python::GetPyString(py_objects[i]); + } + return vals; +} + +template +auto GetPyIntsT(PyObject* o) -> std::vector { + assert(Python::HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.exists()); + Py_ssize_t size = PySequence_Fast_GET_SIZE(sequence.get()); + PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.get()); + std::vector vals(static_cast(size)); + assert(vals.size() == size); + for (Py_ssize_t i = 0; i < size; i++) { + vals[i] = GetPyIntT(pyobjs[i]); + } + return vals; +} + +auto Python::GetPyInts64(PyObject* o) -> std::vector { + return GetPyIntsT(o); +} + +auto Python::GetPyInts(PyObject* o) -> std::vector { + return GetPyIntsT(o); +} + +// Hmm should just template the above func? +auto Python::GetPyUInts64(PyObject* o) -> std::vector { + return GetPyIntsT(o); +} + +auto Python::GetPyNodes(PyObject* o) -> std::vector { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.exists()); + auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.get())); + PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.get()); + std::vector vals(size); + assert(vals.size() == size); + for (size_t i = 0; i < size; i++) { + vals[i] = Python::GetPyNode(pyobjs[i]); + } + return vals; +} + +auto Python::GetPyMaterials(PyObject* o) -> std::vector { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.exists()); + auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.get())); + PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.get()); + std::vector vals(size); + assert(vals.size() == size); + for (size_t i = 0; i < size; i++) { + vals[i] = GetPyMaterial(pyobjs[i]); // DON'T allow nullptr refs. + } + return vals; +} + +auto Python::GetPyTextures(PyObject* o) -> std::vector { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.exists()); + auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.get())); + PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.get()); + std::vector vals(size); + assert(vals.size() == size); + for (size_t i = 0; i < size; i++) { + vals[i] = GetPyTexture(pyobjs[i]); // DON'T allow nullptr refs or None. + } + return vals; +} + +auto Python::GetPySounds(PyObject* o) -> std::vector { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.exists()); + auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.get())); + PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.get()); + std::vector vals(size); + assert(vals.size() == size); + for (size_t i = 0; i < size; i++) { + vals[i] = GetPySound(pyobjs[i]); // DON'T allow nullptr refs + } + return vals; +} + +auto Python::GetPyModels(PyObject* o) -> std::vector { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.exists()); + auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.get())); + PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.get()); + std::vector vals(size); + assert(vals.size() == size); + for (size_t i = 0; i < size; i++) { + vals[i] = GetPyModel(pyobjs[i], false); // DON'T allow nullptr refs. + } + return vals; +} + +auto Python::GetPyCollideModels(PyObject* o) -> std::vector { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (!PySequence_Check(o)) { + throw Exception("Object is not a sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.exists()); + auto size = static_cast(PySequence_Fast_GET_SIZE(sequence.get())); + PyObject** pyobjs = PySequence_Fast_ITEMS(sequence.get()); + std::vector vals(size); + assert(vals.size() == size); + for (size_t i = 0; i < size; i++) { + vals[i] = GetPyCollideModel(pyobjs[i]); // DON'T allow nullptr refs. + } + return vals; +} + +auto Python::GetPyPoint2D(PyObject* o) -> Point2D { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + Point2D p; + if (!PyTuple_Check(o) || (PyTuple_GET_SIZE(o) != 2)) { + throw Exception("Expected 2 member tuple for point.", PyExcType::kType); + } + p.x = Python::GetPyFloat(PyTuple_GET_ITEM(o, 0)); + p.y = Python::GetPyFloat(PyTuple_GET_ITEM(o, 1)); + return p; +} + +auto Python::CanGetPyVector3f(PyObject* o) -> bool { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (PythonClassVec3::Check(o)) { + return true; + } + if (!PySequence_Check(o)) { + return false; + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.exists()); // Should always work; we checked seq. + if (PySequence_Fast_GET_SIZE(sequence.get()) != 3) { + return false; + } + return ( + Python::CanGetPyDouble(PySequence_Fast_GET_ITEM(sequence.get(), 0)) + && Python::CanGetPyDouble(PySequence_Fast_GET_ITEM(sequence.get(), 1)) + && Python::CanGetPyDouble(PySequence_Fast_GET_ITEM(sequence.get(), 2))); +} + +auto Python::GetPyVector3f(PyObject* o) -> Vector3f { + assert(HaveGIL()); + BA_PRECONDITION_FATAL(o != nullptr); + + if (PythonClassVec3::Check(o)) { + return (reinterpret_cast(o))->value; + } + if (!PySequence_Check(o)) { + throw Exception("Object is not a ba.Vec3 or sequence.", PyExcType::kType); + } + PythonRef sequence(PySequence_Fast(o, "Not a sequence."), PythonRef::kSteal); + assert(sequence.exists()); // Should always work; we checked seq. + if (PySequence_Fast_GET_SIZE(sequence.get()) != 3) { + throw Exception("Sequence is not of size 3.", PyExcType::kValue); + } + return {Python::GetPyFloat(PySequence_Fast_GET_ITEM(sequence.get(), 0)), + Python::GetPyFloat(PySequence_Fast_GET_ITEM(sequence.get(), 1)), + Python::GetPyFloat(PySequence_Fast_GET_ITEM(sequence.get(), 2))}; +} + +Python::Python() = default; + +void Python::SetupInterpreterDebugState() { + // Go for python opt mode 1 if we're doing optimization + // (ignores __debug__ and asserts but keeps docstrings intact). +#if !BA_DEBUG_BUILD + Py_OptimizeFlag = 1; +#else + // const char* debug_msg; + // #ifdef Py_DEBUG + // debug_msg = "Python Debug Mode Enabled (also Py_DEBUG build)."; + // #else + // debug_msg = "Python Debug Mode Enabled."; + // #endif + // ScreenMessage(debug_msg, {1.0, 1.0, 0.0}); + // Log(debug_msg, true, false); + + // Sanity test: our XCode, Android, and Windows builds should be + // using a debug build of the python library. + // Todo: could also verify this at runtime by checking for + // existence of sys.gettotalrefcount(). (is that still valid in 3.8?) +#if BA_XCODE_BUILD || BA_OSTYPE_ANDROID || BA_OSTYPE_WINDOWS +#ifndef Py_DEBUG +#error Expected Py_DEBUG to be defined for this build. +#endif // Py_DEBUG +#endif // BA_XCODE_BUILD || BA_OSTYPE_ANDROID + +#endif // !BA_DEBUG_BUILD +} + +void Python::SetupPythonHome() { + // For platforms where we package python ourself, we need to tell + // it where to find its stuff. + + if (g_platform->ContainsPythonDist()) { + if (g_buildconfig.ostype_windows()) { + // Windows Python looks for Lib and DLLs dirs by default, along with some + // others, but we want to be more explicit in limiting to these. It also + // seems that windows Python's paths can be incorrect if we're in strange + // dirs such as \\wsl$\Ubuntu-18.04\ that we get with WSL build setups. + std::string cwd = g_platform->GetCWD(); + + // NOTE: Python for windows actually comes with 'Lib', not 'lib', but + // it seems the interpreter defaults point to ./lib (as of 3.8.5). + // Normally this doesn't matter since windows is case-insensitive but + // under WSL it does. + // So we currently bundle the dir as 'lib' and use that in our path so + // that everything is happy (both with us and with python.exe). + std::string libpath = cwd + BA_DIRSLASH + "lib"; + std::string dllpath = cwd + BA_DIRSLASH + "DLLs"; + std::string fullpath = libpath + ";" + dllpath; + + // Py_DecodeLocale() allocs memory for a wide string which works out + // well since Python requires the pointers we pass to stick around. + Py_SetPath(Py_DecodeLocale(fullpath.c_str(), nullptr)); + + } else { + // Note: tried passing a relative path here + // (to keep tracebacks and other paths cleaner) but it seems + // to get converted to absolute under the hood. + std::string cwd = g_platform->GetCWD(); + std::string libpath = cwd + BA_DIRSLASH + "pylib"; + + // Py_DecodeLocale() allocs memory for a wide string which works out + // well since Python requires the pointers we pass to stick around. + Py_SetPath(Py_DecodeLocale(libpath.c_str(), nullptr)); + } + } +} + +void Python::Reset(bool do_init) { + assert(InGameThread()); + assert(g_python); + + bool was_inited = inited_; + + if (inited_) { + ReleaseGamePadInput(); + ReleaseKeyboardInput(); + g_graphics->ReleaseFadeEndCommand(); + inited_ = false; + } + + if (!was_inited && do_init) { + // Wrangle whether we'll compile our stuff in debug vs opt mode, etc. + SetupInterpreterDebugState(); + + // We want consistent utf-8 everywhere (Python used to default to + // windows-specific file encodings, etc.) + // Note: we have to do this before SetupPythonHome() because it affects + // the Py_DecodeLocale() calls there. + Py_UTF8Mode = 1; + + // Set up system paths on our embedded platforms. + SetupPythonHome(); + + AppInternalInitModule(); + + Py_Initialize(); + + PyObject* m; + BA_PRECONDITION(m = PyImport_AddModule("__main__")); + BA_PRECONDITION(main_dict_ = PyModule_GetDict(m)); + + const char* ver = Py_GetVersion(); + + if (strncmp(ver, "3.8", 3)) { + throw Exception("We require Python 3.8.x; instead found " + + std::string(ver)); + } + + SetObj(ObjID::kEmptyTuple, PyTuple_New(0)); + + // Get the app up and running. + // Run a few core bootstrappy things first: + // - get stdout/stderr redirection up so we can intercept python output + // - add our user and system script dirs to python path + // - import and instantiate our app-state class + +#include "generated/ballistica/bootstrap.inc" + int result = PyRun_SimpleString(bootstrap_code); + if (result != 0) { + PyErr_PrintEx(0); + + // Throw a simple exception so we don't get a stack trace. + throw std::logic_error( + "Error in ba Python bootstrapping. See log for details."); + } + PyObject* appstate = + PythonCommand("_app_state", "").RunReturnObj(); + if (appstate == nullptr) { + throw Exception("Unable to get value: '" + std::string("_app_state") + + "'."); + } + SetObj(ObjID::kApp, appstate); + + // Import and grab all the Python stuff we use. +#include "generated/ballistica/binding.inc" + + AppInternalPythonInit2(); + + // Alright I guess let's pull ba in to main, since pretty + // much all interactive commands will be using it. + // If we ever build the game as a pure python module we should + // of course not do this. + BA_PRECONDITION(PyRun_SimpleString("import ba") == 0); + + // Read the config file and store the config dict for easy access. + obj(ObjID::kReadConfigCall).Call(); + SetObj(ObjID::kConfig, obj(ObjID::kApp).GetAttr("config").get()); + assert(PyDict_Check(obj(ObjID::kConfig).get())); + + // Turn off fancy-pants cyclic garbage-collection. + // We run it only at explicit times to avoid random hitches and keep + // things more deterministic. + // Non-reference-looped objects will still get cleaned up + // immediately, so we should try to structure things to avoid + // reference loops (just like Swift, ObjC, etc). + g_python->obj(Python::ObjID::kGCDisableCall).Call(); + } + if (do_init) { + inited_ = true; + } +} + +void Python::PushObjCall(ObjID obj_id) { + g_game->PushCall([obj_id] { + ScopedSetContext cp(g_game->GetUIContext()); + g_python->obj(obj_id).Call(); + }); +} + +void Python::PushObjCall(ObjID obj_id, const std::string& arg) { + g_game->PushCall([this, obj_id, arg] { + ScopedSetContext cp(g_game->GetUIContext()); + PythonRef args(Py_BuildValue("(s)", arg.c_str()), + ballistica::PythonRef::kSteal); + obj(obj_id).Call(args); + }); +} + +Python::~Python() { Reset(false); } + +auto Python::GetResource(const char* key, const char* fallback_resource, + const char* fallback_value) -> std::string { + assert(InGameThread()); + PythonRef results; + BA_PRECONDITION(key != nullptr); + const PythonRef& get_resource_call(obj(ObjID::kGetResourceCall)); + if (fallback_value != nullptr) { + if (fallback_resource == nullptr) { + BA_PRECONDITION(key != nullptr); + PythonRef args(Py_BuildValue("(sOs)", key, Py_None, fallback_value), + PythonRef::kSteal); + + // Don't print errors. + results = get_resource_call.Call(args, PythonRef(), false); + } else { + PythonRef args( + Py_BuildValue("(sss)", key, fallback_resource, fallback_value), + PythonRef::kSteal); + + // Don't print errors. + results = get_resource_call.Call(args, PythonRef(), false); + } + } else if (fallback_resource != nullptr) { + PythonRef args(Py_BuildValue("(ss)", key, fallback_resource), + PythonRef::kSteal); + + // Don't print errors + results = get_resource_call.Call(args, PythonRef(), false); + } else { + PythonRef args(Py_BuildValue("(s)", key), PythonRef::kSteal); + + // Don't print errors. + results = get_resource_call.Call(args, PythonRef(), false); + } + if (results.exists()) { + try { + return GetPyString(results.get()); + } catch (const std::exception&) { + Log("GetResource failed for '" + std::string(key) + "'"); + + // Hmm; I guess let's just return the key to help identify/fix the + // issue?.. + return std::string(""; + } + } else { + Log("GetResource failed for '" + std::string(key) + "'"); + } + + // Hmm; I guess let's just return the key to help identify/fix the issue?.. + return std::string(""; +} + +auto Python::GetTranslation(const char* category, const char* s) + -> std::string { + assert(InGameThread()); + PythonRef results; + PythonRef args(Py_BuildValue("(ss)", category, s), PythonRef::kSteal); + // Don't print errors. + results = obj(ObjID::kTranslateCall).Call(args, PythonRef(), false); + if (results.exists()) { + try { + return GetPyString(results.get()); + } catch (const std::exception&) { + Log("GetTranslation failed for '" + std::string(category) + "'"); + return ""; + } + } else { + Log("GetTranslation failed for category '" + std::string(category) + "'"); + } + return ""; +} + +void Python::RunDeepLink(const std::string& url) { + assert(InGameThread()); + if (objexists(ObjID::kDeepLinkCall)) { + ScopedSetContext cp(g_game->GetUIContext()); + PythonRef args(Py_BuildValue("(s)", url.c_str()), PythonRef::kSteal); + obj(ObjID::kDeepLinkCall).Call(args); + } else { + Log("Error on deep-link call"); + } +} + +void Python::PlayMusic(const std::string& music_type, bool continuous) { + assert(InGameThread()); + if (music_type.empty()) { + PythonRef args( + Py_BuildValue("(OO)", Py_None, continuous ? Py_True : Py_False), + PythonRef::kSteal); + obj(ObjID::kDoPlayMusicCall).Call(args); + } else { + PythonRef args(Py_BuildValue("(sO)", music_type.c_str(), + continuous ? Py_True : Py_False), + PythonRef::kSteal); + obj(ObjID::kDoPlayMusicCall).Call(args); + } +} + +void Python::ShowURL(const std::string& url) { + if (objexists(ObjID::kShowURLWindowCall)) { + ScopedSetContext cp(g_game->GetUIContext()); + PythonRef args(Py_BuildValue("(s)", url.c_str()), PythonRef::kSteal); + obj(ObjID::kShowURLWindowCall).Call(args); + } else { + Log("Error: ShowURLWindowCall nonexistent."); + } +} + +auto Python::FilterChatMessage(std::string* message, int client_id) -> bool { + assert(message); + ScopedSetContext cp(g_game->GetUIContext()); + PythonRef args(Py_BuildValue("(si)", message->c_str(), client_id), + PythonRef::kSteal); + PythonRef result = obj(ObjID::kFilterChatMessageCall).Call(args); + + // If something went wrong, just allow all messages through verbatim. + if (!result.exists()) { + return true; + } + + // If they returned None, they want to ignore the message. + if (result.get() == Py_None) { + return false; + } + + // Replace the message string with whatever they gave us. + try { + *message = Python::GetPyString(result.get()); + } catch (const std::exception& e) { + Log("Error getting string from chat filter: " + std::string(e.what())); + } + return true; +} + +void Python::HandleLocalChatMessage(const std::string& message) { + ScopedSetContext cp(g_game->GetUIContext()); + PythonRef args(Py_BuildValue("(s)", message.c_str()), PythonRef::kSteal); + obj(ObjID::kHandleLocalChatMessageCall).Call(args); +} + +void Python::DispatchScoresToBeatResponse( + bool success, const std::list& scores_to_beat, + void* callback_in) { + // callback_in was a newly allocated PythonContextCall. + // This will make it ref-counted so it'll die when we're done with it + auto callback( + Object::MakeRefCounted(static_cast(callback_in))); + + // Empty type denotes error. + if (!success) { + PythonRef args(Py_BuildValue("(O)", Py_None), PythonRef::kSteal); + callback->Run(args); + } else { + PyObject* py_list = PyList_New(0); + for (const auto& i : scores_to_beat) { + PyObject* val = Py_BuildValue("{sssssssd}", "player", i.player.c_str(), + "type", i.type.c_str(), "value", + i.value.c_str(), "time", i.time); + PyList_Append(py_list, val); + Py_DECREF(val); + } + PythonRef args(Py_BuildValue("(O)", py_list), PythonRef::kSteal); + Py_DECREF(py_list); + callback->Run(args); + } +} + +// Put together a node message with all args on the provided tuple (starting +// with arg_offset) returns false on failure, true on success. +void Python::DoBuildNodeMessage(PyObject* args, int arg_offset, Buffer* b, + PyObject** user_message_obj) { + Py_ssize_t tuple_size = PyTuple_GET_SIZE(args); + if (tuple_size - arg_offset < 1) { + throw Exception("Got message of size zero.", PyExcType::kValue); + } + std::string type; + PyObject* obj; + + // Pull first arg. + obj = PyTuple_GET_ITEM(args, arg_offset); + BA_PRECONDITION(obj); + if (!PyUnicode_Check(obj)) { + // If first arg is not a string, its an actual message itself. + (*user_message_obj) = obj; + return; + } else { + (*user_message_obj) = nullptr; + } + type = Python::GetPyString(obj); + NodeMessageType ac = Scene::GetNodeMessageType(type); + const char* format = Scene::GetNodeMessageFormat(ac); + assert(format); + const char* f = format; + + // Allow space for 1 type byte (fixme - may need more than 1). + size_t full_size = 1; + for (Py_ssize_t i = arg_offset + 1; i < tuple_size; i++) { + // Make sure our format string ends the same time as our arg count. + if (*f == 0) { + throw Exception( + "Wrong number of arguments on node message '" + type + "'.", + PyExcType::kValue); + } + obj = PyTuple_GET_ITEM(args, i); + BA_PRECONDITION(obj); + switch (*f) { + case 'I': + + // 4 byte int + if (!PyNumber_Check(obj)) { + throw Exception("Expected an int for node message arg " + + std::to_string(i - (arg_offset + 1)) + ".", + PyExcType::kType); + } + full_size += 4; + break; + case 'i': + + // 2 byte int. + if (!PyNumber_Check(obj)) { + throw Exception("Expected an int for node message arg " + + std::to_string(i - (arg_offset + 1)) + ".", + PyExcType::kType); + } + full_size += 2; + break; + case 'c': // NOLINT(bugprone-branch-clone) + + // 1 byte int. + if (!PyNumber_Check(obj)) { + throw Exception("Expected an int for node message arg " + + std::to_string(i - (arg_offset + 1)) + ".", + PyExcType::kType); + } + full_size += 1; + break; + case 'b': + + // bool (currently 1 byte int). + if (!PyNumber_Check(obj)) { + throw Exception("Expected an int for node message arg " + + std::to_string(i - (arg_offset + 1)) + ".", + PyExcType::kType); + } + full_size += 1; + break; + case 'F': + + // 32 bit float. + if (!PyNumber_Check(obj)) { + throw Exception("Expected a float for node message arg " + + std::to_string(i - (arg_offset + 1)) + ".", + PyExcType::kType); + } + full_size += 4; + break; + case 'f': + + // 16 bit float. + if (!PyNumber_Check(obj)) { + throw Exception("Expected a float for node message arg " + + std::to_string(i - (arg_offset + 1)) + ".", + PyExcType::kType); + } + full_size += 2; + break; + case 's': + if (!PyUnicode_Check(obj)) { + throw Exception("Expected a string for node message arg " + + std::to_string(i - (arg_offset + 1)) + ".", + PyExcType::kType); + } + full_size += strlen(PyUnicode_AsUTF8(obj)) + 1; + break; + default: + throw Exception("Invalid argument type: " + std::to_string(*f) + ".", + PyExcType::kValue); + break; + } + f++; + } + + // Make sure our format string ends the same time as our arg count. + if (*f != 0) { + throw Exception("Wrong number of arguments on node message '" + type + "'.", + PyExcType::kValue); + } + (*b).Resize(full_size); + char* ptr = (*b).data(); + *ptr = static_cast(ac); + ptr++; + f = format; + for (Py_ssize_t i = arg_offset + 1; i < tuple_size; i++) { + obj = PyTuple_GET_ITEM(args, i); + BA_PRECONDITION(obj); + switch (*f) { + case 'I': + Utils::EmbedInt32NBO( + &ptr, static_cast_check_fit(Python::GetPyInt64(obj))); + break; + case 'i': + Utils::EmbedInt16NBO( + &ptr, static_cast_check_fit(Python::GetPyInt64(obj))); + break; + case 'c': // NOLINT(bugprone-branch-clone) + Utils::EmbedInt8( + &ptr, static_cast_check_fit(Python::GetPyInt64(obj))); + break; + case 'b': + Utils::EmbedInt8( + &ptr, static_cast_check_fit(Python::GetPyInt64(obj))); + break; + case 'F': + Utils::EmbedFloat32(&ptr, Python::GetPyFloat(obj)); + break; + case 'f': + Utils::EmbedFloat16NBO(&ptr, Python::GetPyFloat(obj)); + break; + case 's': + Utils::EmbedString(&ptr, PyUnicode_AsUTF8(obj)); + break; + default: + throw Exception(PyExcType::kValue); + break; + } + f++; + } +} + +auto Python::GetPythonFileLocation(bool pretty) -> std::string { + PyFrameObject* f = PyEval_GetFrame(); + if (f) { + const char* path; + if (f->f_code && f->f_code->co_filename) { + assert(PyUnicode_Check(f->f_code->co_filename)); + path = PyUnicode_AsUTF8(f->f_code->co_filename); + if (pretty) { + if (path[0] == '<') { + // Filter stuff like :1 + return ""; + } else { + // Advance past any '/' and '\'s + while (true) { + const char* s = strchr(path, '/'); + if (s) { + path = s + 1; + } else { + const char* s2 = strchr(path, '\\'); + if (s2) { + path = s2 + 1; + } else { + break; + } + } + } + } + } + } else { + path = ""; + } + std::string name = + std::string(path) + ":" + std::to_string(PyFrame_GetLineNumber(f)); + return name; + } + return ""; +} + +void Python::SetNodeAttr(Node* node, const char* attr_name, + PyObject* value_obj) { + assert(node); + GameStream* out_stream = node->scene()->GetGameStream(); + NodeAttribute attr = node->GetAttribute(attr_name); + switch (attr.type()) { + case NodeAttributeType::kFloat: { + float val = Python::GetPyFloat(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kInt: { + int64_t val = Python::GetPyInt64(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kBool: { + bool val = Python::GetPyBool(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kFloatArray: { + std::vector vals = Python::GetPyFloats(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, vals); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(vals); + break; + } + case NodeAttributeType::kIntArray: { + std::vector vals = Python::GetPyInts64(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, vals); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(vals); + break; + } + case NodeAttributeType::kString: { + std::string val = Python::GetPyString(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kNode: { + // Allow dead-refs or None. + Node* val = Python::GetPyNode(value_obj, true, true); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kNodeArray: { + std::vector vals = Python::GetPyNodes(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, vals); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(vals); + break; + } + case NodeAttributeType::kPlayer: { + // Allow dead-refs and None. + Player* val = Python::GetPyPlayer(value_obj, true, true); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kMaterialArray: { + std::vector vals = Python::GetPyMaterials(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, vals); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(vals); + break; + } + case NodeAttributeType::kTexture: { + // Don't allow dead-refs, do allow None. + Texture* val = Python::GetPyTexture(value_obj, false, true); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kTextureArray: { + std::vector vals = Python::GetPyTextures(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, vals); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(vals); + break; + } + case NodeAttributeType::kSound: { + // Don't allow dead-refs, do allow None. + Sound* val = Python::GetPySound(value_obj, false, true); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kSoundArray: { + std::vector vals = Python::GetPySounds(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, vals); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(vals); + break; + } + case NodeAttributeType::kModel: { + // Don't allow dead-refs, do allow None. + Model* val = Python::GetPyModel(value_obj, false, true); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kModelArray: { + std::vector vals = Python::GetPyModels(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, vals); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(vals); + break; + } + case NodeAttributeType::kCollideModel: { + // Don't allow dead-refs, do allow None. + CollideModel* val = Python::GetPyCollideModel(value_obj, false, true); + if (out_stream) { + out_stream->SetNodeAttr(attr, val); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(val); + break; + } + case NodeAttributeType::kCollideModelArray: { + std::vector vals = Python::GetPyCollideModels(value_obj); + if (out_stream) { + out_stream->SetNodeAttr(attr, vals); + } + + // If something was driving this attr, disconnect it. + attr.DisconnectIncoming(); + attr.Set(vals); + break; + } + default: + throw Exception("FIXME: unhandled attr type in SetNodeAttr: '" + + attr.GetTypeName() + "'."); + } +} + +static auto CompareAttrIndices( + const std::pair& first, + const std::pair& second) -> bool { + return (first.first->index() < second.first->index()); +} + +auto Python::DoNewNode(PyObject* args, PyObject* keywds) -> Node* { + PyObject* delegate_obj = Py_None; + PyObject* owner_obj = Py_None; + PyObject* name_obj = Py_None; + static const char* kwlist[] = {"type", "owner", "attrs", + "name", "delegate", nullptr}; + char* type; + PyObject* dict = nullptr; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "s|OOOO", const_cast(kwlist), &type, &owner_obj, + &dict, &name_obj, &delegate_obj)) { + return nullptr; + } + + std::string name; + if (name_obj != Py_None) { + name = GetPyString(name_obj); + } else { + // By default do something like 'text@foo.py:20'. + name = std::string(type) + "@" + GetPythonFileLocation(); + } + + Scene* scene = Context::current().GetMutableScene(); + if (!scene) { + throw Exception("Can't create nodes in this context.", PyExcType::kContext); + } + + Node* node = scene->NewNode(type, name, delegate_obj); + + // Handle attr values fed in. + if (dict) { + if (!PyDict_Check(dict)) { + throw Exception("Expected dict for arg 2.", PyExcType::kType); + } + NodeType* t = node->type(); + PyObject* key{}; + PyObject* value{}; + Py_ssize_t pos{}; + + // We want to set initial attrs in order based on their attr indices. + std::list > attr_vals; + + // Grab all initial attr/values and add them to a list. + while (PyDict_Next(dict, &pos, &key, &value)) { + if (!PyUnicode_Check(key)) { + throw Exception("Expected string key in attr dict.", PyExcType::kType); + } + try { + attr_vals.emplace_back( + t->GetAttribute(std::string(PyUnicode_AsUTF8(key))), value); + } catch (const std::exception&) { + Log("ERROR: Attr not found on initial attr set: '" + + std::string(PyUnicode_AsUTF8(key)) + "' on " + type + " node '" + + name + "'"); + } + } + + // Run the sets in the order of attr indices. + attr_vals.sort(CompareAttrIndices); + for (auto&& i : attr_vals) { + try { + SetNodeAttr(node, i.first->name().c_str(), i.second); + } catch (const std::exception& e) { + Log("ERROR: exception in initial attr set for attr '" + i.first->name() + + "' on " + type + " node '" + name + "':" + e.what()); + } + } + } + + // If an owner was provided, set it up. + if (owner_obj != Py_None) { + // If its a node, set up a dependency at the scene level + // (then we just have to delete the owner node and the scene does the + // rest). + if (PythonClassNode::Check(owner_obj)) { + Node* owner_node = GetPyNode(owner_obj, true); + if (owner_node == nullptr) { + Log("ERROR: empty node-ref passed for 'owner'; pass None if you want " + "no owner."); + } else if (owner_node->scene() != node->scene()) { + Log("ERROR: owner node is from a different scene; ignoring."); + } else { + owner_node->AddDependentNode(node); + } + } else { + throw Exception( + "Invalid node owner: " + Python::ObjToString(owner_obj) + ".", + PyExcType::kType); + } + } + + // Lastly, call this node's OnCreate method for any final setup it may want to + // do. + try { + // Tell clients to do the same. + if (GameStream* output_stream = scene->GetGameStream()) { + output_stream->NodeOnCreate(node); + } + node->OnCreate(); + } catch (const std::exception& e) { + Log("ERROR: exception in OnCreate() for node " + + ballistica::ObjToString(node) + "':" + e.what()); + } + + return node; +} + +// Return the node attr as a PyObject, or nullptr if the node doesn't have that +// attr. +auto Python::GetNodeAttr(Node* node, const char* attr_name) -> PyObject* { + assert(node); + NodeAttribute attr = node->GetAttribute(attr_name); + switch (attr.type()) { + case NodeAttributeType::kFloat: + return PyFloat_FromDouble(attr.GetAsFloat()); + break; + case NodeAttributeType::kInt: + return PyLong_FromLong( + static_cast_check_fit(attr.GetAsInt())); // NOLINT + break; + case NodeAttributeType::kBool: + if (attr.GetAsBool()) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + break; + case NodeAttributeType::kString: { + if (g_buildconfig.debug_build()) { + std::string s = attr.GetAsString(); + assert(Utils::IsValidUTF8(s)); + return PyUnicode_FromString(s.c_str()); + } else { + return PyUnicode_FromString(attr.GetAsString().c_str()); + } + break; + } + case NodeAttributeType::kNode: { + // Return a new py ref to this node or create a new empty ref. + Node* n = attr.GetAsNode(); + return n ? n->NewPyRef() : PythonClassNode::Create(nullptr); + break; + } + case NodeAttributeType::kPlayer: { + // Player attrs deal with custom user ba.Player classes; + // not our internal SessionPlayer class. + Player* p = attr.GetAsPlayer(); + if (p == nullptr) { + Py_RETURN_NONE; + } + PyObject* gameplayer = p->GetPyActivityPlayer(); + Py_INCREF(gameplayer); + return gameplayer; + // return p ? p->NewPyRef() : PythonClassSessionPlayer::Create(nullptr); + break; + } + case NodeAttributeType::kFloatArray: { + std::vector vals = attr.GetAsFloats(); + Py_ssize_t size = vals.size(); + PyObject* vals_obj = PyTuple_New(size); + BA_PRECONDITION(vals_obj); + for (Py_ssize_t i = 0; i < size; i++) { + PyTuple_SET_ITEM(vals_obj, i, PyFloat_FromDouble(vals[i])); + } + return vals_obj; + break; + } + case NodeAttributeType::kIntArray: { + std::vector vals = attr.GetAsInts(); + Py_ssize_t size = vals.size(); + PyObject* vals_obj = PyTuple_New(size); + BA_PRECONDITION(vals_obj); + for (Py_ssize_t i = 0; i < size; i++) { + PyTuple_SET_ITEM(vals_obj, i, + PyLong_FromLong(static_cast_check_fit( // NOLINT + vals[i]))); + } + return vals_obj; + break; + } + case NodeAttributeType::kNodeArray: { + std::vector vals = attr.GetAsNodes(); + Py_ssize_t size = vals.size(); + PyObject* vals_obj = PyTuple_New(size); + BA_PRECONDITION(vals_obj); + for (Py_ssize_t i = 0; i < size; i++) { + Node* n = vals[i]; + PyTuple_SET_ITEM(vals_obj, i, + n ? n->NewPyRef() : PythonClassNode::Create(nullptr)); + } + return vals_obj; + break; + } + case NodeAttributeType::kTexture: { + Texture* t = attr.GetAsTexture(); + if (!t) { + Py_RETURN_NONE; + } + return t->NewPyRef(); + break; + } + case NodeAttributeType::kSound: { + Sound* s = attr.GetAsSound(); + if (!s) { + Py_RETURN_NONE; + } + return s->NewPyRef(); + break; + } + case NodeAttributeType::kModel: { + Model* m = attr.GetAsModel(); + if (!m) { + Py_RETURN_NONE; + } + return m->NewPyRef(); + break; + } + case NodeAttributeType::kCollideModel: { + CollideModel* c = attr.GetAsCollideModel(); + if (!c) { + Py_RETURN_NONE; + } + return c->NewPyRef(); + break; + } + case NodeAttributeType::kMaterialArray: { + std::vector vals = attr.GetAsMaterials(); + Py_ssize_t size = vals.size(); + PyObject* vals_obj = PyTuple_New(size); + BA_PRECONDITION(vals_obj); + for (Py_ssize_t i = 0; i < size; i++) { + Material* m = vals[i]; + + // Array attrs should never return nullptr materials. + assert(m); + PyTuple_SET_ITEM(vals_obj, i, m->NewPyRef()); + } + return vals_obj; + break; + } + case NodeAttributeType::kTextureArray: { + std::vector vals = attr.GetAsTextures(); + Py_ssize_t size = vals.size(); + PyObject* vals_obj = PyTuple_New(size); + BA_PRECONDITION(vals_obj); + for (Py_ssize_t i = 0; i < size; i++) { + Texture* t = vals[i]; + + // Array attrs should never return nullptr textures. + assert(t); + PyTuple_SET_ITEM(vals_obj, i, t->NewPyRef()); + } + return vals_obj; + break; + } + case NodeAttributeType::kSoundArray: { + std::vector vals = attr.GetAsSounds(); + Py_ssize_t size = vals.size(); + PyObject* vals_obj = PyTuple_New(size); + BA_PRECONDITION(vals_obj); + for (Py_ssize_t i = 0; i < size; i++) { + Sound* s = vals[i]; + + // Array attrs should never return nullptr sounds. + assert(s); + PyTuple_SET_ITEM(vals_obj, i, s->NewPyRef()); + } + return vals_obj; + break; + } + case NodeAttributeType::kModelArray: { + std::vector vals = attr.GetAsModels(); + Py_ssize_t size = vals.size(); + PyObject* vals_obj = PyTuple_New(size); + BA_PRECONDITION(vals_obj); + for (Py_ssize_t i = 0; i < size; i++) { + Model* m = vals[i]; + + // Array attrs should never return nullptr models. + assert(m); + PyTuple_SET_ITEM(vals_obj, i, m->NewPyRef()); + } + return vals_obj; + break; + } + case NodeAttributeType::kCollideModelArray: { + std::vector vals = attr.GetAsCollideModels(); + Py_ssize_t size = vals.size(); + PyObject* vals_obj = PyTuple_New(size); + BA_PRECONDITION(vals_obj); + for (Py_ssize_t i = 0; i < size; i++) { + CollideModel* c = vals[i]; + + // Array attrs should never return nullptr collide-models. + assert(c); + PyTuple_SET_ITEM(vals_obj, i, c->NewPyRef()); + } + return vals_obj; + break; + } + + default: + throw Exception("FIXME: unhandled attr type in GetNodeAttr: '" + + attr.GetTypeName() + "'."); + } + return nullptr; +} + +void Python::IssueCallInGameThreadWarning(PyObject* call_obj) { + Log("WARNING: ba.pushcall() called from the game thread with " + "from_other_thread set to true (call " + + ObjToString(call_obj) + " at " + GetPythonFileLocation() + + "). That arg should only be used from other threads."); +} + +void Python::LaunchStringEdit(TextWidget* w) { + assert(InGameThread()); + BA_PRECONDITION(w); + + ScopedSetContext cp(g_game->GetUIContext()); + g_audio->PlaySound(g_media->GetSound(SystemSoundID::kSwish)); + + // Gotta run this in the next cycle. + PythonRef args(Py_BuildValue("(Osi)", w->BorrowPyRef(), + w->description().c_str(), w->max_chars()), + PythonRef::kSteal); + g_game->PushPythonCallArgs( + Object::New(obj(ObjID::kOnScreenKeyboardClass).get()), + args); +} + +void Python::CaptureGamePadInput(PyObject* obj) { + assert(InGameThread()); + ReleaseGamePadInput(); + if (PyCallable_Check(obj)) { + game_pad_call_.Acquire(obj); + } else { + throw Exception("Object is not callable.", PyExcType::kType); + } +} + +void Python::ReleaseGamePadInput() { game_pad_call_.Release(); } + +void Python::CaptureKeyboardInput(PyObject* obj) { + assert(InGameThread()); + ReleaseKeyboardInput(); + if (PyCallable_Check(obj)) { + keyboard_call_.Acquire(obj); + } else { + throw Exception("Object is not callable.", PyExcType::kType); + } +} +void Python::ReleaseKeyboardInput() { keyboard_call_.Release(); } + +void Python::HandleFriendScoresCB(const FriendScoreSet& score_set) { + // This is the initial strong-ref to this pointer + // so it will be cleaned up properly. + Object::Ref cb( + static_cast(score_set.user_data)); + + // We pass None on error. + if (!score_set.success) { + PythonRef args(Py_BuildValue("(O)", Py_None), PythonRef::kSteal); + cb->Run(args); + } else { + // Otherwise convert it to a python list and pass that. + PyObject* py_list = PyList_New(0); + std::string icon_str; +#if BA_USE_GOOGLE_PLAY_GAME_SERVICES + icon_str = g_game->CharStr(SpecialChar::kGooglePlayGamesLogo); +#elif BA_USE_GAME_CIRCLE + icon_str = g_game->CharStr(SpecialChar::kGameCircleLogo); +#elif BA_USE_GAME_CENTER + icon_str = g_game->CharStr(SpecialChar::kGameCenterLogo); +#endif + for (auto&& i : score_set.entries) { + PyObject* obj = + Py_BuildValue("[isi]", i.score, (icon_str + i.name).c_str(), + static_cast(i.is_me)); + PyList_Append(py_list, obj); + Py_DECREF(obj); + } + PythonRef args(Py_BuildValue("(O)", py_list), PythonRef::kSteal); + Py_DECREF(py_list); + cb->Run(args); + } +} + +auto Python::HandleKeyPressEvent(const SDL_Keysym& keysym) -> bool { + assert(InGameThread()); + if (!keyboard_call_.exists()) { + return false; + } + ScopedSetContext cp(g_game->GetUIContextTarget()); + InputDevice* keyboard = g_input->keyboard_input(); + PythonRef args( + Py_BuildValue("({s:s,s:i,s:O})", "type", "BUTTONDOWN", "button", + static_cast(keysym.sym), "input_device", + keyboard ? keyboard->BorrowPyRef() : Py_None), + PythonRef::kSteal); + keyboard_call_.Call(args); + return true; +} +auto Python::HandleKeyReleaseEvent(const SDL_Keysym& keysym) -> bool { + assert(InGameThread()); + if (!keyboard_call_.exists()) { + return false; + } + ScopedSetContext cp(g_game->GetUIContextTarget()); + InputDevice* keyboard = g_input->keyboard_input(); + PythonRef args(Py_BuildValue("({s:s,s:i,s:O})", "type", "BUTTONUP", "button", + static_cast(keysym.sym), "input_device", + keyboard ? keyboard->BorrowPyRef() : Py_None), + PythonRef::kSteal); + keyboard_call_.Call(args); + return true; +} + +auto Python::HandleJoystickEvent(const SDL_Event& event, + InputDevice* input_device) -> bool { + assert(InGameThread()); + assert(input_device != nullptr); + if (!game_pad_call_.exists()) { + return false; + } + ScopedSetContext cp(g_game->GetUIContextTarget()); + InputDevice* device{}; + + device = input_device; + + // If we got a device we can pass events. + if (device) { + switch (event.type) { + case SDL_JOYBUTTONDOWN: { + PythonRef args( + Py_BuildValue( + "({s:s,s:i,s:O})", "type", "BUTTONDOWN", "button", + static_cast(event.jbutton.button) + 1, // give them base-1 + "input_device", device->BorrowPyRef()), + PythonRef::kSteal); + game_pad_call_.Call(args); + break; + } + case SDL_JOYBUTTONUP: { + PythonRef args( + Py_BuildValue( + "({s:s,s:i,s:O})", "type", "BUTTONUP", "button", + static_cast(event.jbutton.button) + 1, // give them base-1 + "input_device", device->BorrowPyRef()), + PythonRef::kSteal); + game_pad_call_.Call(args); + break; + } + case SDL_JOYHATMOTION: { + PythonRef args( + Py_BuildValue( + "({s:s,s:i,s:i,s:O})", "type", "HATMOTION", "hat", + static_cast(event.jhat.hat) + 1, // give them base-1 + "value", event.jhat.value, "input_device", + device->BorrowPyRef()), + PythonRef::kSteal); + game_pad_call_.Call(args); + break; + } + case SDL_JOYAXISMOTION: { + PythonRef args( + Py_BuildValue( + "({s:s,s:i,s:f,s:O})", "type", "AXISMOTION", "axis", + static_cast(event.jaxis.axis) + 1, // give them base-1 + "value", + std::min(1.0f, + std::max(-1.0f, static_cast(event.jaxis.value) + / 32767.0f)), + "input_device", device->BorrowPyRef()), + PythonRef::kSteal); + game_pad_call_.Call(args); + break; + } + default: + break; + } + } + return true; +} + +auto Python::GetContextBaseString() -> std::string { + std::string context_str; + std::string sim_time_string; + std::string base_time_string; + try { + sim_time_string = + std::to_string(Context::current().target->GetTime(TimeType::kSim)); + } catch (const std::exception&) { + sim_time_string = ""; + } + try { + base_time_string = + std::to_string(Context::current().target->GetTime(TimeType::kBase)); + } catch (const std::exception&) { + base_time_string = ""; + } + + if (Context::current().GetUIContext()) { + context_str = ""; + } else if (HostActivity* ha = Context::current().GetHostActivity()) { + // If its a HostActivity, print the Python obj. + PythonRef ha_obj(ha->GetPyActivity(), PythonRef::kAcquire); + if (ha_obj.get() != Py_None) { + context_str = ha_obj.Str(); + } else { + context_str = ha->GetObjectDescription(); + } + } else if (Context::current().target.exists()) { + context_str = Context::current().target->GetObjectDescription(); + } else { + context_str = ""; + } + std::string s = "\n context: " + context_str + "\n real-time: " + + std::to_string(GetRealTime()) + "\n sim-time: " + + sim_time_string + "\n base-time: " + base_time_string; + return s; +} + +void Python::LogContextForCallableLabel(const char* label) { + assert(InGameThread()); + assert(label); + std::string s = std::string(" root call: ") + label; + s += g_python->GetContextBaseString(); + Log(s); +} + +void Python::LogContextNonGameThread() { + std::string s = + std::string(" root call: "); + Log(s); +} + +void Python::LogContextEmpty() { + assert(InGameThread()); + std::string s = std::string(" root call: "); + s += g_python->GetContextBaseString(); + Log(s); +} + +void Python::LogContextAuto() { + // Lets print whatever context info is available. + // FIXME: If we have recursive calls this may not print + // the context we'd expect; we'd need a unified stack. + if (!InGameThread()) { + LogContextNonGameThread(); + } else if (const char* label = ScopedCallLabel::current_label()) { + LogContextForCallableLabel(label); + } else if (PythonCommand* cmd = PythonCommand::current_command()) { + cmd->LogContext(); + } else if (PythonContextCall* call = PythonContextCall::current_call()) { + call->LogContext(); + } else { + LogContextEmpty(); + } +} + +void Python::AcquireGIL() { + if (thread_state_) { + PyEval_RestoreThread(thread_state_); + thread_state_ = nullptr; + } +} +void Python::ReleaseGIL() { + assert(thread_state_ == nullptr); + thread_state_ = PyEval_SaveThread(); +} + +void Python::AddCleanFrameCommand(const Object::Ref& c) { + clean_frame_commands_.push_back(c); +} + +void Python::RunCleanFrameCommands() { + for (auto&& i : clean_frame_commands_) { + i->Run(); + } + clean_frame_commands_.clear(); +} + +auto Python::GetControllerValue(InputDevice* input_device, + const std::string& value_name) -> int { + assert(objexists(ObjID::kGetDeviceValueCall)); + PythonRef args( + Py_BuildValue("(Os)", input_device->BorrowPyRef(), value_name.c_str()), + PythonRef::kSteal); + PythonRef ret_val; + { + Python::ScopedCallLabel label("get_device_value"); + ret_val = obj(ObjID::kGetDeviceValueCall).Call(args); + } + if (!PyLong_Check(ret_val.get())) { + throw Exception("Non-int returned from get_device_value call.", + PyExcType::kType); + } + return static_cast(PyLong_AsLong(ret_val.get())); +} + +auto Python::GetControllerFloatValue(InputDevice* input_device, + const std::string& value_name) -> float { + assert(objexists(ObjID::kGetDeviceValueCall)); + PythonRef args( + Py_BuildValue("(Os)", input_device->BorrowPyRef(), value_name.c_str()), + PythonRef::kSteal); + PythonRef ret_val = obj(ObjID::kGetDeviceValueCall).Call(args); + if (!PyFloat_Check(ret_val.get())) { + if (PyLong_Check(ret_val.get())) { + return static_cast(PyLong_AsLong(ret_val.get())); + } else { + throw Exception( + "Non float/int returned from GetControllerFloatValue call.", + PyExcType::kType); + } + } + return static_cast(PyFloat_AsDouble(ret_val.get())); +} + +void Python::HandleDeviceMenuPress(InputDevice* input_device) { + assert(objexists(ObjID::kDeviceMenuPressCall)); + + // Ignore if input is locked... + if (g_input->IsInputLocked()) { + return; + } + ScopedSetContext cp(g_game->GetUIContext()); + PythonRef args(Py_BuildValue("(O)", input_device ? input_device->BorrowPyRef() + : Py_None), + PythonRef::kSteal); + { + Python::ScopedCallLabel label("handleDeviceMenuPress"); + obj(ObjID::kDeviceMenuPressCall).Call(args); + } +} + +auto Python::GetLastPlayerNameFromInputDevice(InputDevice* device) + -> std::string { + assert(objexists(ObjID::kGetLastPlayerNameFromInputDeviceCall)); + PythonRef args(Py_BuildValue("(O)", device ? device->BorrowPyRef() : Py_None), + PythonRef::kSteal); + try { + return Python::GetPyString( + obj(ObjID::kGetLastPlayerNameFromInputDeviceCall).Call(args).get()); + } catch (const std::exception&) { + return ""; + } +} + +auto Python::ObjToString(PyObject* obj) -> std::string { + if (obj) { + return PythonRef(obj, PythonRef::kAcquire).Str(); + } else { + return ""; + } +} + +void Python::PartyInvite(const std::string& player, + const std::string& invite_id) { + ScopedSetContext cp(g_game->GetUIContext()); + PythonRef args( + Py_BuildValue( + "(OO)", + PythonRef(PyUnicode_FromString(player.c_str()), PythonRef::kSteal) + .get(), + PythonRef(PyUnicode_FromString(invite_id.c_str()), PythonRef::kSteal) + .get()), + PythonRef::kSteal); + obj(ObjID::kHandlePartyInviteCall).Call(args); +} + +void Python::PartyInviteRevoke(const std::string& invite_id) { + ScopedSetContext cp(g_game->GetUIContext()); + PythonRef args( + Py_BuildValue("(O)", PythonRef(PyUnicode_FromString(invite_id.c_str()), + PythonRef::kSteal) + .get()), + PythonRef::kSteal); + obj(ObjID::kHandlePartyInviteRevokeCall).Call(args); +} + +void Python::SetObj(ObjID id, PyObject* pyobj, bool incref) { + assert(id < ObjID::kLast); + assert(pyobj); + if (g_buildconfig.debug_build()) { + // Assuming we're setting everything once + // (make sure we don't accidentally overwrite things we don't intend to). + if (objs_[static_cast(id)].exists()) { + throw Exception("Python::SetObj() called twice for val '" + + std::to_string(static_cast(id)) + "'."); + } + + // Also make sure we're not storing an object that's already been stored. + for (auto&& i : objs_) { + if (i.get() != nullptr && i.get() == pyobj) { + throw Exception("Python::SetObj() called twice for same ptr; id=" + + std::to_string(static_cast(id)) + "."); + } + } + } + if (incref) { + Py_INCREF(pyobj); + } + objs_[static_cast(id)].Steal(pyobj); +} + +void Python::SetObjCallable(ObjID id, PyObject* pyobj, bool incref) { + SetObj(id, pyobj, incref); + BA_PRECONDITION(obj(id).CallableCheck()); +} + +void Python::SetObj(ObjID id, const char* expr) { + PyObject* obj = PythonCommand(expr, "").RunReturnObj(); + if (obj == nullptr) { + throw Exception("Unable to get value: '" + std::string(expr) + "'."); + } + SetObj(id, obj); +} + +void Python::SetObjCallable(ObjID id, const char* expr) { + PyObject* obj = PythonCommand(expr, "").RunReturnObj(); + if (obj == nullptr) { + throw Exception("Unable to get value: '" + std::string(expr) + "'."); + } + SetObjCallable(id, obj); +} + +void Python::SetRawConfigValue(const char* name, float value) { + assert(InGameThread()); + assert(objexists(ObjID::kConfig)); + PythonRef value_obj(PyFloat_FromDouble(value), PythonRef::kSteal); + int result = + PyDict_SetItemString(obj(ObjID::kConfig).get(), name, value_obj.get()); + if (result == -1) { + // Failed, we have. + // Clear any Python error that got us here; we're in C++ Exception land now. + PyErr_Clear(); + throw Exception("Error setting config dict value."); + } +} + +auto Python::GetRawConfigValue(const char* name) -> PyObject* { + assert(InGameThread()); + assert(objexists(ObjID::kConfig)); + return PyDict_GetItemString(obj(ObjID::kConfig).get(), name); +} + +auto Python::GetRawConfigValue(const char* name, const char* default_value) + -> std::string { + assert(InGameThread()); + assert(objexists(ObjID::kConfig)); + PyObject* value = PyDict_GetItemString(obj(ObjID::kConfig).get(), name); + if (value == nullptr || !PyUnicode_Check(value)) { + return default_value; + } + return PyUnicode_AsUTF8(value); +} + +auto Python::GetRawConfigValue(const char* name, float default_value) -> float { + assert(InGameThread()); + assert(objexists(ObjID::kConfig)); + PyObject* value = PyDict_GetItemString(obj(ObjID::kConfig).get(), name); + if (value == nullptr) { + return default_value; + } + try { + return GetPyFloat(value); + } catch (const std::exception&) { + Log("expected a float for config value '" + std::string(name) + "'"); + return default_value; + } +} + +auto Python::GetRawConfigValue(const char* name, int default_value) -> int { + assert(InGameThread()); + assert(objexists(ObjID::kConfig)); + PyObject* value = PyDict_GetItemString(obj(ObjID::kConfig).get(), name); + if (value == nullptr) { + return default_value; + } + try { + return static_cast_check_fit(GetPyInt64(value)); + } catch (const std::exception&) { + Log("Expected an int value for config value '" + std::string(name) + "'."); + return default_value; + } +} + +auto Python::GetRawConfigValue(const char* name, bool default_value) -> bool { + assert(InGameThread()); + assert(objexists(ObjID::kConfig)); + PyObject* value = PyDict_GetItemString(obj(ObjID::kConfig).get(), name); + if (value == nullptr) { + return default_value; + } + try { + return GetPyBool(value); + } catch (const std::exception&) { + Log("Expected a bool value for config value '" + std::string(name) + "'."); + return default_value; + } +} + +auto Python::DoOnce() -> bool { + std::string location = GetPythonFileLocation(false); + if (do_once_locations_.find(location) != do_once_locations_.end()) { + return false; + } + do_once_locations_.insert(location); + return true; +} + +void Python::TimeFormatCheck(TimeFormat time_format, PyObject* length_obj) { + std::string warn_msg; + double length = Python::GetPyDouble(length_obj); + if (time_format == TimeFormat::kSeconds) { + // If we get a value more than a few hundred seconds, they might + // have meant milliseconds. + if (length >= 200.0) { + static bool warned = false; + if (!warned) { + Log("Warning: time value " + +std::to_string(length)+" passed as seconds;" + " did you mean milliseconds?" + " (if so, pass suppress_format_warning=True to stop this warning)"); + PrintStackTrace(); + warned = true; + } + } + } else if (time_format == TimeFormat::kMilliseconds) { + // If we get a value less than 1 millisecond, they might have meant + // seconds. (also ignore 0 which could be valid) + if (length < 1.0 && length > 0.0000001) { + static bool warned = false; + if (!warned) { + Log("Warning: time value " + + std::to_string(length) + " passed as milliseconds;" + " did you mean seconds?" + " (if so, pass suppress_format_warning=True to stop this warning)"); + PrintStackTrace(); + warned = true; + } + } + } else { + static bool warned = false; + if (!warned) { + BA_LOG_ONCE("TimeFormatCheck got timeformat value: '" + + std::to_string(static_cast(time_format)) + "'"); + warned = true; + } + } +} + +auto Python::ValidatedPackageAssetName(PyObject* package, const char* name) + -> std::string { + assert(InGameThread()); + assert(objexists(ObjID::kAssetPackageClass)); + + if (!PyObject_IsInstance(package, obj(ObjID::kAssetPackageClass).get())) { + throw Exception("Object is not an AssetPackage.", PyExcType::kType); + } + + // Ok; they've passed us an asset-package object. + // Now validate that its context is current... + PythonRef context_obj(PyObject_GetAttrString(package, "context"), + PythonRef::kSteal); + if (!context_obj.exists() + || !(PyObject_IsInstance( + context_obj.get(), + reinterpret_cast(&PythonClassContext::type_obj)))) { + throw Exception("Asset package context not found.", PyExcType::kNotFound); + } + auto* pycontext = reinterpret_cast(context_obj.get()); + Object::WeakRef ctargetref = pycontext->context().target; + if (!ctargetref.exists()) { + throw Exception("Asset package context does not exist.", + PyExcType::kNotFound); + } + Object::WeakRef ctargetref2 = Context::current().target; + if (ctargetref.get() != ctargetref2.get()) { + throw Exception("Asset package context is not current."); + } + + // Hooray; the asset package's context exists and is current. + // Ok; now pull the package id... + PythonRef package_id(PyObject_GetAttrString(package, "package_id"), + PythonRef::kSteal); + if (!PyUnicode_Check(package_id.get())) { + throw Exception("Got non-string AssetPackage ID.", PyExcType::kType); + } + + // TODO(ericf): make sure the package is valid for this context, + // and return a fully qualified name with the package included. + + printf("would give %s:%s\n", PyUnicode_AsUTF8(package_id.get()), name); + return name; +} + +class Python::ScopedInterpreterLock::Impl { + public: + Impl() : need_lock_(true), gstate_(PyGILState_UNLOCKED) { + if (need_lock_) { + if (need_lock_) { + // Grab the python GIL. + gstate_ = PyGILState_Ensure(); + } + } + } + ~Impl() { + if (need_lock_) { + // Release the python GIL. + PyGILState_Release(gstate_); + } + } + + private: + bool need_lock_ = false; + PyGILState_STATE gstate_; +}; + +Python::ScopedInterpreterLock::ScopedInterpreterLock() + : impl_(new Python::ScopedInterpreterLock::Impl()) {} + +Python::ScopedInterpreterLock::~ScopedInterpreterLock() { delete impl_; } + +template +auto IsPyEnum(Python::ObjID enum_class_id, PyObject* obj) -> bool { + PyObject* enum_class_obj = g_python->obj(enum_class_id).get(); + assert(enum_class_obj != nullptr && enum_class_obj != Py_None); + return static_cast(PyObject_IsInstance(obj, enum_class_obj)); +} + +template +auto GetPyEnum(Python::ObjID enum_class_id, PyObject* obj) -> T { + // First, make sure what they passed is an instance of the enum class + // we want. + PyObject* enum_class_obj = g_python->obj(enum_class_id).get(); + assert(enum_class_obj != nullptr && enum_class_obj != Py_None); + if (!PyObject_IsInstance(obj, enum_class_obj)) { + throw Exception(Python::ObjToString(obj) + " is not an instance of " + + Python::ObjToString(enum_class_obj) + ".", + PyExcType::kType); + } + + // Now get its value as an int and make sure its in range + // (based on its kLast member in C++ land). + PythonRef value_obj(PyObject_GetAttrString(obj, "value"), PythonRef::kSteal); + if (!value_obj.exists() || !PyLong_Check(value_obj.get())) { + throw Exception( + Python::ObjToString(obj) + " is not a valid int-valued enum.", + PyExcType::kType); + } + auto value = PyLong_AS_LONG(value_obj.get()); + if (value < 0 || value >= static_cast(T::kLast)) { + throw Exception( + Python::ObjToString(obj) + " is an invalid out-of-range enum value.", + PyExcType::kValue); + } + return static_cast(value); +} + +// Explicitly instantiate the few variations we use. +// (so we can avoid putting the full function in the header) +// template TimeFormat Python::GetPyEnum(Python::ObjID enum_class, PyObject* +// obj); template TimeType Python::GetPyEnum(Python::ObjID enum_class, PyObject* +// obj); template SpecialChar Python::GetPyEnum(Python::ObjID enum_class, +// PyObject* obj); template Permission Python::GetPyEnum(Python::ObjID +// enum_class, PyObject* obj); + +auto Python::GetPyEnum_Permission(PyObject* obj) -> Permission { + return GetPyEnum(Python::ObjID::kPermissionClass, obj); +} + +auto Python::GetPyEnum_SpecialChar(PyObject* obj) -> SpecialChar { + return GetPyEnum(Python::ObjID::kSpecialCharClass, obj); +} + +auto Python::GetPyEnum_TimeType(PyObject* obj) -> TimeType { + return GetPyEnum(Python::ObjID::kTimeTypeClass, obj); +} + +auto Python::GetPyEnum_TimeFormat(PyObject* obj) -> TimeFormat { + return GetPyEnum(Python::ObjID::kTimeFormatClass, obj); +} + +auto Python::IsPyEnum_InputType(PyObject* obj) -> bool { + return IsPyEnum(Python::ObjID::kInputTypeClass, obj); +} + +auto Python::GetPyEnum_InputType(PyObject* obj) -> InputType { + return GetPyEnum(Python::ObjID::kInputTypeClass, obj); +} + +// (some stuff borrowed from python's source code - used in our overriding of +// objects' dir() results) + +/* alphabetical order */ +_Py_IDENTIFIER(__class__); +_Py_IDENTIFIER(__dict__); + +/* ------------------------- PyObject_Dir() helpers ------------------------- */ + +/* + Merge the __dict__ of aclass into dict, and recursively also all + the __dict__s of aclass's base classes. The order of merging isn't + defined, as it's expected that only the final set of dict keys is + interesting. + Return 0 on success, -1 on error. + */ + +static auto merge_class_dict(PyObject* dict, PyObject* aclass) -> int { + PyObject* classdict; + PyObject* bases; + _Py_IDENTIFIER(__bases__); + + assert(PyDict_Check(dict)); + assert(aclass); + + /* Merge in the type's dict (if any). */ + classdict = _PyObject_GetAttrId(aclass, &PyId___dict__); + if (classdict == nullptr) { + PyErr_Clear(); + } else { + int status = PyDict_Update(dict, classdict); + Py_DECREF(classdict); + if (status < 0) return -1; + } + + /* Recursively merge in the base types' (if any) dicts. */ + bases = _PyObject_GetAttrId(aclass, &PyId___bases__); + if (bases == nullptr) { + PyErr_Clear(); + } else { + /* We have no guarantee that bases is a real tuple */ + Py_ssize_t i; + Py_ssize_t n; + n = PySequence_Size(bases); /* This better be right */ + if (n < 0) { + PyErr_Clear(); + } else { + for (i = 0; i < n; i++) { + int status; + PyObject* base = PySequence_GetItem(bases, i); + if (base == nullptr) { + Py_DECREF(bases); + return -1; + } + status = merge_class_dict(dict, base); + Py_DECREF(base); + if (status < 0) { + Py_DECREF(bases); + return -1; + } + } + } + Py_DECREF(bases); + } + return 0; +} + +/* __dir__ for generic objects: returns __dict__, __class__, + and recursively up the __class__.__bases__ chain. + */ +auto Python::generic_dir(PyObject* self) -> PyObject* { + PyObject* result = nullptr; + PyObject* dict = nullptr; + PyObject* itsclass = nullptr; + + /* Get __dict__ (which may or may not be a real dict...) */ + dict = _PyObject_GetAttrId(self, &PyId___dict__); + if (dict == nullptr) { + PyErr_Clear(); + dict = PyDict_New(); + } else if (!PyDict_Check(dict)) { + Py_DECREF(dict); + dict = PyDict_New(); + } else { + /* Copy __dict__ to avoid mutating it. */ + PyObject* temp = PyDict_Copy(dict); + Py_DECREF(dict); + dict = temp; + } + + if (dict == nullptr) goto error; + + /* Merge in attrs reachable from its class. */ + itsclass = _PyObject_GetAttrId(self, &PyId___class__); + if (itsclass == nullptr) + /* XXX(tomer): Perhaps fall back to obj->ob_type if no + __class__ exists? */ + PyErr_Clear(); + else if (merge_class_dict(dict, itsclass) != 0) + goto error; + + result = PyDict_Keys(dict); + /* fall through */ +error: + Py_XDECREF(itsclass); + Py_XDECREF(dict); + return result; +} +//////////////// end __dir__ helpers + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/generated_src/Makefile b/src/generated_src/Makefile index d18de50a..647a32a1 100644 --- a/src/generated_src/Makefile +++ b/src/generated_src/Makefile @@ -1,8 +1,18 @@ # Released under the MIT License. See LICENSE for details. +# +# This file was generated by tools/update_generated_code_makefile. -# Dummy generated-src makefile; nothing here for now. all: generated_code -generated_code: +generated_code: ../generated/ballistica/binding.inc \ + ../generated/ballistica/bootstrap.inc clean: + @rm -rf ../generated + +../generated/ballistica/bootstrap.inc : ballistica/bootstrap.py + @../../tools/pcommand gen_flat_data_code $< $@ bootstrap_code + +../generated/ballistica/binding.inc : ballistica/binding.py + @../../tools/pcommand gen_binding_code $< $@ + diff --git a/src/generated_src/ballistica/binding.py b/src/generated_src/ballistica/binding.py new file mode 100644 index 00000000..8fd60e69 --- /dev/null +++ b/src/generated_src/ballistica/binding.py @@ -0,0 +1,126 @@ +# Released under the MIT License. See LICENSE for details. +# Where most of our python-c++ binding happens. +# Python objects should be added here along with their associated c++ enum. +# Run make update to update the project after editing this.. +# pylint: disable=missing-module-docstring, missing-function-docstring +# pylint: disable=line-too-long +def get_binding_values() -> object: + from ba import _hooks + import _ba + import json + import copy + import ba + from ba import _lang + from ba import _music + from ba import _input + from ba import _apputils + from ba import _account + from ba import _dependency + from ba import _enums + from ba import _player + # FIXME: There should be no bastd in here; + # should pull in bases from ba which get overridden by bastd (or other). + from bastd.ui.onscreenkeyboard import OnScreenKeyboardWindow + from bastd.ui import party + return ( + _ba.client_info_query_response, # kClientInfoQueryResponseCall + _hooks.reset_to_main_menu, # kResetToMainMenuCall + _hooks.set_config_fullscreen_on, # kSetConfigFullscreenOnCall + _hooks.set_config_fullscreen_off, # kSetConfigFullscreenOffCall + _hooks.not_signed_in_screen_message, # kNotSignedInScreenMessageCall + _hooks.connecting_to_party_message, # kConnectingToPartyMessageCall + _hooks.rejecting_invite_already_in_party_message, # kRejectingInviteAlreadyInPartyMessageCall + _hooks.connection_failed_message, # kConnectionFailedMessageCall + _hooks.temporarily_unavailable_message, # kTemporarilyUnavailableMessageCall + _hooks.in_progress_message, # kInProgressMessageCall + _hooks.error_message, # kErrorMessageCall + _hooks.purchase_not_valid_error, # kPurchaseNotValidErrorCall + _hooks.purchase_already_in_progress_error, # kPurchaseAlreadyInProgressErrorCall + _hooks.gear_vr_controller_warning, # kGearVRControllerWarningCall + _hooks.orientation_reset_cb_message, # kVROrientationResetCBMessageCall + _hooks.orientation_reset_message, # kVROrientationResetMessageCall + _hooks.on_app_resume, # kHandleAppResumeCall + _apputils.handle_log, # kHandleLogCall + _hooks.launch_main_menu_session, # kLaunchMainMenuSessionCall + _hooks.language_test_toggle, # kLanguageTestToggleCall + _hooks.award_in_control_achievement, # kAwardInControlAchievementCall + _hooks.award_dual_wielding_achievement, # kAwardDualWieldingAchievementCall + _apputils.print_corrupt_file_error, # kPrintCorruptFileErrorCall + _hooks.play_gong_sound, # kPlayGongSoundCall + _hooks.launch_coop_game, # kLaunchCoopGameCall + _hooks.purchases_restored_message, # kPurchasesRestoredMessageCall + _hooks.dismiss_wii_remotes_window, # kDismissWiiRemotesWindowCall + _hooks.unavailable_message, # kUnavailableMessageCall + _hooks.submit_analytics_counts, # kSubmitAnalyticsCountsCall + _hooks.set_last_ad_network, # kSetLastAdNetworkCall + _hooks.no_game_circle_message, # kNoGameCircleMessageCall + _hooks.empty_call, # kEmptyCall + _hooks.level_icon_press, # kLevelIconPressCall + _hooks.trophy_icon_press, # kTrophyIconPressCall + _hooks.coin_icon_press, # kCoinIconPressCall + _hooks.ticket_icon_press, # kTicketIconPressCall + _hooks.back_button_press, # kBackButtonPressCall + _hooks.friends_button_press, # kFriendsButtonPressCall + _hooks.print_trace, # kPrintTraceCall + _hooks.toggle_fullscreen, # kToggleFullscreenCall + _hooks.party_icon_activate, # kPartyIconActivateCall + _hooks.read_config, # kReadConfigCall + _hooks.ui_remote_press, # kUIRemotePressCall + _hooks.quit_window, # kQuitWindowCall + _hooks.remove_in_game_ads_message, # kRemoveInGameAdsMessageCall + _hooks.telnet_access_request, # kTelnetAccessRequestCall + _hooks.on_app_pause, # kOnAppPauseCall + _hooks.do_quit, # kQuitCall + _hooks.shutdown, # kShutdownCall + _hooks.gc_disable, # kGCDisableCall + _account.show_post_purchase_message, # kShowPostPurchaseMessageCall + _hooks.device_menu_press, # kDeviceMenuPressCall + _hooks.show_url_window, # kShowURLWindowCall + _hooks.party_invite_revoke, # kHandlePartyInviteRevokeCall + _hooks.filter_chat_message, # kFilterChatMessageCall + _hooks.local_chat_message, # kHandleLocalChatMessageCall + ba.ShouldShatterMessage, # kShouldShatterMessageClass + ba.ImpactDamageMessage, # kImpactDamageMessageClass + ba.PickedUpMessage, # kPickedUpMessageClass + ba.DroppedMessage, # kDroppedMessageClass + ba.OutOfBoundsMessage, # kOutOfBoundsMessageClass + ba.PickUpMessage, # kPickUpMessageClass + ba.DropMessage, # kDropMessageClass + ba.app.on_app_launch, # kOnAppLaunchCall + _input.get_device_value, # kGetDeviceValueCall + _input.get_last_player_name_from_input_device, # kGetLastPlayerNameFromInputDeviceCall + copy.deepcopy, # kDeepCopyCall + copy.copy, # kShallowCopyCall + ba.Activity, # kActivityClass + ba.Session, # kSessionClass + json.dumps, # kJsonDumpsCall + json.loads, # kJsonLoadsCall + OnScreenKeyboardWindow, # kOnScreenKeyboardClass + party.handle_party_invite, # kHandlePartyInviteCall + _music.do_play_music, # kDoPlayMusicCall + ba.app.handle_deep_link, # kDeepLinkCall + _lang.get_resource, # kGetResourceCall + _lang.translate, # kTranslateCall + ba.Lstr, # kLStrClass + ba.Call, # kCallClass + _apputils.garbage_collect, # kGarbageCollectCall + ba.ContextError, # kContextError + ba.NotFoundError, # kNotFoundError + ba.NodeNotFoundError, # kNodeNotFoundError + ba.SessionTeamNotFoundError, # kSessionTeamNotFoundError + ba.InputDeviceNotFoundError, # kInputDeviceNotFoundError + ba.DelegateNotFoundError, # kDelegateNotFoundError + ba.SessionPlayerNotFoundError, # kSessionPlayerNotFoundError + ba.WidgetNotFoundError, # kWidgetNotFoundError + ba.ActivityNotFoundError, # kActivityNotFoundError + ba.SessionNotFoundError, # kSessionNotFoundError + _dependency.AssetPackage, # kAssetPackageClass + _enums.TimeFormat, # kTimeFormatClass + _enums.TimeType, # kTimeTypeClass + _enums.InputType, # kInputTypeClass + _enums.Permission, # kPermissionClass + _enums.SpecialChar, # kSpecialCharClass + _player.Player, # kPlayerClass + _hooks.get_player_icon, # kGetPlayerIconCall + _lang.Lstr.from_json, # kLstrFromJsonCall + ) # yapf: disable diff --git a/src/generated_src/ballistica/bootstrap.py b/src/generated_src/ballistica/bootstrap.py new file mode 100644 index 00000000..babfd660 --- /dev/null +++ b/src/generated_src/ballistica/bootstrap.py @@ -0,0 +1,147 @@ +# Released under the MIT License. See LICENSE for details. +"""Initial ba bootstrapping.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, TextIO, Callable, List + + +def _ballistica_bootstrap() -> object: + import sys + import signal + import _ba + import threading + + class _BAConsoleRedirect: + + def __init__(self, original: TextIO, call: Callable[[str], + None]) -> None: + self._lock = threading.Lock() + self._linebits: List[str] = [] + self._original = original + self._call = call + self._pending_ship = False + + def write(self, sval: Any) -> None: + """Override standard stdout write.""" + + self._call(sval) + + # Now do logging: + # Add it to our accumulated line. + # If the message ends in a newline, we can ship it + # immediately as a log entry. Otherwise, schedule a ship + # next cycle (if it hasn't yet at that point) so that we + # can accumulate subsequent prints. + # (so stuff like print('foo', 123, 'bar') will ship as one entry) + with self._lock: + self._linebits.append(sval) + if sval.endswith('\n'): + self._shiplog() + else: + _ba.pushcall(self._shiplog, + from_other_thread=True, + suppress_other_thread_warning=True) + + def _shiplog(self) -> None: + with self._lock: + line = ''.join(self._linebits) + if not line: + return + self._linebits = [] + + # Log messages aren't expected to have trailing newlines. + if line.endswith('\n'): + line = line[:-1] + _ba.log(line, to_stdout=False) + + def flush(self) -> None: + """Flush the file.""" + self._original.flush() + + def isatty(self) -> bool: + """Are we a terminal?""" + return self._original.isatty() + + sys.stdout = _BAConsoleRedirect( # type: ignore + sys.stdout, _ba.print_stdout) + sys.stderr = _BAConsoleRedirect( # type: ignore + sys.stderr, _ba.print_stderr) + + # Let's lookup mods first (so users can do whatever they want). + # and then our bundled scripts last (don't want to encourage dists + # overriding proper python functionality) + sys.path.insert(0, _ba.env()['python_directory_user']) + sys.path.append(_ba.env()['python_directory_app']) + sys.path.append(_ba.env()['python_directory_app_site']) + + # Tell Python to not handle SIGINT itself (it normally generates + # KeyboardInterrupts which make a mess; we want to do a simple + # clean exit). We capture interrupts per-platform in the C++ layer. + # I tried creating a handler in Python but it seemed to often have + # a delay of up to a second before getting called. (not a huge deal + # but I'm picky). + signal.signal(signal.SIGINT, signal.SIG_DFL) # Do default handling. + + # ..though it turns out we need to set up our C signal handling AFTER + # we've told Python to disable its own; otherwise (on Mac at least) it + # wipes out our existing C handler. + _ba.setup_sigint() + + # Sanity check: we should always be run in UTF-8 mode. + if sys.flags.utf8_mode != 1: + print('ERROR: Python\'s UTF-8 mode is not set.' + ' This will likely result in errors.') + + # FIXME: I think we should init Python in the main thread, which should + # also avoid these issues. (and also might help us play better with + # Python debuggers?) + + # Gloriously hacky workaround here: + # Our 'main' Python thread is the game thread (not the app's main + # thread) which means it has a small stack compared to the main + # thread (at least on apple). Sadly it turns out this causes the + # debug build of Python to blow its stack immediately when doing + # some big imports. + # Normally we'd just give the game thread the same stack size as + # the main thread and that'd be the end of it. However + # we're using std::threads which it turns out have no way to set + # the stack size (as of fall '19). Grumble. + # + # However python threads *can* take custom stack sizes. + # (and it appears they might use the main thread's by default?..) + # ...so as a workaround in the debug version, we can run problematic + # heavy imports here in another thread and all is well. + # If we ever see stack overflows in our release build we'll have + # to take more drastic measures like switching from std::threads + # to pthreads. + + if _ba.env()['debug_build']: + + def _thread_func() -> None: + # pylint: disable=unused-import + import json + import urllib.request + + testthread = threading.Thread(target=_thread_func) + testthread.start() + testthread.join() + + # Now spin up our App instance, store it on both _ba and ba, + # and return it to the C++ layer. + # noinspection PyProtectedMember + from ba._app import App + import ba + app = App() + _ba.app = app + ba.app = app + return app + + +_app_state = _ballistica_bootstrap() + +# This code runs in the main namespace, so clean up after ourself. +del _ballistica_bootstrap, TYPE_CHECKING diff --git a/tools/batools/build.py b/tools/batools/build.py index 70a21fd5..82d97792 100644 --- a/tools/batools/build.py +++ b/tools/batools/build.py @@ -16,7 +16,7 @@ from efro.error import CleanError from efro.terminal import Clr if TYPE_CHECKING: - from typing import List, Sequence, Optional, Any + from typing import List, Sequence, Optional, Any, Dict # Python pip packages we require for this project. @@ -710,3 +710,61 @@ def update_docs_md(check: bool) -> None: with open(docs_hash_path, 'w') as outfile: outfile.write(curhash) print(f'{docs_path} is up to date.') + + +def cmake_prep_dir(dirname: str) -> None: + """Create a dir, recreating it when cmake/python/etc. version changes. + + Useful to prevent builds from breaking when cmake or other components + are updated. + """ + import json + from efrotools import PYVER + verfilename = os.path.join(dirname, '.ba_cmake_env') + + versions: Dict[str, str] + if os.path.isfile(verfilename): + with open(verfilename) as infile: + versions = json.loads(infile.read()) + assert isinstance(versions, dict) + else: + versions = {} + + # Get version of installed cmake. + cmake_ver_output = subprocess.run(['cmake', '--version'], + check=True, + capture_output=True).stdout.decode() + cmake_ver = cmake_ver_output.splitlines()[0].split('cmake version ')[1] + + cmake_ver_existing = versions.get('cmake') + assert isinstance(cmake_ver_existing, (str, type(None))) + + # Get specific version of our target python. + python_ver_output = subprocess.run([f'python{PYVER}', '--version'], + check=True, + capture_output=True).stdout.decode() + python_ver = python_ver_output.splitlines()[0].split('Python ')[1] + + python_ver_existing = versions.get('python') + assert isinstance(python_ver_existing, (str, type(None))) + + # If they don't match, blow away the dir and write the current version. + if cmake_ver_existing != cmake_ver or python_ver_existing != python_ver: + if (cmake_ver_existing != cmake_ver + and cmake_ver_existing is not None): + print(f'{Clr.BLU}CMake version changed from {cmake_ver_existing}' + f' to {cmake_ver}; clearing existing build at' + f' "{dirname}".{Clr.RST}') + if (python_ver_existing != python_ver + and python_ver_existing is not None): + print(f'{Clr.BLU}Python version changed from {python_ver_existing}' + f' to {python_ver}; clearing existing build at' + f' "{dirname}".{Clr.RST}') + subprocess.run(['rm', '-rf', dirname], check=True) + os.makedirs(dirname, exist_ok=True) + with open(verfilename, 'w') as outfile: + outfile.write( + json.dumps({ + 'cmake': cmake_ver, + 'python': python_ver + })) diff --git a/tools/batools/codegen.py b/tools/batools/codegen.py new file mode 100644 index 00000000..503394fd --- /dev/null +++ b/tools/batools/codegen.py @@ -0,0 +1,109 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Functionality related to code generation.""" +from __future__ import annotations + +import os +import json +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import List, Sequence, Optional, Any, Dict + + +def gen_flat_data_code(projroot: str, in_path: str, out_path: str, + var_name: str) -> None: + """Generate a C++ include file from a Python file.""" + + out_dir = os.path.dirname(out_path) + if not os.path.exists(out_dir): + os.makedirs(out_dir, exist_ok=True) + + with open(in_path, 'rb') as infileb: + svalin = infileb.read() + + # JSON should do the trick for us here as far as char escaping/etc. + # There's corner cases where it can differ from C strings but in this + # simple case we shouldn't run into them. + sval_out = f'const char* {var_name} =' + + # Store in ballistica's simple xor encryption to at least + # slightly slow down hackers. + sval = svalin + + sval1: Optional[bytes] + sval1 = sval + while sval1: + sval_out += ' ' + json.dumps(sval1[:1000].decode()) + sval1 = sval1[1000:] + sval_out += ';\n' + + pretty_path = os.path.abspath(out_path) + if pretty_path.startswith(projroot + '/'): + pretty_path = pretty_path[len(projroot) + 1:] + print(f'Generating code: {pretty_path}') + with open(out_path, 'w') as outfile: + outfile.write(sval_out) + + +def gen_binding_code(projroot: str, in_path: str, out_path: str) -> None: + """Generate binding.inc file.""" + + out_dir = os.path.dirname(out_path) + if not os.path.exists(out_dir): + os.makedirs(out_dir, exist_ok=True) + + # Pull all lines in the embedded list and split into py and c++ names. + with open(in_path) as infile: + pycode = infile.read() + + # Double quotes causes errors. + if '"' in pycode: + raise Exception('bindings file can\'t contain double quotes.') + lines = [ + l.strip().split(', # ') for l in pycode.splitlines() + if l.startswith(' ') + ] + if not all(len(l) == 2 for l in lines): + raise Exception('malformatted data') + + # Our C++ code first execs our input as a string. + ccode = ('{const char* bindcode = ' + repr(pycode).replace("'", '"') + ';') + ccode += ('\nint result = PyRun_SimpleString(bindcode);\n' + 'if (result != 0) {\n' + ' PyErr_PrintEx(0);\n' + ' // Use a standard error to avoid a useless stack trace.\n' + ' throw std::logic_error("Error fetching required Python' + ' objects.");\n' + '}\n') + + # Then it grabs the function that was defined and runs it. + ccode += ('PyObject* bindvals = PythonCommand("get_binding_values()",' + ' "")' + '.RunReturnObj(true);\n' + 'if (bindvals == nullptr) {\n' + ' // Use a standard error to avoid a useless stack trace.\n' + ' throw std::logic_error("Error binding required Python' + ' objects.");\n' + '}\n') + + # Then it pulls the individual values out of the returned tuple. + for i, line in enumerate(lines): + ccode += ('SetObjCallable(ObjID::' + line[1] + + ', PyTuple_GET_ITEM(bindvals, ' + str(i) + '), true);\n') + + # Lastly it cleans up after itself. + ccode += ('result = PyRun_SimpleString("del get_binding_values");\n' + 'if (result != 0) {\n' + ' PyErr_PrintEx(0);\n' + ' // Use a standard error to avoid a useless stack trace.\n' + ' throw std::logic_error("Error cleaning up after Python' + ' binding.");\n' + '}\n' + '}\n') + pretty_path = os.path.abspath(out_path) + if pretty_path.startswith(projroot + '/'): + pretty_path = pretty_path[len(projroot) + 1:] + print('Generating code: ' + pretty_path) + with open(out_path, 'w') as outfile: + outfile.write(ccode) diff --git a/tools/batools/pcommand.py b/tools/batools/pcommand.py index e43d95ee..2b4c5ddc 100644 --- a/tools/batools/pcommand.py +++ b/tools/batools/pcommand.py @@ -731,63 +731,33 @@ def cmake_prep_dir() -> None: Useful to prevent builds from breaking when cmake or other components are updated. """ - # pylint: disable=too-many-locals - import os - import subprocess - import json from efro.error import CleanError - from efro.terminal import Clr - from efrotools import PYVER + import batools.build if len(sys.argv) != 3: raise CleanError('Expected 1 arg (dir name)') dirname = sys.argv[2] + batools.build.cmake_prep_dir(dirname) - verfilename = os.path.join(dirname, '.ba_cmake_env') - versions: Dict[str, str] - if os.path.isfile(verfilename): - with open(verfilename) as infile: - versions = json.loads(infile.read()) - assert isinstance(versions, dict) - else: - versions = {} +def gen_binding_code() -> None: + """Generate binding.inc file.""" + from efro.error import CleanError + import batools.codegen + if len(sys.argv) != 4: + raise CleanError('Expected 2 args (srcfile, dstfile)') + inpath = sys.argv[2] + outpath = sys.argv[3] + batools.codegen.gen_binding_code(str(PROJROOT), inpath, outpath) - # Get version of installed cmake. - cmake_ver_output = subprocess.run(['cmake', '--version'], - check=True, - capture_output=True).stdout.decode() - cmake_ver = cmake_ver_output.splitlines()[0].split('cmake version ')[1] - cmake_ver_existing = versions.get('cmake') - assert isinstance(cmake_ver_existing, (str, type(None))) - - # Get specific version of our target python. - python_ver_output = subprocess.run([f'python{PYVER}', '--version'], - check=True, - capture_output=True).stdout.decode() - python_ver = python_ver_output.splitlines()[0].split('Python ')[1] - - python_ver_existing = versions.get('python') - assert isinstance(python_ver_existing, (str, type(None))) - - # If they don't match, blow away the dir and write the current version. - if cmake_ver_existing != cmake_ver or python_ver_existing != python_ver: - if (cmake_ver_existing != cmake_ver - and cmake_ver_existing is not None): - print(f'{Clr.BLU}CMake version changed from {cmake_ver_existing}' - f' to {cmake_ver}; clearing existing build at' - f' "{dirname}".{Clr.RST}') - if (python_ver_existing != python_ver - and python_ver_existing is not None): - print(f'{Clr.BLU}Python version changed from {python_ver_existing}' - f' to {python_ver}; clearing existing build at' - f' "{dirname}".{Clr.RST}') - subprocess.run(['rm', '-rf', dirname], check=True) - os.makedirs(dirname, exist_ok=True) - with open(verfilename, 'w') as outfile: - outfile.write( - json.dumps({ - 'cmake': cmake_ver, - 'python': python_ver - })) +def gen_flat_data_code() -> None: + """Generate a C++ include file from a Python file.""" + from efro.error import CleanError + import batools.codegen + if len(sys.argv) != 5: + raise CleanError('Expected 3 args (srcfile, dstfile, varname)') + inpath = sys.argv[2] + outpath = sys.argv[3] + varname = sys.argv[4] + batools.codegen.gen_flat_data_code(str(PROJROOT), inpath, outpath, varname) diff --git a/tools/pcommand b/tools/pcommand index 82e80a53..779df934 100755 --- a/tools/pcommand +++ b/tools/pcommand @@ -38,7 +38,7 @@ from batools.pcommand import ( prefab_run_var, make_prefab, update_makebob, lazybuild, android_archive_unstripped_libs, efro_gradle, stage_assets, update_assets_makefile, update_project, update_cmake_prefab_lib, - cmake_prep_dir) + cmake_prep_dir, gen_binding_code, gen_flat_data_code) # pylint: enable=unused-import if TYPE_CHECKING: