From 64b008c30415d42cd673797d40e5290e7aceaff4 Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Fri, 9 Oct 2020 17:10:21 -0700 Subject: [PATCH] Bringing over more c++ stuff --- .efrocachemap | 20 +- .idea/dictionaries/ericf.xml | 14 +- assets/src/ba_data/python/ba/_coopgame.py | 3 +- .../ba_data/python/ba/_freeforallsession.py | 6 +- assets/src/ba_data/python/ba/_gameresults.py | 4 +- assets/src/ba_data/python/ba/_hooks.py | 5 - assets/src/ba_data/python/ba/_store.py | 5 +- .../ba_data/python/bastd/activity/coopjoin.py | 4 +- .../python/bastd/activity/coopscore.py | 3 +- .../python/bastd/ui/playlist/browser.py | 3 +- .../bastd/ui/playlist/customizebrowser.py | 3 +- .../python/bastd/ui/profile/browser.py | 3 +- .../python/bastd/ui/soundtrack/browser.py | 3 +- docs/ba_module.md | 2 +- src/ballistica/ballistica.cc | 2 +- src/ballistica/ballistica.h | 27 +- src/ballistica/game/game.h | 31 +- src/ballistica/generic/utils.cc | 124 - src/ballistica/generic/utils.h | 22 - src/ballistica/input/device/input_device.cc | 10 +- src/ballistica/python/class/python_class.cc | 69 + src/ballistica/python/class/python_class.h | 28 + .../class/python_class_activity_data.cc | 183 ++ .../python/class/python_class_activity_data.h | 39 + .../class/python_class_collide_model.cc | 110 + .../python/class/python_class_collide_model.h | 34 + .../python/class/python_class_context.cc | 227 ++ .../python/class/python_class_context.h | 37 + .../python/class/python_class_context_call.cc | 130 + .../python/class/python_class_context_call.h | 33 + .../python/class/python_class_data.cc | 139 + .../python/class/python_class_data.h | 36 + .../python/class/python_class_input_device.cc | 446 +++ .../python/class/python_class_input_device.h | 52 + .../python/class/python_class_material.cc | 712 +++++ .../python/class/python_class_material.h | 46 + .../python/class/python_class_model.cc | 109 + .../python/class/python_class_model.h | 34 + .../python/class/python_class_node.cc | 458 +++ .../python/class/python_class_node.h | 52 + .../python/class/python_class_session_data.cc | 114 + .../python/class/python_class_session_data.h | 36 + .../class/python_class_session_player.cc | 746 +++++ .../class/python_class_session_player.h | 62 + .../python/class/python_class_sound.cc | 108 + .../python/class/python_class_sound.h | 34 + .../python/class/python_class_texture.cc | 109 + .../python/class/python_class_texture.h | 34 + .../python/class/python_class_timer.cc | 195 ++ .../python/class/python_class_timer.h | 35 + .../python/class/python_class_vec3.cc | 350 +++ .../python/class/python_class_vec3.h | 49 + .../python/class/python_class_widget.cc | 288 ++ .../python/class/python_class_widget.h | 45 + .../python/methods/python_methods_app.cc | 1196 ++++++++ .../python/methods/python_methods_app.h | 17 + .../python/methods/python_methods_gameplay.cc | 765 +++++ .../python/methods/python_methods_gameplay.h | 17 + .../python/methods/python_methods_graphics.cc | 315 ++ .../python/methods/python_methods_graphics.h | 17 + .../python/methods/python_methods_input.cc | 394 +++ .../python/methods/python_methods_input.h | 18 + .../python/methods/python_methods_media.cc | 563 ++++ .../python/methods/python_methods_media.h | 18 + .../methods/python_methods_networking.cc | 610 ++++ .../methods/python_methods_networking.h | 18 + .../python/methods/python_methods_system.cc | 1031 +++++++ .../python/methods/python_methods_system.h | 18 + .../python/methods/python_methods_ui.cc | 2710 +++++++++++++++++ .../python/methods/python_methods_ui.h | 18 + src/ballistica/python/python.h | 420 +++ src/ballistica/python/python_command.cc | 202 ++ src/ballistica/python/python_command.h | 73 + src/ballistica/python/python_context_call.cc | 135 + src/ballistica/python/python_context_call.h | 54 + .../python/python_context_call_runnable.h | 25 + src/ballistica/python/python_ref.cc | 185 ++ src/ballistica/python/python_ref.h | 130 + src/ballistica/python/python_sys.h | 84 + tools/batools/build.py | 4 +- tools/efro/util.py | 42 + 81 files changed, 14338 insertions(+), 214 deletions(-) create mode 100644 src/ballistica/python/class/python_class.cc create mode 100644 src/ballistica/python/class/python_class.h create mode 100644 src/ballistica/python/class/python_class_activity_data.cc create mode 100644 src/ballistica/python/class/python_class_activity_data.h create mode 100644 src/ballistica/python/class/python_class_collide_model.cc create mode 100644 src/ballistica/python/class/python_class_collide_model.h create mode 100644 src/ballistica/python/class/python_class_context.cc create mode 100644 src/ballistica/python/class/python_class_context.h create mode 100644 src/ballistica/python/class/python_class_context_call.cc create mode 100644 src/ballistica/python/class/python_class_context_call.h create mode 100644 src/ballistica/python/class/python_class_data.cc create mode 100644 src/ballistica/python/class/python_class_data.h create mode 100644 src/ballistica/python/class/python_class_input_device.cc create mode 100644 src/ballistica/python/class/python_class_input_device.h create mode 100644 src/ballistica/python/class/python_class_material.cc create mode 100644 src/ballistica/python/class/python_class_material.h create mode 100644 src/ballistica/python/class/python_class_model.cc create mode 100644 src/ballistica/python/class/python_class_model.h create mode 100644 src/ballistica/python/class/python_class_node.cc create mode 100644 src/ballistica/python/class/python_class_node.h create mode 100644 src/ballistica/python/class/python_class_session_data.cc create mode 100644 src/ballistica/python/class/python_class_session_data.h create mode 100644 src/ballistica/python/class/python_class_session_player.cc create mode 100644 src/ballistica/python/class/python_class_session_player.h create mode 100644 src/ballistica/python/class/python_class_sound.cc create mode 100644 src/ballistica/python/class/python_class_sound.h create mode 100644 src/ballistica/python/class/python_class_texture.cc create mode 100644 src/ballistica/python/class/python_class_texture.h create mode 100644 src/ballistica/python/class/python_class_timer.cc create mode 100644 src/ballistica/python/class/python_class_timer.h create mode 100644 src/ballistica/python/class/python_class_vec3.cc create mode 100644 src/ballistica/python/class/python_class_vec3.h create mode 100644 src/ballistica/python/class/python_class_widget.cc create mode 100644 src/ballistica/python/class/python_class_widget.h create mode 100644 src/ballistica/python/methods/python_methods_app.cc create mode 100644 src/ballistica/python/methods/python_methods_app.h create mode 100644 src/ballistica/python/methods/python_methods_gameplay.cc create mode 100644 src/ballistica/python/methods/python_methods_gameplay.h create mode 100644 src/ballistica/python/methods/python_methods_graphics.cc create mode 100644 src/ballistica/python/methods/python_methods_graphics.h create mode 100644 src/ballistica/python/methods/python_methods_input.cc create mode 100644 src/ballistica/python/methods/python_methods_input.h create mode 100644 src/ballistica/python/methods/python_methods_media.cc create mode 100644 src/ballistica/python/methods/python_methods_media.h create mode 100644 src/ballistica/python/methods/python_methods_networking.cc create mode 100644 src/ballistica/python/methods/python_methods_networking.h create mode 100644 src/ballistica/python/methods/python_methods_system.cc create mode 100644 src/ballistica/python/methods/python_methods_system.h create mode 100644 src/ballistica/python/methods/python_methods_ui.cc create mode 100644 src/ballistica/python/methods/python_methods_ui.h create mode 100644 src/ballistica/python/python.h create mode 100644 src/ballistica/python/python_command.cc create mode 100644 src/ballistica/python/python_command.h create mode 100644 src/ballistica/python/python_context_call.cc create mode 100644 src/ballistica/python/python_context_call.h create mode 100644 src/ballistica/python/python_context_call_runnable.h create mode 100644 src/ballistica/python/python_ref.cc create mode 100644 src/ballistica/python/python_ref.h create mode 100644 src/ballistica/python/python_sys.h diff --git a/.efrocachemap b/.efrocachemap index b662c580..c3d10c8a 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -3934,14 +3934,14 @@ "assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f", "build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ac/96/c3b9934061393fe09cc90ff24b8d", "build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/38/2b/5641b3b40846f74f232771ac0457", - "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/6e/7e/56adde97a5cb545933bdd52700d9", - "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/3c/f9/d971d471660647f1eacb768f0d10", - "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ef/b7/aa17c70752baab2bd4ea970b7b2d", - "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/43/77/27920088a7fb8490a833623894a1", - "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/70/3a/36ff319dbed727b6bd148073e278", - "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/83/81/5d46cb2627d0ae1f0c59a9dd123a", - "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/3a/8f/502e7fef458bb05da2864f4724ea", - "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/60/38/2d0e9f0cf486bae30056f3d3c11a", - "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/37/a6/ae4e2bf9c60fc0cbfd66136dc344", - "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/02/f4/907cfc73510e071f9ab5ca914646" + "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/6b/43/efceea678ab45ebe36c72ff6fa79", + "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/15/d5/29d9b25931f5c91a7a0db8cd6260", + "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/bc/bf/286df9a4a78d01c5bd02bee224cd", + "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/24/d5/6f25f7ffbdcf3dde835ca8213544", + "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/9c/d1/f745c2663299168c982f752802d0", + "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/9b/dd/90e274f18a93c82e9c2c29a59b41", + "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/9d/9a/c14692e42e5a7376b665af6a8463", + "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/2a/c8/f661b157edda3920f8834124d24b", + "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/7e/ed/5db67414f8d9444f91631a448bc0", + "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/7d/9f/9ff4e4c2d64c3dfac362f2b5af15" } \ No newline at end of file diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index cc801a97..37848787 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -29,8 +29,8 @@ achname achs acinstance - ack ack'ed + ack acked acks acnt @@ -100,6 +100,7 @@ asdict aspx assertnode + asserttype assetbundle assetcache assetdata @@ -151,8 +152,8 @@ bacommon badguy bafoundation - ballistica ballistica's + ballistica ballisticacore ballisticacorecb bamaster @@ -313,6 +314,7 @@ checkpaths checkroundover checksums + checktype childnode chinesetraditional chipfork @@ -793,8 +795,8 @@ gamedata gameinstance gamemap - gamepad gamepad's + gamepad gamepadadvanced gamepads gamepadselect @@ -1177,8 +1179,8 @@ lsqlite lssl lstart - lstr lstr's + lstr lstrs lsval ltex @@ -1803,8 +1805,8 @@ sessionname sessionplayer sessionplayers - sessionteam sessionteam's + sessionteam sessionteams sessiontype setactivity @@ -2135,8 +2137,8 @@ txtw typeargs typecheck - typechecker typechecker's + typechecker typedval typeshed typestr diff --git a/assets/src/ba_data/python/ba/_coopgame.py b/assets/src/ba_data/python/ba/_coopgame.py index d04c2c69..f1a516f3 100644 --- a/assets/src/ba_data/python/ba/_coopgame.py +++ b/assets/src/ba_data/python/ba/_coopgame.py @@ -67,6 +67,7 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): def _show_standard_scores_to_beat_ui(self, scores: List[Dict[str, Any]]) -> None: + from efro.util import asserttype from ba._gameutils import timestring, animate from ba._nodeactor import NodeActor from ba._enums import TimeFormat @@ -74,7 +75,7 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): if scores is not None: # Sort by originating date so that the most recent is first. - scores.sort(reverse=True, key=lambda s: s['time']) + scores.sort(reverse=True, key=lambda s: asserttype(s['time'], int)) # Now make a display for the most recent challenge. for score in scores: diff --git a/assets/src/ba_data/python/ba/_freeforallsession.py b/assets/src/ba_data/python/ba/_freeforallsession.py index 73304cc5..e81eb47d 100644 --- a/assets/src/ba_data/python/ba/_freeforallsession.py +++ b/assets/src/ba_data/python/ba/_freeforallsession.py @@ -53,6 +53,7 @@ class FreeForAllSession(MultiTeamSession): def _switch_to_score_screen(self, results: ba.GameResults) -> None: # pylint: disable=cyclic-import + from efro.util import asserttype from bastd.activity.drawscore import DrawScoreScreenActivity from bastd.activity.multiteamvictory import ( TeamSeriesVictoryScoreScreenActivity) @@ -80,8 +81,9 @@ class FreeForAllSession(MultiTeamSession): team for team in self.sessionteams if team.customdata['score'] >= self._ffa_series_length ] - series_winners.sort(reverse=True, - key=lambda tm: (tm.customdata['score'])) + series_winners.sort( + reverse=True, + key=lambda t: asserttype(t.customdata['score'], int)) if (len(series_winners) == 1 or (len(series_winners) > 1 and series_winners[0].customdata['score'] != diff --git a/assets/src/ba_data/python/ba/_gameresults.py b/assets/src/ba_data/python/ba/_gameresults.py index 65bd3ef8..563c9527 100644 --- a/assets/src/ba_data/python/ba/_gameresults.py +++ b/assets/src/ba_data/python/ba/_gameresults.py @@ -8,6 +8,7 @@ import weakref from dataclasses import dataclass from typing import TYPE_CHECKING +from efro.util import asserttype from ba._team import Team, SessionTeam if TYPE_CHECKING: @@ -187,7 +188,8 @@ class GameResults: sval.append(team) results: List[Tuple[Optional[int], List[ba.SessionTeam]]] = list(winners.items()) - results.sort(reverse=not self._lower_is_better, key=lambda x: x[0]) + results.sort(reverse=not self._lower_is_better, + key=lambda x: asserttype(x[0], int)) # Also group the 'None' scores. none_sessionteams: List[ba.SessionTeam] = [] diff --git a/assets/src/ba_data/python/ba/_hooks.py b/assets/src/ba_data/python/ba/_hooks.py index 368d35ca..1365329a 100644 --- a/assets/src/ba_data/python/ba/_hooks.py +++ b/assets/src/ba_data/python/ba/_hooks.py @@ -328,11 +328,6 @@ def local_chat_message(msg: str) -> None: _ba.app.ui.party_window().on_chat_message(msg) -def handle_remote_achievement_list(completed_achievements: List[str]) -> None: - from ba import _achievement - _achievement.set_completed_achievements(completed_achievements) - - def get_player_icon(sessionplayer: ba.SessionPlayer) -> Dict[str, Any]: info = sessionplayer.get_icon_info() return { diff --git a/assets/src/ba_data/python/ba/_store.py b/assets/src/ba_data/python/ba/_store.py index 09252e47..9bf0ec38 100644 --- a/assets/src/ba_data/python/ba/_store.py +++ b/assets/src/ba_data/python/ba/_store.py @@ -500,8 +500,9 @@ def get_available_sale_time(tab: str) -> Optional[int]: if to_end > 0: sale_times.append(int(to_end * 1000)) - # Return the smallest time i guess? - return min(sale_times) if sale_times else None + # Return the smallest time I guess? + sale_times_int = [t for t in sale_times if isinstance(t, int)] + return min(sale_times_int) if sale_times_int else None except Exception: from ba import _error diff --git a/assets/src/ba_data/python/bastd/activity/coopjoin.py b/assets/src/ba_data/python/bastd/activity/coopjoin.py index 9e63747a..3a62e68b 100644 --- a/assets/src/ba_data/python/bastd/activity/coopjoin.py +++ b/assets/src/ba_data/python/bastd/activity/coopjoin.py @@ -57,12 +57,14 @@ class CoopJoinActivity(JoinActivity): scores: Optional[List[Dict[str, Any]]]) -> None: # pylint: disable=too-many-locals # pylint: disable=too-many-statements + from efro.util import asserttype from bastd.actor.text import Text from ba.internal import get_achievements_for_coop_level # Sort by originating date so that the most recent is first. if scores is not None: - scores.sort(reverse=True, key=lambda score: score['time']) + scores.sort(reverse=True, + key=lambda score: asserttype(score['time'], int)) # We only show achievements and challenges for CoopGameActivities. session = self.session diff --git a/assets/src/ba_data/python/bastd/activity/coopscore.py b/assets/src/ba_data/python/bastd/activity/coopscore.py index 15d1c9fe..a870f8f9 100644 --- a/assets/src/ba_data/python/bastd/activity/coopscore.py +++ b/assets/src/ba_data/python/bastd/activity/coopscore.py @@ -873,6 +873,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements + from efro.util import asserttype # delay a bit if results come in too fast assert self._begin_time is not None base_delay = max(0, 1.9 - (ba.time() - self._begin_time)) @@ -909,7 +910,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): break results.append(our_score_entry) results.sort(reverse=self._score_order == 'increasing', - key=lambda x: x[0]) + key=lambda x: asserttype(x[0], int)) # If we're not submitting our own score, we still want to change the # name of our own score to 'Me'. diff --git a/assets/src/ba_data/python/bastd/ui/playlist/browser.py b/assets/src/ba_data/python/bastd/ui/playlist/browser.py index af37ace9..284ff31f 100644 --- a/assets/src/ba_data/python/bastd/ui/playlist/browser.py +++ b/assets/src/ba_data/python/bastd/ui/playlist/browser.py @@ -284,6 +284,7 @@ class PlaylistBrowserWindow(ba.Window): # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-nested-blocks + from efro.util import asserttype from ba.internal import (get_map_class, get_default_free_for_all_playlist, get_default_teams_playlist, filter_playlist) @@ -303,7 +304,7 @@ class PlaylistBrowserWindow(ba.Window): items = [(i[0].decode(), i[1]) if not isinstance(i[0], str) else i for i in items] - items.sort(key=lambda x2: x2[0].lower()) + items.sort(key=lambda x2: asserttype(x2[0], str).lower()) items = [['__default__', None]] + items # default is always first count = len(items) diff --git a/assets/src/ba_data/python/bastd/ui/playlist/customizebrowser.py b/assets/src/ba_data/python/bastd/ui/playlist/customizebrowser.py index f743e412..8387c12a 100644 --- a/assets/src/ba_data/python/bastd/ui/playlist/customizebrowser.py +++ b/assets/src/ba_data/python/bastd/ui/playlist/customizebrowser.py @@ -299,6 +299,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): _ba.lock_all_input() def _refresh(self, select_playlist: str = None) -> None: + from efro.util import asserttype old_selection = self._selected_playlist_name # If there was no prev selection, look in prefs. @@ -318,7 +319,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): items = [(i[0].decode(), i[1]) if not isinstance(i[0], str) else i for i in items] - items.sort(key=lambda x: x[0].lower()) + items.sort(key=lambda x: asserttype(x[0], str).lower()) items = [['__default__', None]] + items # Default is always first. index = 0 diff --git a/assets/src/ba_data/python/bastd/ui/profile/browser.py b/assets/src/ba_data/python/bastd/ui/profile/browser.py index ca67d641..6521b815 100644 --- a/assets/src/ba_data/python/bastd/ui/profile/browser.py +++ b/assets/src/ba_data/python/bastd/ui/profile/browser.py @@ -270,6 +270,7 @@ class ProfileBrowserWindow(ba.Window): def _refresh(self) -> None: # pylint: disable=too-many-locals + from efro.util import asserttype from ba.internal import (PlayerProfilesChangedMessage, get_player_profile_colors, get_player_profile_icon) @@ -281,7 +282,7 @@ class ProfileBrowserWindow(ba.Window): self._profiles = ba.app.config.get('Player Profiles', {}) assert self._profiles is not None items = list(self._profiles.items()) - items.sort(key=lambda x: x[0].lower()) + items.sort(key=lambda x: asserttype(x[0], str).lower()) index = 0 account_name: Optional[str] if _ba.get_account_state() == 'signed_in': diff --git a/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py b/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py index 3fcb5d89..ffb6898f 100644 --- a/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py +++ b/assets/src/ba_data/python/bastd/ui/soundtrack/browser.py @@ -362,6 +362,7 @@ class SoundtrackBrowserWindow(ba.Window): return ba.Lstr(value=soundtrack) def _refresh(self, select_soundtrack: str = None) -> None: + from efro.util import asserttype self._allow_changing_soundtracks = False old_selection = self._selected_soundtrack @@ -377,7 +378,7 @@ class SoundtrackBrowserWindow(ba.Window): self._soundtracks = ba.app.config.get('Soundtracks', {}) assert self._soundtracks is not None items = list(self._soundtracks.items()) - items.sort(key=lambda x: x[0].lower()) + items.sort(key=lambda x: asserttype(x[0], str).lower()) items = [('__default__', None)] + items # default is always first index = 0 for pname, _pval in items: diff --git a/docs/ba_module.md b/docs/ba_module.md index 8f282d8e..4b1bcdc7 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,5 +1,5 @@ -

last updated on 2020-10-09 for Ballistica version 1.5.26 build 20195

+

last updated on 2020-10-09 for Ballistica version 1.5.26 build 20198

This page documents the Python classes and functions in the 'ba' module, which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please let me know. Happy modding!


diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc index a977a442..1ba009e9 100644 --- a/src/ballistica/ballistica.cc +++ b/src/ballistica/ballistica.cc @@ -29,7 +29,7 @@ namespace ballistica { // These are set automatically via script; don't change here. -const int kAppBuildNumber = 20196; +const int kAppBuildNumber = 20198; const char* kAppVersion = "1.5.26"; // Our standalone globals. diff --git a/src/ballistica/ballistica.h b/src/ballistica/ballistica.h index ec319921..f4c05fe0 100644 --- a/src/ballistica/ballistica.h +++ b/src/ballistica/ballistica.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -151,10 +152,32 @@ auto GetUniqueSessionIdentifier() -> const std::string&; /// Have our main threads/modules all been inited yet? auto IsBootstrapped() -> bool; -/// Create/init our internal (non-public) parts. +/// Internal bits. auto CreateAppInternal() -> AppInternal*; -auto AppInternalGameThreadInit() -> void; +auto AppInternalPythonInit() -> PyObject*; +auto AppInternalPythonInit2() -> void; auto AppInternalHasBlessingHash() -> bool; +auto AppInternalPutLog(bool fatal) -> bool; +auto AppInternalAwardAdTickets() -> void; +auto AppInternalAwardAdTournamentEntry() -> void; +auto AppInternalSetAdCompletionCall(PyObject* obj, bool pass_actually_showed) + -> void; +auto AppInternalPushAdViewComplete(const std::string& purpose, + bool actually_showed) -> void; +auto AppInternalPushPublicPartyState() -> void; +auto AppInternalPushSetFriendListCall(const std::vector& friends) + -> void; +auto AppInternalDispatchRemoteAchievementList(const std::set& achs) + -> void; +auto AppInternalPushAnalyticsCall(const std::string& type, int increment) + -> void; +auto AppInternalPushPurchaseTransactionCall(const std::string& item, + const std::string& receipt, + const std::string& signature, + const std::string& order_id, + bool user_initiated) -> void; +auto AppInternalGetPublicAccountID() -> std::string; +auto AppInternalOnGameThreadPause() -> void; /// Does it appear that we are a blessed build with no known user-modifications? auto IsUnmodifiedBlessedBuild() -> bool; diff --git a/src/ballistica/game/game.h b/src/ballistica/game/game.h index c913b37b..c09334eb 100644 --- a/src/ballistica/game/game.h +++ b/src/ballistica/game/game.h @@ -36,16 +36,8 @@ class Game : public Module { const std::string& account_id) -> void; auto PushSetAccountTokenCall(const std::string& account_id, const std::string& token) -> void; - auto PushAdViewCompleteCall(const std::string& purpose, bool actually_showed) - -> void; - auto PushAnalyticsCall(const std::string& type, int increment) -> void; auto PushAwardAdTicketsCall() -> void; auto PushAwardAdTournamentEntryCall() -> void; - auto PushPurchaseTransactionCall(const std::string& item, - const std::string& receipt, - const std::string& signature, - const std::string& order_id, - bool user_initiated) -> void; auto PushUDPConnectionPacketCall(const std::vector& data, const SockAddr& addr) -> void; auto PushPartyInviteCall(const std::string& name, @@ -78,7 +70,6 @@ class Game : public Module { auto PushHavePendingLoadsDoneCall() -> void; auto PushFreeMediaComponentRefsCall( const std::vector*>& components) -> void; - auto PushSetFriendListCall(const std::vector& friends) -> void; auto PushHavePendingLoadsCall() -> void; auto PushShutdownCall(bool soft) -> void; @@ -289,10 +280,6 @@ class Game : public Module { auto LocalDisplayChatMessage(const std::vector& buffer) -> void; auto ShouldAnnouncePartyJoinsAndLeaves() -> bool; - auto SetAdCompletionCall(PyObject* obj, bool pass_actually_showed) -> void; - auto CallAdCompletionCall(bool actually_showed) -> void; - auto RunGeneralAdComplete(bool actually_watched) -> void; - auto StartKickVote(ConnectionToClient* starter, ConnectionToClient* target) -> void; auto require_client_authentication() const { @@ -330,6 +317,15 @@ class Game : public Module { auto public_party_size() const { return public_party_size_; } auto SetPublicPartySize(int count) -> void; auto public_party_max_size() const { return public_party_max_size_; } + auto public_party_max_player_count() const { + return public_party_max_player_count_; + } + auto public_party_min_league() const -> const std::string& { + return public_party_min_league_; + } + auto public_party_stats_url() const -> const std::string& { + return public_party_stats_url_; + } auto SetPublicPartyMaxSize(int count) -> void; auto SetPublicPartyName(const std::string& name) -> void; auto SetPublicPartyStatsURL(const std::string& name) -> void; @@ -341,14 +337,9 @@ class Game : public Module { private: auto InitSpecialChars() -> void; auto AdViewComplete(const std::string& purpose, bool actually_showed) -> void; - auto Analytics(const std::string& type, int increment) -> void; auto AwardAdTickets() -> void; auto AwardAdTournamentEntry() -> void; auto Draw() -> void; - auto PurchaseTransaction(const std::string& item, const std::string& receipt, - const std::string& signature, - const std::string& order_id, bool user_initiated) - -> void; auto UDPConnectionPacket(const std::vector& data, const SockAddr& addr) -> void; auto PartyInvite(const std::string& name, const std::string& invite_id) @@ -384,7 +375,6 @@ class Game : public Module { auto GetGameRosterMessage() -> std::vector; auto CleanUpBeforeConnectingToHost() -> void; auto Shutdown(bool soft) -> void; - auto PushPublicPartyState() -> void; std::map google_play_id_to_client_id_map_; std::map client_id_to_google_play_id_map_; @@ -446,9 +436,6 @@ class Game : public Module { int last_kick_votes_needed_{-1}; Object::WeakRef kick_vote_starter_; Object::WeakRef kick_vote_target_; - Object::Ref ad_completion_callback_; - millisecs_t last_ad_start_time_{}; - bool ad_completion_callback_pass_actually_showed_{}; bool public_party_enabled_{false}; int public_party_size_{1}; // Always count ourself (is that what we want?). int public_party_max_size_{8}; diff --git a/src/ballistica/generic/utils.cc b/src/ballistica/generic/utils.cc index fbe93847..8788a98c 100644 --- a/src/ballistica/generic/utils.cc +++ b/src/ballistica/generic/utils.cc @@ -461,130 +461,6 @@ void Utils::SetRandomNameList(const std::list& custom_names) { } } -#define HEXVAL(x) ('0' + (x) + ((x) > 9u) * 7u) -static auto ToHex(const std::string& s_in) -> std::string { - uint32_t s_size = static_cast(s_in.size()); - std::string s_out; - s_out.resize(static_cast(s_size) * 2); - for (uint32_t i = 0; i < s_size; i++) { - s_out[i * 2] = - static_cast(HEXVAL((static_cast(s_in[i])) >> 4u)); - s_out[i * 2 + 1] = - static_cast(HEXVAL((static_cast(s_in[i]) & 15u))); - } - return s_out; -} -#undef HEXVAL - -static auto FromHex(const std::string& s_in) -> std::string { - int s_size = static_cast(s_in.size()); - BA_PRECONDITION(s_size % 2 == 0); - s_size /= 2; - std::string s_out; - s_out.resize(static_cast(s_size)); - for (int i = 0; i < s_size; i++) { - auto val = (uint32_t)s_in[i * 2]; // NOLINT(cert-str34-c) - if (val >= '0' && val <= '9') { - s_out[i] = static_cast((val - '0') << 4u); - } else if (val >= 'A' && val <= 'F') { - s_out[i] = static_cast((10u + (val - 'A')) << 4u); - } else { - throw Exception(); - } - val = (uint32_t)s_in[i * 2 + 1]; // NOLINT(cert-str34-c) - if (val >= '0' && val <= '9') { - s_out[i] = - static_cast(static_cast(s_out[i]) | (val - '0')); - } else if (val >= 'A' && val <= 'F') { - s_out[i] = static_cast(static_cast(s_out[i]) - | (10 + (val - 'A'))); - } else { - throw Exception(); - } - } - return s_out; -} - -static auto EncryptDecrypt(const std::string& to_encrypt) -> std::string { - assert(g_platform); - const char* key = g_platform->GetUniqueDeviceIdentifier().c_str(); - int key_size = - static_cast(g_platform->GetUniqueDeviceIdentifier().size()); - std::string output = to_encrypt; - for (size_t i = 0; i < to_encrypt.size(); i++) { - output[i] = to_encrypt[i] ^ key[i % (key_size)]; // NOLINT - } - return output; -} - -static auto EncryptDecryptCustom(const std::string& to_encrypt, - const std::string& key_in) -> std::string { - assert(g_platform); - const char* key = key_in.c_str(); - int key_size = static_cast(key_in.size()); - std::string output = to_encrypt; - for (size_t i = 0; i < to_encrypt.size(); i++) { - output[i] = to_encrypt[i] ^ key[i % (key_size)]; // NOLINT - } - return output; -} - -static auto PublicEncryptDecrypt(const std::string& to_encrypt) -> std::string { - std::string key_str = "create an account"; // A non-key-looking key. - const char* key = key_str.c_str(); - int key_size = static_cast(key_str.size()); - std::string output = to_encrypt; - for (size_t i = 0; i < to_encrypt.size(); i++) - output[i] = to_encrypt[i] ^ key[i % (key_size)]; // NOLINT - return output; -} - -auto Utils::LocalEncrypt(const std::string& s_in) -> std::string { - return ToHex(EncryptDecrypt(s_in)); -} - -auto Utils::LocalEncrypt2(const std::string& s_in) -> std::string { - std::string s = EncryptDecrypt(s_in); - return base64_encode((const unsigned char*)s.c_str(), - static_cast(s.size())); -} -auto Utils::EncryptCustom(const std::string& s_in, const std::string& key) - -> std::string { - std::string s = EncryptDecryptCustom(s_in, key); - return base64_encode((const unsigned char*)s.c_str(), - static_cast(s.size())); -} - -auto Utils::LocalDecrypt(const std::string& s_in) -> std::string { - return EncryptDecrypt(FromHex(s_in)); -} - -auto Utils::LocalDecrypt2(const std::string& s_in) -> std::string { - return EncryptDecrypt(base64_decode(s_in)); -} -auto Utils::DecryptCustom(const std::string& s_in, const std::string& key) - -> std::string { - return EncryptDecryptCustom(base64_decode(s_in), key); -} - -auto Utils::PublicEncrypt(const std::string& s_in) -> std::string { - return ToHex(PublicEncryptDecrypt(s_in)); -} - -auto Utils::PublicDecrypt(const std::string& s_in) -> std::string { - return PublicEncryptDecrypt(FromHex(s_in)); -} - -auto Utils::PublicEncrypt2(const std::string& s_in) -> std::string { - std::string s = PublicEncryptDecrypt(s_in); - return base64_encode((const unsigned char*)s.c_str(), - static_cast(s.size())); -} - -auto Utils::PublicDecrypt2(const std::string& s_in) -> std::string { - return PublicEncryptDecrypt(base64_decode(s_in)); -} - auto Utils::Sphrand(float radius) -> Vector3f { while (true) { float x = RandomFloat(); diff --git a/src/ballistica/generic/utils.h b/src/ballistica/generic/utils.h index 8d401297..45117d82 100644 --- a/src/ballistica/generic/utils.h +++ b/src/ballistica/generic/utils.h @@ -343,28 +343,6 @@ class Utils { static float precalc_rands_3[]; auto huffman() -> Huffman* { return huffman_.get(); } - /// Encrypt a string in a manner specific to this device. - static auto LocalEncrypt(const std::string& s) -> std::string; - static auto LocalEncrypt2(const std::string& s) -> std::string; - - /// Decode a local string that was encoded specific to this device. - /// Throws an exception on failure. - static auto LocalDecrypt(const std::string& s) -> std::string; - static auto LocalDecrypt2(const std::string& s) -> std::string; - - /// Encrypt a string using a custom key. - static auto EncryptCustom(const std::string& s, const std::string& key) - -> std::string; - /// Decrypt a string using a custom key. - static auto DecryptCustom(const std::string& s, const std::string& key) - -> std::string; - - /// Encrypt/decrypt strings to send to the master-server - static auto PublicEncrypt(const std::string& s) -> std::string; - static auto PublicDecrypt(const std::string& s) -> std::string; - static auto PublicEncrypt2(const std::string& s) -> std::string; - static auto PublicDecrypt2(const std::string& s) -> std::string; - // FIXME - move to a nice math-y place static auto Sphrand(float radius = 1.0f) -> Vector3f; diff --git a/src/ballistica/input/device/input_device.cc b/src/ballistica/input/device/input_device.cc index e0cb0215..a47a9a11 100644 --- a/src/ballistica/input/device/input_device.cc +++ b/src/ballistica/input/device/input_device.cc @@ -78,14 +78,10 @@ auto InputDevice::GetPlayerProfiles() const -> PyObject* { return nullptr; } auto InputDevice::GetPublicAccountID() const -> std::string { assert(InGameThread()); - // this default implementation assumes the device is local - // so just returns the locally signed in account's public id.. + // This default implementation assumes the device is local + // so just returns the locally signed in account's public id. - // the master-server makes our public account-id available to us - // through a misc-read-val; look for that.. - std::string pub_id = - g_python->GetAccountMiscReadVal2String("resolvedAccountID"); - return pub_id; + return AppInternalGetPublicAccountID(); } auto InputDevice::GetAccountName(bool full) const -> std::string { diff --git a/src/ballistica/python/class/python_class.cc b/src/ballistica/python/class/python_class.cc new file mode 100644 index 00000000..38316058 --- /dev/null +++ b/src/ballistica/python/class/python_class.cc @@ -0,0 +1,69 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/class/python_class.h" + +#include "ballistica/ballistica.h" + +namespace ballistica { + +void PythonClass::SetupType(PyTypeObject* obj) { + PyTypeObject t = { + PyVarObject_HEAD_INIT(nullptr, 0) + // .tp_name = "ba.Object", + // .tp_basicsize = sizeof(PythonClass), + // .tp_itemsize = 0, + // .tp_dealloc = (destructor)tp_dealloc, + // .tp_repr = (reprfunc)tp_repr, + // .tp_getattro = (getattrofunc)tp_getattro, + // .tp_setattro = (setattrofunc)tp_setattro, + // .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + // .tp_doc = "A ballistica object.", + // .tp_new = tp_new, + }; + + // python samples use the initializer style above, but it fails + // in g++ and sounds like it might not be allowed in c++ anyway, + // ..so this is close enough... + // (and still more readable than setting ALL values positionally) + assert(t.tp_itemsize == 0); // should all be zeroed though.. + t.tp_name = "ba.Object"; + t.tp_basicsize = sizeof(PythonClass); + t.tp_itemsize = 0; + t.tp_dealloc = (destructor)tp_dealloc; + // t.tp_repr = (reprfunc)tp_repr; + t.tp_getattro = (getattrofunc)tp_getattro; + t.tp_setattro = (setattrofunc)tp_setattro; + // NOLINTNEXTLINE (signed bitwise ops) + t.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; + t.tp_doc = "A ballistica object."; + t.tp_new = tp_new; + + memcpy(obj, &t, sizeof(t)); +} + +auto PythonClass::tp_repr(PythonClass* self) -> PyObject* { + BA_PYTHON_TRY; + return Py_BuildValue("s", ""); + BA_PYTHON_CATCH; +} +auto PythonClass::tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) + -> PyObject* { + auto* self = reinterpret_cast(type->tp_alloc(type, 0)); + return reinterpret_cast(self); +} +void PythonClass::tp_dealloc(PythonClass* self) { + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} +auto PythonClass::tp_getattro(PythonClass* node, PyObject* attr) -> PyObject* { + BA_PYTHON_TRY; + return PyObject_GenericGetAttr(reinterpret_cast(node), attr); + BA_PYTHON_CATCH; +} +auto PythonClass::tp_setattro(PythonClass* node, PyObject* attr, PyObject* val) + -> int { + BA_PYTHON_TRY; + return PyObject_GenericSetAttr(reinterpret_cast(node), attr, val); + BA_PYTHON_INT_CATCH; +} + +} // namespace ballistica diff --git a/src/ballistica/python/class/python_class.h b/src/ballistica/python/class/python_class.h new file mode 100644 index 00000000..b25834b2 --- /dev/null +++ b/src/ballistica/python/class/python_class.h @@ -0,0 +1,28 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_H_ +#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_H_ + +#include "ballistica/python/python_sys.h" + +namespace ballistica { + +// a convenient base class for defining custom python types +class PythonClass { + public: + PyObject_HEAD; + static void SetupType(PyTypeObject* obj); + + private: + static auto tp_repr(PythonClass* self) -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) + -> PyObject*; + static void tp_dealloc(PythonClass* self); + static auto tp_getattro(PythonClass* node, PyObject* attr) -> PyObject*; + static auto tp_setattro(PythonClass* node, PyObject* attr, PyObject* val) + -> int; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_H_ diff --git a/src/ballistica/python/class/python_class_activity_data.cc b/src/ballistica/python/class/python_class_activity_data.cc new file mode 100644 index 00000000..793c539e --- /dev/null +++ b/src/ballistica/python/class/python_class_activity_data.cc @@ -0,0 +1,183 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/class/python_class_activity_data.h" + +#include + +#include "ballistica/game/game.h" +#include "ballistica/game/host_activity.h" +#include "ballistica/game/session/host_session.h" +#include "ballistica/generic/utils.h" + +namespace ballistica { + +auto PythonClassActivityData::nb_bool(PythonClassActivityData* self) -> int { + return self->host_activity_->exists(); +} + +PyNumberMethods PythonClassActivityData::as_number_; + +void PythonClassActivityData::SetupType(PyTypeObject* obj) { + PythonClass::SetupType(obj); + obj->tp_name = "_ba.ActivityData"; + obj->tp_basicsize = sizeof(PythonClassActivityData); + obj->tp_doc = "(internal)"; + obj->tp_new = tp_new; + obj->tp_dealloc = (destructor)tp_dealloc; + obj->tp_repr = (reprfunc)tp_repr; + obj->tp_methods = tp_methods; + + // We provide number methods only for bool functionality. + memset(&as_number_, 0, sizeof(as_number_)); + as_number_.nb_bool = (inquiry)nb_bool; + obj->tp_as_number = &as_number_; +} + +auto PythonClassActivityData::Create(HostActivity* host_activity) -> PyObject* { + auto* py_activity_data = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + BA_PRECONDITION(py_activity_data); + *(py_activity_data->host_activity_) = host_activity; + return reinterpret_cast(py_activity_data); +} + +auto PythonClassActivityData::GetHostActivity() const -> HostActivity* { + HostActivity* host_activity = host_activity_->get(); + if (!host_activity) + throw Exception( + "Invalid ActivityData; this activity has probably been expired and " + "should not be getting used."); + return host_activity; +} + +auto PythonClassActivityData::tp_repr(PythonClassActivityData* self) + -> PyObject* { + BA_PYTHON_TRY; + return Py_BuildValue( + "s", (std::string("host_activity_->get()) + " >") + .c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassActivityData::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = + reinterpret_cast(type->tp_alloc(type, 0)); + if (self) { + BA_PYTHON_TRY; + if (!InGameThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the game thread (current is (" + + GetCurrentThreadName() + ")."); + } + self->host_activity_ = new Object::WeakRef(); + BA_PYTHON_NEW_CATCH; + } + return reinterpret_cast(self); +} + +void PythonClassActivityData::tp_dealloc(PythonClassActivityData* self) { + BA_PYTHON_TRY; + + // These have to be destructed in the game thread; send them along to + // it if need be; otherwise do it immediately. + if (!InGameThread()) { + Object::WeakRef* h = self->host_activity_; + g_game->PushCall([h] { delete h; }); + } else { + delete self->host_activity_; + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +auto PythonClassActivityData::exists(PythonClassActivityData* self) + -> PyObject* { + BA_PYTHON_TRY; + + HostActivity* host_activity = self->host_activity_->get(); + if (host_activity) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + + BA_PYTHON_CATCH; +} + +auto PythonClassActivityData::make_foreground(PythonClassActivityData* self) + -> PyObject* { + BA_PYTHON_TRY; + + HostActivity* a = self->host_activity_->get(); + if (!a) { + throw Exception("Invalid activity.", PyExcType::kActivityNotFound); + } + HostSession* session = a->GetHostSession(); + if (!session) { + throw Exception("Activity's Session not found.", + PyExcType::kSessionNotFound); + } + session->SetForegroundHostActivity(a); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassActivityData::start(PythonClassActivityData* self) + -> PyObject* { + BA_PYTHON_TRY; + + HostActivity* a = self->host_activity_->get(); + if (!a) { + throw Exception("Invalid activity data.", PyExcType::kActivityNotFound); + } + a->start(); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassActivityData::expire(PythonClassActivityData* self) + -> PyObject* { + BA_PYTHON_TRY; + HostActivity* a = self->host_activity_->get(); + + // The python side may have stuck around after our c++ side was + // torn down; that's ok. + if (a) { + HostSession* session = a->GetHostSession(); + if (!session) { + throw Exception("Activity's Session not found.", + PyExcType::kSessionNotFound); + } + session->DestroyHostActivity(a); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +PyTypeObject PythonClassActivityData::type_obj; +PyMethodDef PythonClassActivityData::tp_methods[] = { + {"exists", (PyCFunction)exists, METH_NOARGS, + "exists() -> bool\n" + "\n" + "Returns whether the ActivityData still exists.\n" + "Most functionality will fail on a nonexistent instance."}, + {"make_foreground", (PyCFunction)make_foreground, METH_NOARGS, + "make_foreground() -> None\n" + "\n" + "Sets this activity as the foreground one in its session."}, + {"expire", (PyCFunction)expire, METH_NOARGS, + "expire() -> None\n" + "\n" + "Expires the internal data for the activity"}, + {"start", (PyCFunction)start, METH_NOARGS, + "start() -> None\n" + "\n" + "Begins the activity running"}, + {nullptr}}; + +} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_activity_data.h b/src/ballistica/python/class/python_class_activity_data.h new file mode 100644 index 00000000..4da2a532 --- /dev/null +++ b/src/ballistica/python/class/python_class_activity_data.h @@ -0,0 +1,39 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_ACTIVITY_DATA_H_ +#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_ACTIVITY_DATA_H_ + +#include "ballistica/core/object.h" +#include "ballistica/python/class/python_class.h" + +namespace ballistica { + +class PythonClassActivityData : public PythonClass { + public: + static auto type_name() -> const char* { return "ActivityData"; } + static void SetupType(PyTypeObject* obj); + static auto Create(HostActivity* host_activity) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + static PyTypeObject type_obj; + auto GetHostActivity() const -> HostActivity*; + + private: + static PyMethodDef tp_methods[]; + static auto tp_repr(PythonClassActivityData* self) -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassActivityData* self); + static auto exists(PythonClassActivityData* self) -> PyObject*; + static auto make_foreground(PythonClassActivityData* self) -> PyObject*; + static auto start(PythonClassActivityData* self) -> PyObject*; + static auto expire(PythonClassActivityData* self) -> PyObject*; + Object::WeakRef* host_activity_; + static auto nb_bool(PythonClassActivityData* self) -> int; + static PyNumberMethods as_number_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_ACTIVITY_DATA_H_ diff --git a/src/ballistica/python/class/python_class_collide_model.cc b/src/ballistica/python/class/python_class_collide_model.cc new file mode 100644 index 00000000..8411b23a --- /dev/null +++ b/src/ballistica/python/class/python_class_collide_model.cc @@ -0,0 +1,110 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/class/python_class_collide_model.h" + +#include + +#include "ballistica/game/game.h" +#include "ballistica/media/component/collide_model.h" + +namespace ballistica { + +auto PythonClassCollideModel::tp_repr(PythonClassCollideModel* self) + -> PyObject* { + BA_PYTHON_TRY; + Object::Ref m = *(self->collide_model_); + return Py_BuildValue( + "s", (std::string("name() + "\"") : "(empty ref)") + ">") + .c_str()); + BA_PYTHON_CATCH; +} + +void PythonClassCollideModel::SetupType(PyTypeObject* obj) { + PythonClass::SetupType(obj); + obj->tp_name = "ba.CollideModel"; + obj->tp_basicsize = sizeof(PythonClassCollideModel); + obj->tp_doc = + "A reference to a collide-model.\n" + "\n" + "Category: Asset Classes\n" + "\n" + "Use ba.getcollidemodel() to instantiate one."; + obj->tp_repr = (reprfunc)tp_repr; + obj->tp_new = tp_new; + obj->tp_dealloc = (destructor)tp_dealloc; +} + +auto PythonClassCollideModel::Create(CollideModel* collide_model) -> PyObject* { + s_create_empty_ = true; // prevent class from erroring on create + auto* t = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + s_create_empty_ = false; + if (!t) { + throw Exception("ba.CollideModel creation failed."); + } + *(t->collide_model_) = collide_model; + return reinterpret_cast(t); +} + +auto PythonClassCollideModel::GetCollideModel(bool doraise) const + -> CollideModel* { + CollideModel* collide_model = collide_model_->get(); + if (!collide_model && doraise) { + throw Exception("Invalid CollideModel.", PyExcType::kNotFound); + } + return collide_model; +} + +auto PythonClassCollideModel::tp_new(PyTypeObject* type, PyObject* args, + PyObject* kwds) -> PyObject* { + auto* self = + reinterpret_cast(type->tp_alloc(type, 0)); + if (self) { + BA_PYTHON_TRY; + if (!InGameThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the game thread (current is (" + + GetCurrentThreadName() + ")."); + } + if (!s_create_empty_) { + throw Exception( + "Can't instantiate CollideModels directly; use " + "ba.getcollidemodel() to get them."); + } + self->collide_model_ = new Object::Ref(); + BA_PYTHON_NEW_CATCH; + } + return reinterpret_cast(self); +} + +void PythonClassCollideModel::Delete(Object::Ref* ref) { + assert(InGameThread()); + // if we're the py-object for a collide_model, clear them out + // (FIXME - we should pass the old pointer in here to sanity-test that we + // were their ref) + if (ref->exists()) { + (*ref)->ClearPyObject(); + } + delete ref; +} + +void PythonClassCollideModel::tp_dealloc(PythonClassCollideModel* self) { + BA_PYTHON_TRY; + // these have to be deleted in the game thread - send the ptr along if need + // be; otherwise do it immediately + if (!InGameThread()) { + Object::Ref* c = self->collide_model_; + g_game->PushCall([c] { Delete(c); }); + } else { + Delete(self->collide_model_); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +bool PythonClassCollideModel::s_create_empty_ = false; +PyTypeObject PythonClassCollideModel::type_obj; + +} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_collide_model.h b/src/ballistica/python/class/python_class_collide_model.h new file mode 100644 index 00000000..d59c3442 --- /dev/null +++ b/src/ballistica/python/class/python_class_collide_model.h @@ -0,0 +1,34 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_COLLIDE_MODEL_H_ +#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_COLLIDE_MODEL_H_ + +#include "ballistica/core/object.h" +#include "ballistica/python/class/python_class.h" + +namespace ballistica { + +class PythonClassCollideModel : public PythonClass { + public: + static auto type_name() -> const char* { return "CollideModel"; } + static auto tp_repr(PythonClassCollideModel* self) -> PyObject*; + static void SetupType(PyTypeObject* obj); + static PyTypeObject type_obj; + static auto Create(CollideModel* collide_model) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + auto GetCollideModel(bool doraise = true) const -> CollideModel*; + + private: + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) + -> PyObject*; + static void Delete(Object::Ref* ref); + static void tp_dealloc(PythonClassCollideModel* self); + static bool s_create_empty_; + Object::Ref* collide_model_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_COLLIDE_MODEL_H_ diff --git a/src/ballistica/python/class/python_class_context.cc b/src/ballistica/python/class/python_class_context.cc new file mode 100644 index 00000000..b5539c98 --- /dev/null +++ b/src/ballistica/python/class/python_class_context.cc @@ -0,0 +1,227 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/class/python_class_context.h" + +#include + +#include "ballistica/game/game.h" +#include "ballistica/game/host_activity.h" +#include "ballistica/game/session/host_session.h" +#include "ballistica/python/python.h" +#include "ballistica/ui/ui.h" + +namespace ballistica { + +void PythonClassContext::SetupType(PyTypeObject* obj) { + PythonClass::SetupType(obj); + obj->tp_name = "ba.Context"; + obj->tp_basicsize = sizeof(PythonClassContext); + obj->tp_doc = + "Context(source: Any)\n" + "\n" + "A game context state.\n" + "\n" + "Category: General Utility Classes\n" + "\n" + "Many operations such as ba.newnode() or ba.gettexture() operate\n" + "implicitly on the current context. Each ba.Activity has its own\n" + "Context and objects within that activity (nodes, media, etc) can only\n" + "interact with other objects from that context.\n" + "\n" + "In general, as a modder, you should not need to worry about contexts,\n" + "since timers and other callbacks will take care of saving and\n" + "restoring the context automatically, but there may be rare cases where\n" + "you need to deal with them, such as when loading media in for use in\n" + "the UI (there is a special 'ui' context for all user-interface-related\n" + "functionality)\n" + "\n" + "When instantiating a ba.Context instance, a single 'source' argument\n" + "is passed, which can be one of the following strings/objects:\n\n" + "'empty':\n" + " Gives an empty context; it can be handy to run code here to ensure\n" + " it does no loading of media, creation of nodes, etc.\n" + "\n" + "'current':\n" + " Sets the context object to the current context.\n" + "\n" + "'ui':\n" + " Sets to the UI context. UI functions as well as loading of media to\n" + " be used in said functions must happen in the UI context.\n" + "\n" + "A ba.Activity instance:\n" + " Gives the context for the provided ba.Activity.\n" + " Most all code run during a game happens in an Activity's Context.\n" + "\n" + "A ba.Session instance:\n" + " Gives the context for the provided ba.Session.\n" + " Generally a user should not need to run anything here.\n" + "\n" + "\n" + "Usage:\n" + "\n" + "Contexts are generally used with the python 'with' statement, which\n" + "sets the context as current on entry and resets it to the previous\n" + "value on exit.\n" + "\n" + "# Example: load a few textures into the UI context\n" + "# (for use in widgets, etc):\n" + "with ba.Context('ui'):\n" + " tex1 = ba.gettexture('foo_tex_1')\n" + " tex2 = ba.gettexture('foo_tex_2')\n"; + + obj->tp_new = tp_new; + obj->tp_dealloc = (destructor)tp_dealloc; + obj->tp_repr = (reprfunc)tp_repr; + obj->tp_richcompare = (richcmpfunc)tp_richcompare; + obj->tp_methods = tp_methods; +} + +auto PythonClassContext::tp_repr(PythonClassContext* self) -> PyObject* { + BA_PYTHON_TRY; + + std::string context_str; + if (self->context_->GetUIContext()) { + context_str = "ui"; + } else if (HostActivity* ha = self->context_->GetHostActivity()) { + 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 (self->context_->target.exists()) { + context_str = self->context_->target->GetObjectDescription(); + } else { + context_str = "empty"; + } + context_str = ""; + return PyUnicode_FromString(context_str.c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassContext::tp_richcompare(PythonClassContext* c1, PyObject* c2, + int op) -> PyObject* { + // always return false against other types + if (!Check(c2)) { + Py_RETURN_FALSE; + } + bool eq = (*(c1->context_) + == *((reinterpret_cast(c2))->context_)); + if (op == Py_EQ) { + if (eq) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + } else if (op == Py_NE) { + if (!eq) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + } else { + // don't support other ops + Py_RETURN_NOTIMPLEMENTED; + } +} + +auto PythonClassContext::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + PyObject* source_obj = Py_None; + if (!PyArg_ParseTuple(args, "O", &source_obj)) { + return nullptr; + } + + if (!InGameThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the game thread (current is (" + + GetCurrentThreadName() + ")."); + } + + Context cs(nullptr); + + if (Python::IsPyString(source_obj)) { + std::string source = Python::GetPyString(source_obj); + if (source == "ui") { + cs = Context(g_game->GetUIContextTarget()); + } else if (source == "UI") { + BA_LOG_ONCE("'UI' context-target option is deprecated; please use 'ui'"); + Python::PrintStackTrace(); + cs = Context(g_game->GetUIContextTarget()); + } else if (source == "current") { + cs = Context::current(); + } else if (source == "empty") { + cs = Context(nullptr); + } else { + throw Exception("invalid context identifier: '" + source + "'"); + } + } else if (Python::IsPyHostActivity(source_obj)) { + cs = Context(Python::GetPyHostActivity(source_obj)); + } else if (Python::IsPySession(source_obj)) { + auto* hs = dynamic_cast(Python::GetPySession(source_obj)); + assert(hs != nullptr); + cs = Context(hs); + } else { + throw Exception( + "Invalid argument to ba.Context(): " + Python::ObjToString(source_obj) + + "; expected 'ui', 'current', 'empty', a ba.Activity, or a " + "ba.Session"); + } + + auto* self = reinterpret_cast(type->tp_alloc(type, 0)); + if (self) { + BA_PYTHON_TRY; + self->context_ = new Context(cs); + self->context_prev_ = new Context(); + BA_PYTHON_NEW_CATCH; + } + return reinterpret_cast(self); + BA_PYTHON_CATCH; +} + +void PythonClassContext::tp_dealloc(PythonClassContext* self) { + BA_PYTHON_TRY; + // Contexts have to be deleted in the game thread; + // ship them to it for deletion if need be; otherwise do it immediately. + if (!InGameThread()) { + Context* c = self->context_; + Context* c2 = self->context_prev_; + g_game->PushCall([c, c2] { + delete c; + delete c2; + }); + } else { + delete self->context_; + delete self->context_prev_; + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +auto PythonClassContext::__enter__(PythonClassContext* self) -> PyObject* { + BA_PYTHON_TRY; + *(self->context_prev_) = Context::current(); + Context::set_current(*(self->context_)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassContext::__exit__(PythonClassContext* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + Context::set_current(*(self->context_prev_)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +PyTypeObject PythonClassContext::type_obj; +PyMethodDef PythonClassContext::tp_methods[] = { + {"__enter__", (PyCFunction)__enter__, METH_NOARGS, + "enter call for 'with' functionality"}, + {"__exit__", (PyCFunction)__exit__, METH_VARARGS, + "exit call for 'with' functionality"}, + {nullptr}}; + +} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_context.h b/src/ballistica/python/class/python_class_context.h new file mode 100644 index 00000000..9065fb37 --- /dev/null +++ b/src/ballistica/python/class/python_class_context.h @@ -0,0 +1,37 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_H_ +#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_H_ + +#include "ballistica/core/context.h" +#include "ballistica/python/class/python_class.h" + +namespace ballistica { + +class PythonClassContext : public PythonClass { + public: + static auto type_name() -> const char* { return "Context"; } + static void SetupType(PyTypeObject* obj); + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + static PyTypeObject type_obj; + auto context() const -> const Context& { return *context_; } + + private: + static PyMethodDef tp_methods[]; + static auto tp_repr(PythonClassContext* self) -> PyObject*; + static auto tp_richcompare(PythonClassContext* c1, PyObject* c2, int op) + -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassContext* self); + static auto __enter__(PythonClassContext* self) -> PyObject*; + static auto __exit__(PythonClassContext* self, PyObject* args) -> PyObject*; + Context* context_; + Context* context_prev_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_H_ diff --git a/src/ballistica/python/class/python_class_context_call.cc b/src/ballistica/python/class/python_class_context_call.cc new file mode 100644 index 00000000..69d0854b --- /dev/null +++ b/src/ballistica/python/class/python_class_context_call.cc @@ -0,0 +1,130 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/class/python_class_context_call.h" + +#include + +#include "ballistica/game/game.h" +#include "ballistica/python/python_context_call.h" + +namespace ballistica { + +void PythonClassContextCall::SetupType(PyTypeObject* obj) { + PythonClass::SetupType(obj); + obj->tp_name = "ba.ContextCall"; + obj->tp_basicsize = sizeof(PythonClassContextCall); + obj->tp_doc = + "ContextCall(call: Callable)\n" + "\n" + "A context-preserving callable.\n" + "\n" + "Category: General Utility Classes\n" + "\n" + "A ContextCall wraps a callable object along with a reference\n" + "to the current context (see ba.Context); it handles restoring the\n" + "context when run and automatically clears itself if the context\n" + "it belongs to shuts down.\n" + "\n" + "Generally you should not need to use this directly; all standard\n" + "Ballistica callbacks involved with timers, materials, UI functions,\n" + "etc. handle this under-the-hood you don't have to worry about it.\n" + "The only time it may be necessary is if you are implementing your\n" + "own callbacks, such as a worker thread that does some action and then\n" + "runs some game code when done. By wrapping said callback in one of\n" + "these, you can ensure that you will not inadvertently be keeping the\n" + "current activity alive or running code in a torn-down (expired)\n" + "context.\n" + "\n" + "You can also use ba.WeakCall for similar functionality, but\n" + "ContextCall has the added bonus that it will not run during context\n" + "shutdown, whereas ba.WeakCall simply looks at whether the target\n" + "object still exists.\n" + "\n" + "# Example A: code like this can inadvertently prevent our activity\n" + "# (self) from ending until the operation completes, since the bound\n" + "# method we're passing (self.dosomething) contains a strong-reference\n" + "# to self).\n" + "start_some_long_action(callback_when_done=self.dosomething)\n" + "\n" + "# Example B: in this case our activity (self) can still die\n" + "# properly; the callback will clear itself when the activity starts\n" + "# shutting down, becoming a harmless no-op and releasing the reference\n" + "# to our activity.\n" + "start_long_action(callback_when_done=ba.ContextCall(self.mycallback))\n"; + + obj->tp_new = tp_new; + obj->tp_dealloc = (destructor)tp_dealloc; + obj->tp_repr = (reprfunc)tp_repr; + obj->tp_methods = tp_methods; + obj->tp_call = (ternaryfunc)tp_call; +} + +auto PythonClassContextCall::tp_call(PythonClassContextCall* self, + PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) + return nullptr; + + (*(self->context_call_))->Run(); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassContextCall::tp_repr(PythonClassContextCall* self) + -> PyObject* { + BA_PYTHON_TRY; + assert(self->context_call_->exists()); + return PyUnicode_FromString( + ("") + .c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassContextCall::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = + reinterpret_cast(type->tp_alloc(type, 0)); + if (self) { + BA_PYTHON_TRY; + + // try to do anything that may throw an exception before/during our + // placement-new so we don't have to worry about tearing it down if + // something goes wrong afterwards + PyObject* source_obj = Py_None; + if (!PyArg_ParseTuple(args, "O", &source_obj)) return nullptr; + if (!InGameThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the game thread (current is (" + + GetCurrentThreadName() + ")."); + } + self->context_call_ = new Object::Ref( + Object::New(source_obj)); + BA_PYTHON_NEW_CATCH; + } + return reinterpret_cast(self); +} + +void PythonClassContextCall::tp_dealloc(PythonClassContextCall* self) { + BA_PYTHON_TRY; + // these have to be deleted in the game thread - send the ptr along if need + // be; otherwise do it immediately + if (!InGameThread()) { + Object::Ref* c = self->context_call_; + g_game->PushCall([c] { delete c; }); + } else { + delete self->context_call_; + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +PyTypeObject PythonClassContextCall::type_obj; +PyMethodDef PythonClassContextCall::tp_methods[] = {{nullptr}}; + +} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_context_call.h b/src/ballistica/python/class/python_class_context_call.h new file mode 100644 index 00000000..34e830fd --- /dev/null +++ b/src/ballistica/python/class/python_class_context_call.h @@ -0,0 +1,33 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_CALL_H_ +#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_CALL_H_ + +#include "ballistica/core/object.h" +#include "ballistica/python/class/python_class.h" + +namespace ballistica { + +class PythonClassContextCall : public PythonClass { + public: + static auto type_name() -> const char* { return "ContextCall"; } + static void SetupType(PyTypeObject* obj); + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + static PyTypeObject type_obj; + + private: + static PyMethodDef tp_methods[]; + static auto tp_call(PythonClassContextCall* self, PyObject* args, + PyObject* keywds) -> PyObject*; + static auto tp_repr(PythonClassContextCall* self) -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassContextCall* self); + Object::Ref* context_call_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_CONTEXT_CALL_H_ diff --git a/src/ballistica/python/class/python_class_data.cc b/src/ballistica/python/class/python_class_data.cc new file mode 100644 index 00000000..2f8b9113 --- /dev/null +++ b/src/ballistica/python/class/python_class_data.cc @@ -0,0 +1,139 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/class/python_class_data.h" + +#include + +#include "ballistica/game/game.h" +#include "ballistica/media/component/data.h" + +namespace ballistica { + +auto PythonClassData::tp_repr(PythonClassData* self) -> PyObject* { + BA_PYTHON_TRY; + Object::Ref m = *(self->data_); + return Py_BuildValue( + "s", (std::string("name() + "\"") : "(empty ref)") + ">") + .c_str()); + BA_PYTHON_CATCH; +} + +void PythonClassData::SetupType(PyTypeObject* obj) { + PythonClass::SetupType(obj); + obj->tp_name = "ba.Data"; + obj->tp_basicsize = sizeof(PythonClassData); + obj->tp_doc = + "A reference to a data object.\n" + "\n" + "Category: Asset Classes\n" + "\n" + "Use ba.getdata() to instantiate one."; + obj->tp_repr = (reprfunc)tp_repr; + obj->tp_new = tp_new; + obj->tp_dealloc = (destructor)tp_dealloc; + obj->tp_methods = tp_methods; +} + +auto PythonClassData::Create(Data* data) -> PyObject* { + s_create_empty_ = true; // prevent class from erroring on create + auto* t = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + s_create_empty_ = false; + if (!t) { + throw Exception("ba.Data creation failed."); + } + *(t->data_) = data; + return reinterpret_cast(t); +} + +auto PythonClassData::GetData(bool doraise) const -> Data* { + Data* data = data_->get(); + if (!data && doraise) { + throw Exception("Invalid Data.", PyExcType::kNotFound); + } + return data; +} + +auto PythonClassData::tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) + -> PyObject* { + auto* self = reinterpret_cast(type->tp_alloc(type, 0)); + if (self) { + BA_PYTHON_TRY; + if (!InGameThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the game thread (current is (" + + GetCurrentThreadName() + ")."); + } + if (!s_create_empty_) { + throw Exception( + "Can't instantiate Datas directly; use ba.getdata() to get " + "them."); + } + self->data_ = new Object::Ref(); + BA_PYTHON_NEW_CATCH; + } + return reinterpret_cast(self); +} + +void PythonClassData::Delete(Object::Ref* ref) { + assert(InGameThread()); + + // if we're the py-object for a data, clear them out + // (FIXME - wej should pass the old pointer in here to sanity-test that we + // were their ref) + if (ref->exists()) { + (*ref)->ClearPyObject(); + } + delete ref; +} + +void PythonClassData::tp_dealloc(PythonClassData* self) { + BA_PYTHON_TRY; + // these have to be deleted in the game thread - send the ptr along if need + // be; otherwise do it immediately + if (!InGameThread()) { + Object::Ref* s = self->data_; + g_game->PushCall([s] { Delete(s); }); + } else { + Delete(self->data_); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +auto PythonClassData::GetValue(PythonClassData* self) -> PyObject* { + BA_PYTHON_TRY; + Data* data = self->data_->get(); + if (data == nullptr) { + throw Exception("Invalid data object.", PyExcType::kNotFound); + } + // haha really need to rename this class. + DataData* datadata = data->data_data(); + datadata->Load(); + datadata->set_last_used_time(GetRealTime()); + PyObject* obj = datadata->object().get(); + assert(obj); + Py_INCREF(obj); + return obj; + BA_PYTHON_CATCH; +} + +bool PythonClassData::s_create_empty_ = false; +PyTypeObject PythonClassData::type_obj; + +PyMethodDef PythonClassData::tp_methods[] = { + {"getvalue", (PyCFunction)GetValue, METH_NOARGS, + "getvalue() -> Any\n" + "\n" + "Return the data object's value.\n" + "\n" + "This can consist of anything representable by json (dicts, lists,\n" + "numbers, bools, None, etc).\n" + "Note that this call will block if the data has not yet been loaded,\n" + "so it can be beneficial to plan a short bit of time between when\n" + "the data object is requested and when it's value is accessed.\n"}, + {nullptr}}; + +} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_data.h b/src/ballistica/python/class/python_class_data.h new file mode 100644 index 00000000..075fbad6 --- /dev/null +++ b/src/ballistica/python/class/python_class_data.h @@ -0,0 +1,36 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_DATA_H_ +#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_DATA_H_ + +#include "ballistica/core/object.h" +#include "ballistica/python/class/python_class.h" + +namespace ballistica { + +class PythonClassData : public PythonClass { + public: + static auto type_name() -> const char* { return "Data"; } + static PyTypeObject type_obj; + static auto tp_repr(PythonClassData* self) -> PyObject*; + static void SetupType(PyTypeObject* obj); + static auto Create(Data* data) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + auto GetData(bool doraise = true) const -> Data*; + + private: + static PyMethodDef tp_methods[]; + static auto GetValue(PythonClassData* self) -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) + -> PyObject*; + static void tp_dealloc(PythonClassData* self); + static void Delete(Object::Ref* ref); + static bool s_create_empty_; + Object::Ref* data_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_DATA_H_ diff --git a/src/ballistica/python/class/python_class_input_device.cc b/src/ballistica/python/class/python_class_input_device.cc new file mode 100644 index 00000000..6176f119 --- /dev/null +++ b/src/ballistica/python/class/python_class_input_device.cc @@ -0,0 +1,446 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/class/python_class_input_device.h" + +#include + +#include "ballistica/game/player.h" +#include "ballistica/input/device/input_device.h" +#include "ballistica/input/device/keyboard_input.h" +#include "ballistica/python/python.h" + +namespace ballistica { + +// Ignore a few things that python macros do. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "RedundantCast" + +void PythonClassInputDevice::SetupType(PyTypeObject* obj) { + PythonClass::SetupType(obj); + obj->tp_name = "ba.InputDevice"; + obj->tp_basicsize = sizeof(PythonClassInputDevice); + obj->tp_doc = + "An input-device such as a gamepad, touchscreen, or keyboard.\n" + "\n" + "Category: Gameplay Classes\n" + "\n" + "Attributes:\n" + "\n" + " allows_configuring: bool\n" + " Whether the input-device can be configured.\n" + "\n" + " has_meaningful_button_names: bool\n" + " Whether button names returned by this instance match labels\n" + " on the actual device. (Can be used to determine whether to show\n" + " them in controls-overlays, etc.)\n" + "\n" + " player: Optional[ba.SessionPlayer]\n" + " The player associated with this input device.\n" + "\n" + " client_id: int\n" + " The numeric client-id this device is associated with.\n" + " This is only meaningful for remote client inputs; for\n" + " all local devices this will be -1.\n" + "\n" + " name: str\n" + " The name of the device.\n" + "\n" + " unique_identifier: str\n" + " A string that can be used to persistently identify the device,\n" + " even among other devices of the same type. Used for saving\n" + " prefs, etc.\n" + "\n" + " id: int\n" + " The unique numeric id of this device.\n" + "\n" + " instance_number: int\n" + " The number of this device among devices of the same type.\n" + "\n" + " is_controller_app: bool\n" + " Whether this input-device represents a locally-connected\n" + " controller-app.\n" + "\n" + " is_remote_client: bool\n" + " Whether this input-device represents a remotely-connected\n" + " client.\n" + "\n"; + + obj->tp_new = tp_new; + obj->tp_dealloc = (destructor)tp_dealloc; + obj->tp_repr = (reprfunc)tp_repr; + obj->tp_methods = tp_methods; + obj->tp_getattro = (getattrofunc)tp_getattro; + obj->tp_setattro = (setattrofunc)tp_setattro; + + // We provide number methods only for bool functionality. + memset(&as_number_, 0, sizeof(as_number_)); + as_number_.nb_bool = (inquiry)nb_bool; + obj->tp_as_number = &as_number_; +} + +auto PythonClassInputDevice::Create(InputDevice* input_device) -> PyObject* { + // Make sure we only have one python ref per material. + if (input_device) { + assert(!input_device->has_py_ref()); + } + auto* py_input_device = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + if (!py_input_device) { + throw Exception("ba.InputDevice creation failed."); + } + *(py_input_device->input_device_) = input_device; + return reinterpret_cast(py_input_device); +} + +auto PythonClassInputDevice::GetInputDevice() const -> InputDevice* { + InputDevice* input_device = input_device_->get(); + if (!input_device) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + return input_device; +} + +auto PythonClassInputDevice::tp_repr(PythonClassInputDevice* self) + -> PyObject* { + BA_PYTHON_TRY; + InputDevice* d = self->input_device_->get(); + int input_device_id = d ? d->index() : -1; + std::string dname = d ? d->GetDeviceName() : "invalid device"; + return Py_BuildValue("s", + (std::string("") + .c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassInputDevice::nb_bool(PythonClassInputDevice* self) -> int { + return self->input_device_->exists(); +} + +PyNumberMethods PythonClassInputDevice::as_number_; + +auto PythonClassInputDevice::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = + reinterpret_cast(type->tp_alloc(type, 0)); + if (self) { + BA_PYTHON_TRY; + if (!InGameThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the game thread (current is (" + + GetCurrentThreadName() + ")."); + } + self->input_device_ = new Object::WeakRef(); + BA_PYTHON_NEW_CATCH; + } + return reinterpret_cast(self); +} + +void PythonClassInputDevice::tp_dealloc(PythonClassInputDevice* self) { + BA_PYTHON_TRY; + // These have to be destructed in the game thread - send them along to it if + // need be. + // FIXME: Technically the main thread has a pointer to a dead PyObject + // until the delete goes through; could that ever be a problem? + if (!InGameThread()) { + Object::WeakRef* d = self->input_device_; + g_game->PushCall([d] { delete d; }); + } else { + delete self->input_device_; + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +auto PythonClassInputDevice::tp_getattro(PythonClassInputDevice* self, + PyObject* attr) -> PyObject* { + BA_PYTHON_TRY; + assert(PyUnicode_Check(attr)); // NOLINT (signed bitwise ops) + const char* s = PyUnicode_AsUTF8(attr); + if (!strcmp(s, "player")) { + InputDevice* input_device = self->input_device_->get(); + if (!input_device) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + Player* player = input_device->GetPlayer(); + if (player != nullptr) { + return player->NewPyRef(); + } + Py_RETURN_NONE; + } else if (!strcmp(s, "allows_configuring")) { + InputDevice* d = self->input_device_->get(); + if (!d) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + if (d->GetAllowsConfiguring()) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + } else if (!strcmp(s, "has_meaningful_button_names")) { + InputDevice* d = self->input_device_->get(); + if (!d) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + if (d->HasMeaningfulButtonNames()) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + } else if (!strcmp(s, "client_id")) { + InputDevice* d = self->input_device_->get(); + if (!d) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + return PyLong_FromLong(d->GetClientID()); + } else if (!strcmp(s, "name")) { + InputDevice* d = self->input_device_->get(); + if (!d) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + return PyUnicode_FromString(d->GetDeviceName().c_str()); + } else if (!strcmp(s, "unique_identifier")) { + InputDevice* d = self->input_device_->get(); + if (!d) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + return PyUnicode_FromString(d->GetPersistentIdentifier().c_str()); + } else if (!strcmp(s, "id")) { + InputDevice* d = self->input_device_->get(); + if (!d) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + return PyLong_FromLong(d->index()); + } else if (!strcmp(s, "instance_number")) { + InputDevice* d = self->input_device_->get(); + if (!d) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + return PyLong_FromLong(d->device_number()); + } else if (!strcmp(s, "is_controller_app")) { + InputDevice* input_device = self->input_device_->get(); + if (!input_device) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + if (input_device->IsRemoteApp()) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + } else if (!strcmp(s, "is_remote_client")) { + InputDevice* input_device = self->input_device_->get(); + if (!input_device) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + if (input_device->IsRemoteClient()) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + } + + // Fall back to generic behavior. + PyObject* val; + val = PyObject_GenericGetAttr(reinterpret_cast(self), attr); + return val; + BA_PYTHON_CATCH; +} + +auto PythonClassInputDevice::tp_setattro(PythonClassInputDevice* self, + PyObject* attr, PyObject* val) -> int { + BA_PYTHON_TRY; + assert(PyUnicode_Check(attr)); // NOLINT (signed bitwise) + throw Exception("Attr '" + std::string(PyUnicode_AsUTF8(attr)) + + "' is not settable on input device objects."); + // return PyObject_GenericSetAttr(reinterpret_cast(self), attr, + // val); + BA_PYTHON_INT_CATCH; +} + +auto PythonClassInputDevice::RemoveRemotePlayerFromGame( + PythonClassInputDevice* self) -> PyObject* { + BA_PYTHON_TRY; + InputDevice* d = self->input_device_->get(); + if (!d) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + d->RemoveRemotePlayerFromGame(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassInputDevice::GetDefaultPlayerName(PythonClassInputDevice* self) + -> PyObject* { + BA_PYTHON_TRY; + InputDevice* d = self->input_device_->get(); + if (!d) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + return PyUnicode_FromString(d->GetDefaultPlayerName().c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassInputDevice::GetPlayerProfiles(PythonClassInputDevice* self) + -> PyObject* { + BA_PYTHON_TRY; + InputDevice* d = self->input_device_->get(); + if (!d) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + if (PyObject* profiles = d->GetPlayerProfiles()) { + Py_INCREF(profiles); + return profiles; + } else { + return Py_BuildValue("{}"); // Empty dict. + } + BA_PYTHON_CATCH; +} + +auto PythonClassInputDevice::GetAccountName(PythonClassInputDevice* self, + PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + int full; + static const char* kwlist[] = {"full", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", + const_cast(kwlist), &full)) { + return nullptr; + } + InputDevice* d = self->input_device_->get(); + if (!d) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + return PyUnicode_FromString( + d->GetAccountName(static_cast(full)).c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassInputDevice::IsConnectedToRemotePlayer( + PythonClassInputDevice* self) -> PyObject* { + BA_PYTHON_TRY; + InputDevice* input_device = self->input_device_->get(); + if (!input_device) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + if (input_device->GetRemotePlayer()) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +auto PythonClassInputDevice::Exists(PythonClassInputDevice* self) -> PyObject* { + BA_PYTHON_TRY; + if (self->input_device_->exists()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +auto PythonClassInputDevice::GetAxisName(PythonClassInputDevice* self, + PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + int id; + static const char* kwlist[] = {"axis_id", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "i", + const_cast(kwlist), &id)) { + return nullptr; + } + InputDevice* input_device = self->input_device_->get(); + if (!input_device) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + return PyUnicode_FromString(input_device->GetAxisName(id).c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassInputDevice::GetButtonName(PythonClassInputDevice* self, + PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + int id{}; + static const char* kwlist[] = {"button_id", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "i", + const_cast(kwlist), &id)) { + return nullptr; + } + InputDevice* d = self->input_device_->get(); + if (!d) { + throw Exception(PyExcType::kInputDeviceNotFound); + } + + // Ask the input-device for the button name. + std::string bname = d->GetButtonName(id); + + // If this doesn't appear to be lstr json itself, convert it to that. + if (bname.length() < 1 || bname.c_str()[0] != '{') { + Utils::StringReplaceAll(&bname, "\"", "\\\""); + bname = R"({"v":")" + bname + "\"}"; + } + PythonRef args2(Py_BuildValue("(s)", bname.c_str()), PythonRef::kSteal); + PythonRef results = + g_python->obj(Python::ObjID::kLstrFromJsonCall).Call(args2); + return results.NewRef(); + + BA_PYTHON_CATCH; +} + +PyTypeObject PythonClassInputDevice::type_obj; +PyMethodDef PythonClassInputDevice::tp_methods[] = { + {"remove_remote_player_from_game", (PyCFunction)RemoveRemotePlayerFromGame, + METH_NOARGS, + "remove_remote_player_from_game() -> None\n" + "\n" + "(internal)"}, + {"is_connected_to_remote_player", (PyCFunction)IsConnectedToRemotePlayer, + METH_NOARGS, + "is_connected_to_remote_player() -> bool\n" + "\n" + "(internal)"}, + {"exists", (PyCFunction)Exists, METH_NOARGS, + "exists() -> bool\n" + "\n" + "Return whether the underlying device for this object is still present."}, + {"get_button_name", (PyCFunction)GetButtonName, + METH_VARARGS | METH_KEYWORDS, // NOLINT (signed bitwise ops) + "get_button_name(button_id: int) -> ba.Lstr\n" + "\n" + "Given a button ID, return a human-readable name for that key/button.\n" + "\n" + "Can return an empty string if the value is not meaningful to humans."}, + // NOLINTNEXTLINE (signed bitwise ops) + {"get_axis_name", (PyCFunction)GetAxisName, METH_VARARGS | METH_KEYWORDS, + "get_axis_name(axis_id: int) -> str\n" + "\n" + "Given an axis ID, return the name of the axis on this device.\n" + "\n" + "Can return an empty string if the value is not meaningful to humans."}, + {"get_default_player_name", (PyCFunction)GetDefaultPlayerName, METH_NOARGS, + "get_default_player_name() -> str\n" + "\n" + "(internal)\n" + "\n" + "Returns the default player name for this device. (used for the 'random'\n" + "profile)"}, + {"get_account_name", (PyCFunction)GetAccountName, + METH_VARARGS | METH_KEYWORDS, // NOLINT (signed bitwise ops) + "get_account_name(full: bool) -> str\n" + "\n" + "Returns the account name associated with this device.\n" + "\n" + "(can be used to get account names for remote players)"}, + {"get_player_profiles", (PyCFunction)GetPlayerProfiles, METH_NOARGS, + "get_player_profiles() -> dict\n" + "\n" + "(internal)"}, + {nullptr}}; // namespace ballistica + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_input_device.h b/src/ballistica/python/class/python_class_input_device.h new file mode 100644 index 00000000..a3508939 --- /dev/null +++ b/src/ballistica/python/class/python_class_input_device.h @@ -0,0 +1,52 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_INPUT_DEVICE_H_ +#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_INPUT_DEVICE_H_ + +#include "ballistica/core/object.h" +#include "ballistica/python/class/python_class.h" + +namespace ballistica { + +class PythonClassInputDevice : public PythonClass { + public: + static auto type_name() -> const char* { return "InputDevice"; } + static void SetupType(PyTypeObject* obj); + static auto Create(InputDevice* input_device) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + static PyTypeObject type_obj; + auto GetInputDevice() const -> InputDevice*; + + private: + static PyMethodDef tp_methods[]; + static auto tp_repr(PythonClassInputDevice* self) -> PyObject*; + static auto tp_getattro(PythonClassInputDevice* self, PyObject* attr) + -> PyObject*; + static auto tp_setattro(PythonClassInputDevice* self, PyObject* attr, + PyObject* val) -> int; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassInputDevice* self); + static auto nb_bool(PythonClassInputDevice* self) -> int; + static auto RemoveRemotePlayerFromGame(PythonClassInputDevice* self) + -> PyObject*; + static auto GetDefaultPlayerName(PythonClassInputDevice* self) -> PyObject*; + static auto GetPlayerProfiles(PythonClassInputDevice* self) -> PyObject*; + static auto GetAccountName(PythonClassInputDevice* self, PyObject* args, + PyObject* keywds) -> PyObject*; + static auto IsConnectedToRemotePlayer(PythonClassInputDevice* self) + -> PyObject*; + static auto Exists(PythonClassInputDevice* self) -> PyObject*; + static auto GetAxisName(PythonClassInputDevice* self, PyObject* args, + PyObject* keywds) -> PyObject*; + static auto GetButtonName(PythonClassInputDevice* self, PyObject* args, + PyObject* keywds) -> PyObject*; + static PyNumberMethods as_number_; + Object::WeakRef* input_device_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_INPUT_DEVICE_H_ diff --git a/src/ballistica/python/class/python_class_material.cc b/src/ballistica/python/class/python_class_material.cc new file mode 100644 index 00000000..21d5a93a --- /dev/null +++ b/src/ballistica/python/class/python_class_material.cc @@ -0,0 +1,712 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/class/python_class_material.h" + +#include +#include + +#include "ballistica/dynamics/material/impact_sound_material_action.h" +#include "ballistica/dynamics/material/material.h" +#include "ballistica/dynamics/material/material_component.h" +#include "ballistica/dynamics/material/material_condition_node.h" +#include "ballistica/dynamics/material/node_message_material_action.h" +#include "ballistica/dynamics/material/node_mod_material_action.h" +#include "ballistica/dynamics/material/node_user_message_material_action.h" +#include "ballistica/dynamics/material/part_mod_material_action.h" +#include "ballistica/dynamics/material/python_call_material_action.h" +#include "ballistica/dynamics/material/roll_sound_material_action.h" +#include "ballistica/dynamics/material/skid_sound_material_action.h" +#include "ballistica/dynamics/material/sound_material_action.h" +#include "ballistica/game/game.h" +#include "ballistica/game/host_activity.h" +#include "ballistica/python/python.h" + +namespace ballistica { + +// Ignore signed bitwise stuff since python macros do a lot of it. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +#pragma ide diagnostic ignored "RedundantCast" + +bool PythonClassMaterial::s_create_empty_ = false; +PyTypeObject PythonClassMaterial::type_obj; + +static void DoAddConditions(PyObject* cond_obj, + Object::Ref* c); +static void DoAddAction(PyObject* actions_obj, + std::vector >* actions); + +// Attrs we expose through our custom getattr/setattr. +#define ATTR_LABEL "label" + +// The set we expose via dir(). +static const char* extra_dir_attrs[] = {ATTR_LABEL, nullptr}; + +void PythonClassMaterial::SetupType(PyTypeObject* obj) { + PythonClass::SetupType(obj); + obj->tp_name = "ba.Material"; + obj->tp_repr = (reprfunc)tp_repr; + obj->tp_basicsize = sizeof(PythonClassMaterial); + + // clang-format off + obj->tp_doc = + "Material(label: str = None)\n" + "\n" + "An entity applied to game objects to modify collision behavior.\n" + "\n" + "Category: Gameplay Classes\n" + "\n" + "A material can affect physical characteristics, generate sounds,\n" + "or trigger callback functions when collisions occur.\n" + "\n" + "Materials are applied to 'parts', which are groups of one or more\n" + "rigid bodies created as part of a ba.Node. Nodes can have any number\n" + "of parts, each with its own set of materials. Generally materials are\n" + "specified as array attributes on the Node. The 'spaz' node, for\n" + "example, has various attributes such as 'materials',\n" + "'roller_materials', and 'punch_materials', which correspond to the\n" + "various parts it creates.\n" + "\n" + "Use ba.Material() to instantiate a blank material, and then use its\n" + "add_actions() method to define what the material does.\n" + "\n" + "Attributes:\n" + "\n" + " " ATTR_LABEL ": str\n" + " A label for the material; only used for debugging.\n"; + // clang-format on + + obj->tp_new = tp_new; + obj->tp_dealloc = (destructor)tp_dealloc; + obj->tp_methods = tp_methods; + obj->tp_getattro = (getattrofunc)tp_getattro; + obj->tp_setattro = (setattrofunc)tp_setattro; +} + +auto PythonClassMaterial::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = reinterpret_cast(type->tp_alloc(type, 0)); + if (self) { + BA_PYTHON_TRY; + + // Do anything that might throw an exception *before* our placement-new + // stuff so we don't have to worry about cleaning it up on errors. + if (!InGameThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the game thread (current is (" + + GetCurrentThreadName() + ")."); + } + PyObject* name_obj = Py_None; + std::string name; + Object::Ref m; + if (!s_create_empty_) { + static const char* kwlist[] = {"label", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|O", + const_cast(kwlist), &name_obj)) { + return nullptr; + } + if (name_obj != Py_None) { + name = Python::GetPyString(name_obj); + } else { + name = Python::GetPythonFileLocation(); + } + + if (HostActivity* host_activity = Context::current().GetHostActivity()) { + m = host_activity->NewMaterial(name); + m->set_py_object(reinterpret_cast(self)); + } else { + throw Exception("Can't create materials in this context.", + PyExcType::kContext); + } + } + self->material_ = new Object::Ref(m); + BA_PYTHON_NEW_CATCH; + } + return reinterpret_cast(self); +} + +void PythonClassMaterial::Delete(Object::Ref* m) { + assert(InGameThread()); + + // If we're the py-object for a material, clear them out. + if (m->exists()) { + assert((*m)->py_object() != nullptr); + (*m)->set_py_object(nullptr); + } + delete m; +} + +void PythonClassMaterial::tp_dealloc(PythonClassMaterial* self) { + BA_PYTHON_TRY; + + // These have to be deleted in the game thread - push a call if + // need be.. otherwise do it immediately. + if (!InGameThread()) { + Object::Ref* ptr = self->material_; + g_game->PushCall([ptr] { Delete(ptr); }); + } else { + Delete(self->material_); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +auto PythonClassMaterial::tp_repr(PythonClassMaterial* self) -> PyObject* { + BA_PYTHON_TRY; + return Py_BuildValue( + "s", + std::string("").c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassMaterial::tp_getattro(PythonClassMaterial* self, PyObject* attr) + -> PyObject* { + BA_PYTHON_TRY; + + // Assuming this will always be a str? + assert(PyUnicode_Check(attr)); + + const char* s = PyUnicode_AsUTF8(attr); + + if (!strcmp(s, ATTR_LABEL)) { + Material* material = self->material_->get(); + if (!material) { + throw Exception("Invalid Material.", PyExcType::kNotFound); + } + return PyUnicode_FromString(material->label().c_str()); + } + + // Fall back to generic behavior. + PyObject* val; + val = PyObject_GenericGetAttr(reinterpret_cast(self), attr); + return val; + BA_PYTHON_CATCH; +} + +auto PythonClassMaterial::tp_setattro(PythonClassMaterial* self, PyObject* attr, + PyObject* val) -> int { + BA_PYTHON_TRY; + assert(PyUnicode_Check(attr)); + + throw Exception("Attr '" + std::string(PyUnicode_AsUTF8(attr)) + + "' is not settable on Material objects.", + PyExcType::kAttribute); + + // return PyObject_GenericSetAttr(reinterpret_cast(self), attr, + // val); + BA_PYTHON_INT_CATCH; +} + +auto PythonClassMaterial::Dir(PythonClassMaterial* self) -> PyObject* { + BA_PYTHON_TRY; + + // Start with the standard python dir listing. + PyObject* dir_list = Python::generic_dir(reinterpret_cast(self)); + assert(PyList_Check(dir_list)); + + // ..and add in our custom attr names. + for (const char** name = extra_dir_attrs; *name != nullptr; name++) { + PyList_Append( + dir_list, + PythonRef(PyUnicode_FromString(*name), PythonRef::kSteal).get()); + } + PyList_Sort(dir_list); + return dir_list; + + BA_PYTHON_CATCH; +} + +auto PythonClassMaterial::AddActions(PythonClassMaterial* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + PyObject* conditions_obj{Py_None}; + PyObject* actions_obj{nullptr}; + const char* kwlist[] = {"actions", "conditions", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|O", + const_cast(kwlist), &actions_obj, + &conditions_obj)) { + return nullptr; + } + + Object::Ref conditions; + if (conditions_obj != Py_None) { + DoAddConditions(conditions_obj, &conditions); + } + + Material* m = self->material_->get(); + if (!m) { + throw Exception("Invalid Material.", PyExcType::kNotFound); + } + std::vector > actions; + if (PyTuple_Check(actions_obj)) { + Py_ssize_t size = PyTuple_GET_SIZE(actions_obj); + if (size > 0) { + // If the first item is a string, process this tuple as a single action. + if (PyUnicode_Check(PyTuple_GET_ITEM(actions_obj, 0))) { + DoAddAction(actions_obj, &actions); + } else { + // Otherwise each item is assumed to be an action. + for (Py_ssize_t i = 0; i < size; i++) { + DoAddAction(PyTuple_GET_ITEM(actions_obj, i), &actions); + } + } + } + } else { + PyErr_SetString(PyExc_AttributeError, + "expected a tuple for \"actions\" argument"); + return nullptr; + } + m->AddComponent(Object::New(conditions, actions)); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +PyMethodDef PythonClassMaterial::tp_methods[] = { + {"add_actions", (PyCFunction)AddActions, METH_VARARGS | METH_KEYWORDS, + "add_actions(actions: Tuple, conditions: Optional[Tuple] = None)\n" + " -> None\n" + "\n" + "Add one or more actions to the material, optionally with conditions.\n" + "\n" + "Conditions:\n" + "\n" + "Conditions are provided as tuples which can be combined to form boolean\n" + "logic. A single condition might look like ('condition_name', cond_arg),\n" + "or a more complex nested one might look like (('some_condition',\n" + "cond_arg), 'or', ('another_condition', cond2_arg)).\n" + "\n" + "'and', 'or', and 'xor' are available to chain together 2 conditions, as\n" + " seen above.\n" + "\n" + "Available Conditions:\n" + "\n" + "('they_have_material', material) - does the part we\'re hitting have a\n" + " given ba.Material?\n" + "\n" + "('they_dont_have_material', material) - does the part we\'re hitting\n" + " not have a given ba.Material?\n" + "\n" + "('eval_colliding') - is 'collide' true at this point in material\n" + " evaluation? (see the modify_part_collision action)\n" + "\n" + "('eval_not_colliding') - is 'collide' false at this point in material\n" + " evaluation? (see the modify_part_collision action)\n" + "\n" + "('we_are_younger_than', age) - is our part younger than 'age'\n" + " (in milliseconds)?\n" + "\n" + "('we_are_older_than', age) - is our part older than 'age'\n" + " (in milliseconds)?\n" + "\n" + "('they_are_younger_than', age) - is the part we're hitting younger than\n" + " 'age' (in milliseconds)?\n" + "\n" + "('they_are_older_than', age) - is the part we're hitting older than\n" + " 'age' (in milliseconds)?\n" + "\n" + "('they_are_same_node_as_us') - does the part we're hitting belong to\n" + " the same ba.Node as us?\n" + "\n" + "('they_are_different_node_than_us') - does the part we're hitting\n" + " belong to a different ba.Node than us?\n" + "\n" + "Actions:\n" + "\n" + "In a similar manner, actions are specified as tuples. Multiple actions\n" + "can be specified by providing a tuple of tuples.\n" + "\n" + "Available Actions:\n" + "\n" + "('call', when, callable) - calls the provided callable; 'when' can be\n" + " either 'at_connect' or 'at_disconnect'. 'at_connect' means to fire\n" + " when the two parts first come in contact; 'at_disconnect' means to\n" + " fire once they cease being in contact.\n" + "\n" + "('message', who, when, message_obj) - sends a message object; 'who' can\n" + " be either 'our_node' or 'their_node', 'when' can be 'at_connect' or\n" + " 'at_disconnect', and message_obj is the message object to send.\n" + " This has the same effect as calling the node's handlemessage()\n" + " method.\n" + "\n" + "('modify_part_collision', attr, value) - changes some characteristic\n" + " of the physical collision that will occur between our part and their\n" + " part. This change will remain in effect as long as the two parts\n" + " remain overlapping. This means if you have a part with a material\n" + " that turns 'collide' off against parts younger than 100ms, and it\n" + " touches another part that is 50ms old, it will continue to not\n" + " collide with that part until they separate, even if the 100ms\n" + " threshold is passed. Options for attr/value are: 'physical' (boolean\n" + " value; whether a *physical* response will occur at all), 'friction'\n" + " (float value; how friction-y the physical response will be),\n" + " 'collide' (boolean value; whether *any* collision will occur at all,\n" + " including non-physical stuff like callbacks), 'use_node_collide'\n" + " (boolean value; whether to honor modify_node_collision overrides for\n" + " this collision), 'stiffness' (float value, how springy the physical\n" + " response is), 'damping' (float value, how damped the physical\n" + " response is), 'bounce' (float value; how bouncy the physical response\n" + " is).\n" + "\n" + "('modify_node_collision', attr, value) - similar to\n" + " modify_part_collision, but operates at a node-level.\n" + " collision attributes set here will remain in effect as long as\n" + " *anything* from our part's node and their part's node overlap.\n" + " A key use of this functionality is to prevent new nodes from\n" + " colliding with each other if they appear overlapped;\n" + " if modify_part_collision is used, only the individual parts that\n" + " were overlapping would avoid contact, but other parts could still\n" + " contact leaving the two nodes 'tangled up'. Using\n" + " modify_node_collision ensures that the nodes must completely\n" + " separate before they can start colliding. Currently the only attr\n" + " available here is 'collide' (a boolean value).\n" + "\n" + "('sound', sound, volume) - plays a ba.Sound when a collision occurs, at\n" + " a given volume, regardless of the collision speed/etc.\n" + "\n" + "('impact_sound', sound, targetImpulse, volume) - plays a sound when a\n" + " collision occurs, based on the speed of impact. Provide a ba.Sound, a\n" + " target-impulse, and a volume.\n" + "\n" + "('skid_sound', sound, targetImpulse, volume) - plays a sound during a\n" + " collision when parts are 'scraping' against each other. Provide a\n" + " ba.Sound, a target-impulse, and a volume.\n" + "\n" + "('roll_sound', sound, targetImpulse, volume) - plays a sound during a\n" + " collision when parts are 'rolling' against each other. Provide a\n" + " ba.Sound, a target-impulse, and a volume.\n" + "\n" + "# example 1: create a material that lets us ignore\n" + "# collisions against any nodes we touch in the first\n" + "# 100 ms of our existence; handy for preventing us from\n" + "# exploding outward if we spawn on top of another object:\n" + "m = ba.Material()\n" + "m.add_actions(conditions=(('we_are_younger_than', 100),\n" + " 'or',('they_are_younger_than', 100)),\n" + " actions=('modify_node_collision', 'collide', False))\n" + "\n" + "# example 2: send a DieMessage to anything we touch, but cause\n" + "# no physical response. This should cause any ba.Actor to drop dead:\n" + "m = ba.Material()\n" + "m.add_actions(actions=(('modify_part_collision', 'physical', False),\n" + " ('message', 'their_node', 'at_connect',\n" + " ba.DieMessage())))\n" + "\n" + "# example 3: play some sounds when we're contacting the ground:\n" + "m = ba.Material()\n" + "m.add_actions(conditions=('they_have_material',\n" + " shared.footing_material),\n" + " actions=(('impact_sound', ba.getsound('metalHit'), 2, 5),\n" + " ('skid_sound', ba.getsound('metalSkid'), 2, 5)))\n" + "\n"}, + {"__dir__", (PyCFunction)Dir, METH_NOARGS, + "allows inclusion of our custom attrs in standard python dir()"}, + + {nullptr}}; + +void DoAddConditions(PyObject* cond_obj, + Object::Ref* c) { + assert(InGameThread()); + if (PyTuple_Check(cond_obj)) { + Py_ssize_t size = PyTuple_GET_SIZE(cond_obj); + if (size < 1) { + throw Exception("Malformed arguments.", PyExcType::kValue); + } + + PyObject* first = PyTuple_GET_ITEM(cond_obj, 0); + assert(first); + + // If the first element is a string, + // its a leaf node; process its elements as a single statement. + if (PyUnicode_Check(first)) { + (*c) = Object::New(); + (*c)->opmode = MaterialConditionNode::OpMode::LEAF_NODE; + int argc; + const char* cond_str = PyUnicode_AsUTF8(first); + bool first_arg_is_material = false; + if (!strcmp(cond_str, "they_have_material")) { + argc = 1; + first_arg_is_material = true; + (*c)->cond = MaterialCondition::kDstIsMaterial; + } else if (!strcmp(cond_str, "they_dont_have_material")) { + argc = 1; + first_arg_is_material = true; + (*c)->cond = MaterialCondition::kDstNotMaterial; + } else if (!strcmp(cond_str, "eval_colliding")) { + argc = 0; + (*c)->cond = MaterialCondition::kEvalColliding; + } else if (!strcmp(cond_str, "eval_not_colliding")) { + argc = 0; + (*c)->cond = MaterialCondition::kEvalNotColliding; + } else if (!strcmp(cond_str, "we_are_younger_than")) { + argc = 1; + (*c)->cond = MaterialCondition::kSrcYoungerThan; + } else if (!strcmp(cond_str, "we_are_older_than")) { + argc = 1; + (*c)->cond = MaterialCondition::kSrcOlderThan; + } else if (!strcmp(cond_str, "they_are_younger_than")) { + argc = 1; + (*c)->cond = MaterialCondition::kDstYoungerThan; + } else if (!strcmp(cond_str, "they_are_older_than")) { + argc = 1; + (*c)->cond = MaterialCondition::kDstOlderThan; + } else if (!strcmp(cond_str, "they_are_same_node_as_us")) { + argc = 0; + (*c)->cond = MaterialCondition::kSrcDstSameNode; + } else if (!strcmp(cond_str, "they_are_different_node_than_us")) { + argc = 0; + (*c)->cond = MaterialCondition::kSrcDstDiffNode; + } else { + throw Exception( + std::string("Invalid material condition: \"") + cond_str + "\".", + PyExcType::kValue); + } + if (size != (argc + 1)) { + throw Exception( + std::string("Wrong number of arguments for condition: \"") + + cond_str + "\".", + PyExcType::kValue); + } + if (argc > 0) { + if (first_arg_is_material) { + (*c)->val1_material = + Python::GetPyMaterial(PyTuple_GET_ITEM(cond_obj, 1)); + } else { + PyObject* o = PyTuple_GET_ITEM(cond_obj, 1); + if (!PyLong_Check(o)) { + throw Exception( + std::string("Expected int for first arg of condition: \"") + + cond_str + "\".", + PyExcType::kType); + } + (*c)->val1 = static_cast(PyLong_AsLong(o)); + } + } + if (argc > 1) { + PyObject* o = PyTuple_GET_ITEM(cond_obj, 2); + if (!PyLong_Check(o)) { + throw Exception( + std::string("Expected int for second arg of condition: \"") + + cond_str + "\".", + PyExcType::kType); + } + (*c)->val1 = static_cast(PyLong_AsLong(o)); + } + } else if (PyTuple_Check(first)) { + // First item is a tuple - assume its a tuple of size 3+2*n + // containing tuples for odd index vals and operators for even. + if (size < 3 || (size % 2 != 1)) { + throw Exception("Malformed conditional statement.", PyExcType::kValue); + } + Object::Ref c2; + Object::Ref c2_prev; + for (Py_ssize_t i = 0; i < (size - 1); i += 2) { + c2 = Object::New(); + if (c2_prev.exists()) { + c2->left_child = c2_prev; + } else { + DoAddConditions(PyTuple_GET_ITEM(cond_obj, i), &c2->left_child); + } + DoAddConditions(PyTuple_GET_ITEM(cond_obj, i + 2), &c2->right_child); + + // Pull a string from between to set up our opmode with. + std::string opmode_str = + Python::GetPyString(PyTuple_GET_ITEM(cond_obj, i + 1)); + const char* opmode = opmode_str.c_str(); + if (!strcmp(opmode, "&&") || !strcmp(opmode, "and")) { + c2->opmode = MaterialConditionNode::OpMode::AND_OPERATOR; + } else if (!strcmp(opmode, "||") || !strcmp(opmode, "or")) { + c2->opmode = MaterialConditionNode::OpMode::OR_OPERATOR; + } else if (!strcmp(opmode, "^") || !strcmp(opmode, "xor")) { + c2->opmode = MaterialConditionNode::OpMode::XOR_OPERATOR; + } else { + throw Exception( + std::string("Invalid conditional operator: \"") + opmode + "\".", + PyExcType::kValue); + } + c2_prev = c2; + } + // Keep our lowest level. + (*c) = c2; + } + } else { + throw Exception("Conditions argument not a tuple.", PyExcType::kType); + } +} + +void DoAddAction(PyObject* actions_obj, + std::vector >* actions) { + assert(InGameThread()); + if (!PyTuple_Check(actions_obj)) { + throw Exception("Expected a tuple.", PyExcType::kType); + } + Py_ssize_t size = PyTuple_GET_SIZE(actions_obj); + assert(size > 0); + PyObject* obj = PyTuple_GET_ITEM(actions_obj, 0); + std::string type = Python::GetPyString(obj); + if (type == "call") { + if (size != 3) { + throw Exception("Expected 3 values for command action tuple.", + PyExcType::kValue); + } + std::string when = Python::GetPyString(PyTuple_GET_ITEM(actions_obj, 1)); + bool at_disconnect; + if (when == "at_connect") { + at_disconnect = false; + } else if (when == "at_disconnect") { + at_disconnect = true; + } else { + throw Exception("Invalid command execution time: '" + when + "'.", + PyExcType::kValue); + } + PyObject* call_obj = PyTuple_GET_ITEM(actions_obj, 2); + (*actions).push_back(Object::New( + at_disconnect, call_obj)); + } else if (type == "message") { + if (size < 4) { + throw Exception("Expected >= 4 values for message action tuple.", + PyExcType::kValue); + } + std::string target = Python::GetPyString(PyTuple_GET_ITEM(actions_obj, 1)); + bool target_other_val; + if (target == "our_node") { + target_other_val = false; + } else if (target == "their_node") { + target_other_val = true; + } else { + throw Exception("Invalid message target: '" + target + "'.", + PyExcType::kValue); + } + std::string when = Python::GetPyString(PyTuple_GET_ITEM(actions_obj, 2)); + bool at_disconnect; + if (when == "at_connect") { + at_disconnect = false; + } else if (when == "at_disconnect") { + at_disconnect = true; + } else { + throw Exception("Invalid command execution time: '" + when + "'.", + PyExcType::kValue); + } + + // Pull the rest of the message. + Buffer b; + PyObject* user_message_obj = nullptr; + Python::DoBuildNodeMessage(actions_obj, 3, &b, &user_message_obj); + if (user_message_obj) { + (*actions).push_back( + Object::New( + target_other_val, at_disconnect, user_message_obj)); + } else if (b.size() > 0) { + (*actions).push_back( + Object::New( + target_other_val, at_disconnect, b.data(), b.size())); + } + } else if (type == "modify_node_collision") { + if (size != 3) { + throw Exception( + "Expected 3 values for modify_node_collision action tuple.", + PyExcType::kValue); + } + std::string attr = Python::GetPyString(PyTuple_GET_ITEM(actions_obj, 1)); + NodeCollideAttr attr_type; + if (attr == "collide") { + attr_type = NodeCollideAttr::kCollideNode; + } else { + throw Exception("Invalid node mod attr: '" + attr + "'.", + PyExcType::kValue); + } + + // Pull value. + float val = Python::GetPyFloat(PyTuple_GET_ITEM(actions_obj, 2)); + (*actions).push_back( + Object::New(attr_type, val)); + } else if (type == "modify_part_collision") { + if (size != 3) { + throw Exception( + "Expected 3 values for modify_part_collision action tuple.", + PyExcType::kValue); + } + PartCollideAttr attr_type; + std::string attr = Python::GetPyString(PyTuple_GET_ITEM(actions_obj, 1)); + if (attr == "physical") { + attr_type = PartCollideAttr::kPhysical; + } else if (attr == "friction") { + attr_type = PartCollideAttr::kFriction; + } else if (attr == "collide") { + attr_type = PartCollideAttr::kCollide; + } else if (attr == "use_node_collide") { + attr_type = PartCollideAttr::kUseNodeCollide; + } else if (attr == "stiffness") { + attr_type = PartCollideAttr::kStiffness; + } else if (attr == "damping") { + attr_type = PartCollideAttr::kDamping; + } else if (attr == "bounce") { + attr_type = PartCollideAttr::kBounce; + } else { + throw Exception("Invalid part mod attr: '" + attr + "'.", + PyExcType::kValue); + } + float val = Python::GetPyFloat(PyTuple_GET_ITEM(actions_obj, 2)); + (*actions).push_back( + Object::New(attr_type, val)); + } else if (type == "sound") { + if (size != 3) { + throw Exception("Expected 3 values for sound action tuple.", + PyExcType::kValue); + } + Sound* sound = Python::GetPySound(PyTuple_GET_ITEM(actions_obj, 1)); + float volume = Python::GetPyFloat(PyTuple_GET_ITEM(actions_obj, 2)); + (*actions).push_back( + Object::New(sound, volume)); + } else if (type == "impact_sound") { + if (size != 4) { + throw Exception("Expected 4 values for impact_sound action tuple.", + PyExcType::kValue); + } + PyObject* sounds_obj = PyTuple_GET_ITEM(actions_obj, 1); + std::vector sounds; + if (PySequence_Check(sounds_obj)) { + sounds = Python::GetPySounds(sounds_obj); // Sequence of sounds. + } else { + sounds.push_back(Python::GetPySound(sounds_obj)); // Single sound. + } + if (sounds.empty()) { + throw Exception("Require at least 1 sound.", PyExcType::kValue); + } + if (Utils::HasNullMembers(sounds)) { + throw Exception("One or more invalid sound refs passed.", + PyExcType::kValue); + } + float target_impulse = Python::GetPyFloat(PyTuple_GET_ITEM(actions_obj, 2)); + float volume = Python::GetPyFloat(PyTuple_GET_ITEM(actions_obj, 3)); + (*actions).push_back(Object::New( + sounds, target_impulse, volume)); + } else if (type == "skid_sound") { + if (size != 4) { + throw Exception("Expected 4 values for skid_sound action tuple.", + PyExcType::kValue); + } + Sound* sound = Python::GetPySound(PyTuple_GET_ITEM(actions_obj, 1)); + float target_impulse = Python::GetPyFloat(PyTuple_GET_ITEM(actions_obj, 2)); + float volume = Python::GetPyFloat(PyTuple_GET_ITEM(actions_obj, 3)); + (*actions).push_back(Object::New( + sound, target_impulse, volume)); + } else if (type == "roll_sound") { + if (size != 4) { + throw Exception("Expected 4 values for roll_sound action tuple.", + PyExcType::kValue); + } + Sound* sound = Python::GetPySound(PyTuple_GET_ITEM(actions_obj, 1)); + float target_impulse = Python::GetPyFloat(PyTuple_GET_ITEM(actions_obj, 2)); + float volume = Python::GetPyFloat(PyTuple_GET_ITEM(actions_obj, 3)); + (*actions).push_back(Object::New( + sound, target_impulse, volume)); + } else { + throw Exception("Invalid action type: '" + type + "'.", PyExcType::kValue); + } +} + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_material.h b/src/ballistica/python/class/python_class_material.h new file mode 100644 index 00000000..7e759903 --- /dev/null +++ b/src/ballistica/python/class/python_class_material.h @@ -0,0 +1,46 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_MATERIAL_H_ +#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_MATERIAL_H_ + +#include "ballistica/core/object.h" +#include "ballistica/python/class/python_class.h" + +namespace ballistica { + +class PythonClassMaterial : public PythonClass { + public: + static auto type_name() -> const char* { return "Material"; } + static void SetupType(PyTypeObject* obj); + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + static PyTypeObject type_obj; + + auto GetMaterial(bool doraise = true) const -> Material* { + Material* m = material_->get(); + if ((!m) && doraise) throw Exception("Invalid Material"); + return m; + } + + private: + static bool s_create_empty_; + static PyMethodDef tp_methods[]; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void Delete(Object::Ref* m); + static void tp_dealloc(PythonClassMaterial* self); + static auto tp_getattro(PythonClassMaterial* self, PyObject* attr) + -> PyObject*; + static auto tp_setattro(PythonClassMaterial* self, PyObject* attr, + PyObject* val) -> int; + static auto tp_repr(PythonClassMaterial* self) -> PyObject*; + static auto AddActions(PythonClassMaterial* self, PyObject* args, + PyObject* keywds) -> PyObject*; + static auto Dir(PythonClassMaterial* self) -> PyObject*; + Object::Ref* material_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_MATERIAL_H_ diff --git a/src/ballistica/python/class/python_class_model.cc b/src/ballistica/python/class/python_class_model.cc new file mode 100644 index 00000000..65966b02 --- /dev/null +++ b/src/ballistica/python/class/python_class_model.cc @@ -0,0 +1,109 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/class/python_class_model.h" + +#include + +#include "ballistica/game/game.h" +#include "ballistica/media/component/model.h" + +namespace ballistica { + +auto PythonClassModel::tp_repr(PythonClassModel* self) -> PyObject* { + BA_PYTHON_TRY; + Object::Ref m = *(self->model_); + return Py_BuildValue( + "s", (std::string("name() + "\"") : "(empty ref)") + ">") + .c_str()); + BA_PYTHON_CATCH; +} + +void PythonClassModel::SetupType(PyTypeObject* obj) { + PythonClass::SetupType(obj); + obj->tp_name = "ba.Model"; + obj->tp_basicsize = sizeof(PythonClassModel); + obj->tp_doc = + "A reference to a model.\n" + "\n" + "Category: Asset Classes\n" + "\n" + "Models are used for drawing.\n" + "Use ba.getmodel() to instantiate one."; + obj->tp_repr = (reprfunc)tp_repr; + obj->tp_new = tp_new; + obj->tp_dealloc = (destructor)tp_dealloc; +} + +auto PythonClassModel::Create(Model* model) -> PyObject* { + s_create_empty_ = true; // prevent class from erroring on create + auto* t = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + s_create_empty_ = false; + if (!t) { + throw Exception("ba.Model creation failed."); + } + *(t->model_) = model; + return reinterpret_cast(t); +} + +auto PythonClassModel::GetModel(bool doraise) const -> Model* { + Model* model = model_->get(); + if (!model && doraise) { + throw Exception("Invalid Model.", PyExcType::kNotFound); + } + return model; +} + +auto PythonClassModel::tp_new(PyTypeObject* type, PyObject* args, + PyObject* kwds) -> PyObject* { + auto* self = reinterpret_cast(type->tp_alloc(type, 0)); + if (self) { + BA_PYTHON_TRY; + if (!InGameThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the game thread (current is (" + + GetCurrentThreadName() + ")."); + } + if (!s_create_empty_) { + throw Exception( + "Can't instantiate Models directly; use ba.getmodel() to get " + "them."); + } + self->model_ = new Object::Ref(); + BA_PYTHON_NEW_CATCH; + } + return reinterpret_cast(self); +} + +void PythonClassModel::Delete(Object::Ref* ref) { + assert(InGameThread()); + + // if we're the py-object for a model, clear them out + // (FIXME - we should pass the old pointer in here to sanity-test that we + // were their ref) + if (ref->exists()) { + (*ref)->ClearPyObject(); + } + delete ref; +} + +void PythonClassModel::tp_dealloc(PythonClassModel* self) { + BA_PYTHON_TRY; + // these have to be deleted in the game thread - send the ptr along if need + // be; otherwise do it immediately + if (!InGameThread()) { + Object::Ref* m = self->model_; + g_game->PushCall([m] { Delete(m); }); + } else { + Delete(self->model_); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +bool PythonClassModel::s_create_empty_ = false; +PyTypeObject PythonClassModel::type_obj; + +} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_model.h b/src/ballistica/python/class/python_class_model.h new file mode 100644 index 00000000..b6ca9f7c --- /dev/null +++ b/src/ballistica/python/class/python_class_model.h @@ -0,0 +1,34 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_MODEL_H_ +#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_MODEL_H_ + +#include "ballistica/core/object.h" +#include "ballistica/python/class/python_class.h" + +namespace ballistica { + +class PythonClassModel : public PythonClass { + public: + static auto type_name() -> const char* { return "Model"; } + static auto tp_repr(PythonClassModel* self) -> PyObject*; + static void SetupType(PyTypeObject* obj); + static PyTypeObject type_obj; + static auto Create(Model* model) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + auto GetModel(bool doraise = true) const -> Model*; + + private: + static bool s_create_empty_; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) + -> PyObject*; + static void tp_dealloc(PythonClassModel* self); + static void Delete(Object::Ref* ref); + Object::Ref* model_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_MODEL_H_ diff --git a/src/ballistica/python/class/python_class_node.cc b/src/ballistica/python/class/python_class_node.cc new file mode 100644 index 00000000..cee7795e --- /dev/null +++ b/src/ballistica/python/class/python_class_node.cc @@ -0,0 +1,458 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/class/python_class_node.h" + +#include +#include + +#include "ballistica/game/game_stream.h" +#include "ballistica/python/python.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +// Ignore a few things that python macros do. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +#pragma ide diagnostic ignored "RedundantCast" + +PyNumberMethods PythonClassNode::as_number_; + +void PythonClassNode::SetupType(PyTypeObject* obj) { + PythonClass::SetupType(obj); + obj->tp_repr = (reprfunc)tp_repr; + obj->tp_name = "ba.Node"; + obj->tp_basicsize = sizeof(PythonClassNode); + obj->tp_doc = + "Reference to a Node; the low level building block of the game.\n" + "\n" + "Category: Gameplay Classes\n" + "\n" + "At its core, a game is nothing more than a scene of Nodes\n" + "with attributes getting interconnected or set over time.\n" + "\n" + "A ba.Node instance should be thought of as a weak-reference\n" + "to a game node; *not* the node itself. This means a Node's\n" + "lifecycle is completely independent of how many Python references\n" + "to it exist. To explicitly add a new node to the game, use\n" + "ba.newnode(), and to explicitly delete one, use ba.Node.delete().\n" + "ba.Node.exists() can be used to determine if a Node still points to\n" + "a live node in the game.\n" + "\n" + "You can use ba.Node(None) to instantiate an invalid\n" + "Node reference (sometimes used as attr values/etc)."; + obj->tp_new = tp_new; + obj->tp_dealloc = (destructor)tp_dealloc; + obj->tp_getattro = (getattrofunc)tp_getattro; + obj->tp_setattro = (setattrofunc)tp_setattro; + obj->tp_methods = tp_methods; + + // We provide number methods only for bool functionality. + memset(&as_number_, 0, sizeof(as_number_)); + as_number_.nb_bool = (inquiry)nb_bool; + obj->tp_as_number = &as_number_; +} + +auto PythonClassNode::Create(Node* node) -> PyObject* { + // Make sure we only have one python ref per node. + if (node) { + assert(!node->has_py_ref()); + } + + s_create_empty_ = true; // Prevent class from erroring on create. + auto* py_node = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + s_create_empty_ = false; + if (!py_node) { + throw Exception("ba.Node creation failed."); + } + + *(py_node->node_) = node; + return reinterpret_cast(py_node); +} + +auto PythonClassNode::GetNode(bool doraise) const -> Node* { + Node* n = node_->get(); + if (!n && doraise) { + throw Exception(PyExcType::kNodeNotFound); + } + return n; +} + +auto PythonClassNode::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = reinterpret_cast(type->tp_alloc(type, 0)); + if (self) { + BA_PYTHON_TRY; + if (!InGameThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the game thread (current is (" + + GetCurrentThreadName() + ")."); + } + if (!s_create_empty_) { + if (!PyTuple_Check(args) || (PyTuple_GET_SIZE(args) != 1) + || (keywds != nullptr) || (PyTuple_GET_ITEM(args, 0) != Py_None)) { + throw Exception( + "Can't create Nodes this way; use ba.newnode() or use " + "ba.Node(None) to get an invalid reference."); + } + } + self->node_ = new Object::WeakRef(); + BA_PYTHON_NEW_CATCH; + } + return reinterpret_cast(self); +} + +void PythonClassNode::tp_dealloc(PythonClassNode* self) { + BA_PYTHON_TRY; + // These have to be deleted in the game thread; send the ptr along if need + // be; otherwise do it immediately. + if (!InGameThread()) { + Object::WeakRef* n = self->node_; + g_game->PushCall([n] { delete n; }); + } else { + delete self->node_; + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +auto PythonClassNode::tp_repr(PythonClassNode* self) -> PyObject* { + BA_PYTHON_TRY; + Node* node = self->node_->get(); + return Py_BuildValue( + "s", + std::string("id()) + " ") : "") + + (node ? ("'" + node->label() + "'") : "(empty ref)") + ">") + .c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassNode::tp_getattro(PythonClassNode* self, PyObject* attr) + -> PyObject* { + BA_PYTHON_TRY; + + // Do we need to support other attr types? + assert(PyUnicode_Check(attr)); + + // If our node exists and has this attr, return it. + // Otherwise do default python path. + Node* node = self->node_->get(); + const char* attr_name = PyUnicode_AsUTF8(attr); + if (node && node->HasAttribute(attr_name)) { + return Python::GetNodeAttr(node, attr_name); + } else { + return PyObject_GenericGetAttr(reinterpret_cast(self), attr); + } + BA_PYTHON_CATCH; +} + +auto PythonClassNode::Exists(PythonClassNode* self) -> PyObject* { + BA_PYTHON_TRY; + if (self->node_->exists()) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +auto PythonClassNode::GetNodeType(PythonClassNode* self) -> PyObject* { + BA_PYTHON_TRY; + + Node* node = self->node_->get(); + if (!node) { + throw Exception(PyExcType::kNodeNotFound); + } + return PyUnicode_FromString(node->type()->name().c_str()); + + BA_PYTHON_CATCH; +} + +auto PythonClassNode::GetName(PythonClassNode* self) -> PyObject* { + BA_PYTHON_TRY; + + Node* node = self->node_->get(); + if (!node) { + throw Exception(PyExcType::kNodeNotFound); + } + return PyUnicode_FromString(node->label().c_str()); + + BA_PYTHON_CATCH; +} + +auto PythonClassNode::GetDelegate(PythonClassNode* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + static const char* kwlist[] = {"type", "doraise", nullptr}; + PyObject* type_obj{}; + int doraise{}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|p", + const_cast(kwlist), &type_obj, + &doraise)) { + return nullptr; + } + Node* node = self->node_->get(); + if (!node) { + throw Exception(PyExcType::kNodeNotFound); + } + if (!PyType_Check(type_obj)) { + throw Exception("Passed type arg is not a type.", PyExcType::kType); + } + if (PyObject* obj = node->GetDelegate()) { + int isinst = PyObject_IsInstance(obj, type_obj); + if (isinst == -1) { + return nullptr; + } + if (isinst) { + Py_INCREF(obj); + return obj; + } else { + if (doraise) { + throw Exception("Requested delegate type not found on '" + + node->type()->name() + + "' node. (type=" + Python::ObjToString(type_obj) + + ", delegate=" + Python::ObjToString(obj) + ")", + PyExcType::kDelegateNotFound); + } + } + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassNode::Delete(PythonClassNode* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + int ignore_missing = 1; + static const char* kwlist[] = {"ignore_missing", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "|i", const_cast(kwlist), &ignore_missing)) { + return nullptr; + } + Node* node = self->node_->get(); + if (!node) { + if (!ignore_missing) { + throw Exception(PyExcType::kNodeNotFound); + } + } else { + node->scene()->DeleteNode(node); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassNode::HandleMessage(PythonClassNode* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + Py_ssize_t tuple_size = PyTuple_GET_SIZE(args); + if (tuple_size < 1) { + PyErr_SetString(PyExc_AttributeError, "must provide at least 1 arg"); + return nullptr; + } + Buffer b; + PyObject* user_message_obj; + Python::DoBuildNodeMessage(args, 0, &b, &user_message_obj); + + // Should we fail if the node doesn't exist?? + Node* node = self->node_->get(); + if (node) { + HostActivity* host_activity = node->context().GetHostActivity(); + if (!host_activity) { + throw Exception("Invalid context.", PyExcType::kContext); + } + // For user messages we pass them directly to the node + // since by their nature they don't go out over the network and are just + // for use within the scripting system. + if (user_message_obj) { + node->DispatchUserMessage(user_message_obj, "Node User-Message dispatch"); + } else { + if (GameStream* output_stream = node->scene()->GetGameStream()) { + output_stream->NodeMessage(node, b.data(), b.size()); + } + node->DispatchNodeMessage(b.data()); + } + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassNode::AddDeathAction(PythonClassNode* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + PyObject* call_obj; + if (!PyArg_ParseTuple(args, "O", &call_obj)) { + return nullptr; + } + Node* n = self->node_->get(); + if (!n) { + throw Exception(PyExcType::kNodeNotFound); + } + + // We don't have to go through a host-activity but lets make sure we're in + // one. + HostActivity* host_activity = n->context().GetHostActivity(); + if (!host_activity) { + throw Exception("Invalid context.", PyExcType::kContext); + } + n->AddNodeDeathAction(call_obj); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassNode::ConnectAttr(PythonClassNode* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + PyObject* dst_node_obj; + Node* node = self->node_->get(); + if (!node) { + throw Exception(PyExcType::kNodeNotFound); + } + char *src_attr_name, *dst_attr_name; + if (!PyArg_ParseTuple(args, "sOs", &src_attr_name, &dst_node_obj, + &dst_attr_name)) { + return nullptr; + } + + // Allow dead-refs and None. + Node* dst_node = Python::GetPyNode(dst_node_obj, true, true); + if (!dst_node) { + throw Exception(PyExcType::kNodeNotFound); + } + NodeAttributeUnbound* src_attr = + node->type()->GetAttribute(std::string(src_attr_name)); + NodeAttributeUnbound* dst_attr = + dst_node->type()->GetAttribute(std::string(dst_attr_name)); + + // Push to output_stream first to catch scene mismatch errors. + if (GameStream* output_stream = node->scene()->GetGameStream()) { + output_stream->ConnectNodeAttribute(node, src_attr, dst_node, dst_attr); + } + + // Now apply locally. + node->ConnectAttribute(src_attr, dst_node, dst_attr); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassNode::Dir(PythonClassNode* self) -> PyObject* { + BA_PYTHON_TRY; + + // Start with the standard python dir listing. + PyObject* dir_list = Python::generic_dir(reinterpret_cast(self)); + assert(PyList_Check(dir_list)); + + // ..now grab all this guy's BA attributes and add them in. + Node* node = self->node_->get(); + if (node) { + std::list attrs; + node->ListAttributes(&attrs); + for (auto& attr : attrs) { + PyList_Append(dir_list, PythonRef(PyUnicode_FromString(attr.c_str()), + PythonRef::kSteal) + .get()); + } + } + PyList_Sort(dir_list); + return dir_list; + BA_PYTHON_CATCH; +} + +auto PythonClassNode::nb_bool(PythonClassNode* self) -> int { + return self->node_->exists(); +} + +auto PythonClassNode::tp_setattro(PythonClassNode* self, PyObject* attr, + PyObject* val) -> int { + BA_PYTHON_TRY; + + // FIXME: do we need to support other attr types? + assert(PyUnicode_Check(attr)); + Node* n = self->node_->get(); + if (!n) { + throw Exception(PyExcType::kNodeNotFound); + } + Python::SetNodeAttr(n, PyUnicode_AsUTF8(attr), val); + return 0; + BA_PYTHON_INT_CATCH; +} + +PyMethodDef PythonClassNode::tp_methods[] = { + {"exists", (PyCFunction)Exists, METH_NOARGS, + "exists() -> bool\n" + "\n" + "Returns whether the Node still exists.\n" + "Most functionality will fail on a nonexistent Node, so it's never a bad\n" + "idea to check this.\n" + "\n" + "Note that you can also use the boolean operator for this same\n" + "functionality, so a statement such as \"if mynode\" will do\n" + "the right thing both for Node objects and values of None."}, + {"getnodetype", (PyCFunction)GetNodeType, METH_NOARGS, + "getnodetype() -> str\n" + "\n" + "Return the type of Node referenced by this object as a string.\n" + "(Note this is different from the Python type which is always ba.Node)"}, + {"getname", (PyCFunction)GetName, METH_NOARGS, + "getname() -> str\n" + "\n" + "Return the name assigned to a Node; used mainly for debugging"}, + {"getdelegate", (PyCFunction)GetDelegate, METH_VARARGS | METH_KEYWORDS, + "getdelegate(type: Type, doraise: bool = False) -> \n" + "\n" + "Return the node's current delegate object if it matches a certain type.\n" + "\n" + "If the node has no delegate or it is not an instance of the passed\n" + "type, then None will be returned. If 'doraise' is True, then an\n" + "ba.DelegateNotFoundError will be raised instead."}, + {"delete", (PyCFunction)Delete, METH_VARARGS | METH_KEYWORDS, + "delete(ignore_missing: bool = True) -> None\n" + "\n" + "Delete the node. Ignores already-deleted nodes if ignore_missing\n" + "is True; otherwise a ba.NodeNotFoundError is thrown."}, + {"handlemessage", (PyCFunction)HandleMessage, METH_VARARGS, + "handlemessage(*args: Any) -> None\n" + "\n" + "General message handling; can be passed any message object.\n" + "\n" + "All standard message objects are forwarded along to the ba.Node's\n" + "delegate for handling (generally the ba.Actor that made the node).\n" + "\n" + "ba.Nodes are unique, however, in that they can be passed a second\n" + "form of message; 'node-messages'. These consist of a string type-name\n" + "as a first argument along with the args specific to that type name\n" + "as additional arguments.\n" + "Node-messages communicate directly with the low-level node layer\n" + "and are delivered simultaneously on all game clients,\n" + "acting as an alternative to setting node attributes."}, + {"add_death_action", (PyCFunction)AddDeathAction, METH_VARARGS, + "add_death_action(action: Callable[[], None]) -> None\n" + "\n" + "Add a callable object to be called upon this node's death.\n" + "Note that these actions are run just after the node dies, not before.\n"}, + {"connectattr", (PyCFunction)ConnectAttr, METH_VARARGS, + "connectattr(srcattr: str, dstnode: Node, dstattr: str) -> None\n" + "\n" + "Connect one of this node's attributes to an attribute on another node.\n" + "This will immediately set the target attribute's value to that of the\n" + "source attribute, and will continue to do so once per step as long as\n" + "the two nodes exist. The connection can be severed by setting the\n" + "target attribute to any value or connecting another node attribute\n" + "to it.\n" + "\n" + "# Example: create a locator and attach a light to it:\n" + "light = ba.newnode('light')\n" + "loc = ba.newnode('locator', attrs={'position': (0,10,0)})\n" + "loc.connectattr('position', light, 'position')"}, + {"__dir__", (PyCFunction)Dir, METH_NOARGS, + "allows inclusion of our custom attrs in standard python dir()"}, + {nullptr}}; + +bool PythonClassNode::s_create_empty_ = false; +PyTypeObject PythonClassNode::type_obj; + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_node.h b/src/ballistica/python/class/python_class_node.h new file mode 100644 index 00000000..6e19773b --- /dev/null +++ b/src/ballistica/python/class/python_class_node.h @@ -0,0 +1,52 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_NODE_H_ +#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_NODE_H_ + +#include "ballistica/core/object.h" +#include "ballistica/python/class/python_class.h" +#include "ballistica/scene/node/node_type.h" + +namespace ballistica { + +class PythonClassNode : public PythonClass { + public: + static auto type_name() -> const char* { return "Node"; } + static void SetupType(PyTypeObject* obj); + static auto Create(Node* node) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + static PyTypeObject type_obj; + auto GetNode(bool doraise = true) const -> Node*; + + private: + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassNode* self); + static auto tp_repr(PythonClassNode* self) -> PyObject*; + static auto tp_getattro(PythonClassNode* self, PyObject* attr) -> PyObject*; + static auto tp_setattro(PythonClassNode* self, PyObject* attr, PyObject* val) + -> int; + static auto Exists(PythonClassNode* self) -> PyObject*; + static auto GetNodeType(PythonClassNode* self) -> PyObject*; + static auto GetName(PythonClassNode* self) -> PyObject*; + static auto GetDelegate(PythonClassNode* self, PyObject* args, + PyObject* keywds) -> PyObject*; + static auto Delete(PythonClassNode* self, PyObject* args, PyObject* keywds) + -> PyObject*; + static auto HandleMessage(PythonClassNode* self, PyObject* args) -> PyObject*; + static auto AddDeathAction(PythonClassNode* self, PyObject* args) + -> PyObject*; + static auto ConnectAttr(PythonClassNode* self, PyObject* args) -> PyObject*; + static auto Dir(PythonClassNode* self) -> PyObject*; + static auto nb_bool(PythonClassNode* self) -> int; + static bool s_create_empty_; + static PyMethodDef tp_methods[]; + Object::WeakRef* node_; + static PyNumberMethods as_number_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_NODE_H_ diff --git a/src/ballistica/python/class/python_class_session_data.cc b/src/ballistica/python/class/python_class_session_data.cc new file mode 100644 index 00000000..57aadb34 --- /dev/null +++ b/src/ballistica/python/class/python_class_session_data.cc @@ -0,0 +1,114 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/class/python_class_session_data.h" + +#include + +#include "ballistica/game/game.h" +#include "ballistica/game/session/session.h" +#include "ballistica/generic/utils.h" + +namespace ballistica { + +auto PythonClassSessionData::nb_bool(PythonClassSessionData* self) -> int { + return self->session_->exists(); +} + +PyNumberMethods PythonClassSessionData::as_number_; + +void PythonClassSessionData::SetupType(PyTypeObject* obj) { + PythonClass::SetupType(obj); + obj->tp_name = "_ba.SessionData"; + obj->tp_basicsize = sizeof(PythonClassSessionData); + obj->tp_doc = "(internal)"; + obj->tp_new = tp_new; + obj->tp_dealloc = (destructor)tp_dealloc; + obj->tp_repr = (reprfunc)tp_repr; + obj->tp_methods = tp_methods; + + // We provide number methods only for bool functionality. + memset(&as_number_, 0, sizeof(as_number_)); + as_number_.nb_bool = (inquiry)nb_bool; + obj->tp_as_number = &as_number_; +} + +auto PythonClassSessionData::Create(Session* session) -> PyObject* { + auto* py_session_data = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + BA_PRECONDITION(py_session_data); + *(py_session_data->session_) = session; + return reinterpret_cast(py_session_data); +} + +auto PythonClassSessionData::GetSession() const -> Session* { + Session* session = session_->get(); + if (!session) { + throw Exception("Invalid SessionData.", PyExcType::kSessionNotFound); + } + return session; +} + +auto PythonClassSessionData::tp_repr(PythonClassSessionData* self) + -> PyObject* { + BA_PYTHON_TRY; + return Py_BuildValue("s", (std::string("session_->get()) + " >") + .c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassSessionData::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = + reinterpret_cast(type->tp_alloc(type, 0)); + if (self) { + BA_PYTHON_TRY; + if (!InGameThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the game thread (current is (" + + GetCurrentThreadName() + ")."); + } + self->session_ = new Object::WeakRef(); + BA_PYTHON_NEW_CATCH; + } + return reinterpret_cast(self); +} + +void PythonClassSessionData::tp_dealloc(PythonClassSessionData* self) { + BA_PYTHON_TRY; + // These have to be deleted in the game thread; + // ...send the ptr along if need be. + // FIXME: technically the main thread has a pointer to a dead PyObject + // until the delete goes through; could that ever be a problem? + if (!InGameThread()) { + Object::WeakRef* s = self->session_; + g_game->PushCall([s] { delete s; }); + } else { + delete self->session_; + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +auto PythonClassSessionData::Exists(PythonClassSessionData* self) -> PyObject* { + BA_PYTHON_TRY; + Session* sgc = self->session_->get(); + if (sgc) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +PyTypeObject PythonClassSessionData::type_obj; +PyMethodDef PythonClassSessionData::tp_methods[] = { + {"exists", (PyCFunction)Exists, METH_NOARGS, + "exists() -> bool\n" + "\n" + "Returns whether the SessionData still exists.\n" + "Most functionality will fail on a nonexistent instance."}, + {nullptr}}; + +} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_session_data.h b/src/ballistica/python/class/python_class_session_data.h new file mode 100644 index 00000000..1d32287b --- /dev/null +++ b/src/ballistica/python/class/python_class_session_data.h @@ -0,0 +1,36 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SESSION_DATA_H_ +#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SESSION_DATA_H_ + +#include "ballistica/core/object.h" +#include "ballistica/python/class/python_class.h" + +namespace ballistica { + +class PythonClassSessionData : public PythonClass { + public: + static auto type_name() -> const char* { return "SessionData"; } + static void SetupType(PyTypeObject* obj); + static auto Create(Session* session) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + static PyTypeObject type_obj; + auto GetSession() const -> Session*; + + private: + static PyMethodDef tp_methods[]; + static auto tp_repr(PythonClassSessionData* self) -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassSessionData* self); + static auto Exists(PythonClassSessionData* self) -> PyObject*; + Object::WeakRef* session_; + static auto nb_bool(PythonClassSessionData* self) -> int; + static PyNumberMethods as_number_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SESSION_DATA_H_ diff --git a/src/ballistica/python/class/python_class_session_player.cc b/src/ballistica/python/class/python_class_session_player.cc new file mode 100644 index 00000000..a407107b --- /dev/null +++ b/src/ballistica/python/class/python_class_session_player.cc @@ -0,0 +1,746 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/class/python_class_session_player.h" + +#include +#include + +#include "ballistica/game/host_activity.h" +#include "ballistica/game/player.h" +#include "ballistica/game/session/host_session.h" +#include "ballistica/input/device/input_device.h" +#include "ballistica/python/python.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" + +auto PythonClassSessionPlayer::nb_bool(PythonClassSessionPlayer* self) -> int { + return self->player_->exists(); +} + +PyNumberMethods PythonClassSessionPlayer::as_number_; + +// Attrs we expose through our custom getattr/setattr. +#define ATTR_IN_GAME "in_game" +#define ATTR_SESSIONTEAM "sessionteam" +#define ATTR_COLOR "color" +#define ATTR_HIGHLIGHT "highlight" +#define ATTR_CHARACTER "character" +#define ATTR_ACTIVITYPLAYER "activityplayer" +#define ATTR_ID "id" +#define ATTR_INPUT_DEVICE "inputdevice" + +// The set we expose via dir(). +static const char* extra_dir_attrs[] = { + ATTR_ID, ATTR_IN_GAME, ATTR_SESSIONTEAM, ATTR_COLOR, + ATTR_HIGHLIGHT, ATTR_CHARACTER, ATTR_INPUT_DEVICE, nullptr}; + +void PythonClassSessionPlayer::SetupType(PyTypeObject* obj) { + PythonClass::SetupType(obj); + obj->tp_name = "ba.SessionPlayer"; + obj->tp_basicsize = sizeof(PythonClassSessionPlayer); + + // clang-format off + + obj->tp_doc = + "A reference to a player in the ba.Session.\n" + "\n" + "Category: Gameplay Classes\n" + "\n" + "These are created and managed internally and\n" + "provided to your Session/Activity instances.\n" + "Be aware that, like ba.Nodes, ba.SessionPlayer objects are 'weak'\n" + "references under-the-hood; a player can leave the game at\n" + " any point. For this reason, you should make judicious use of the\n" + "ba.SessionPlayer.exists() method (or boolean operator) to ensure\n" + "that a SessionPlayer is still present if retaining references to one\n" + "for any length of time.\n" + "\n" + "Attributes:\n" + "\n" + " " ATTR_ID ": int\n" + " The unique numeric ID of the Player.\n" + "\n" + " Note that you can also use the boolean operator for this same\n" + " functionality, so a statement such as \"if player\" will do\n" + " the right thing both for Player objects and values of None.\n" + "\n" + " " ATTR_IN_GAME ": bool\n" + " This bool value will be True once the Player has completed\n" + " any lobby character/team selection.\n" + "\n" + " " ATTR_SESSIONTEAM ": ba.SessionTeam\n" + " The ba.SessionTeam this Player is on. If the SessionPlayer\n" + " is still in its lobby selecting a team/etc. then a\n" + " ba.SessionTeamNotFoundError will be raised.\n" + "\n" + " " ATTR_INPUT_DEVICE ": ba.InputDevice\n" + " The input device associated with the player.\n" + "\n" + " " ATTR_COLOR ": Sequence[float]\n" + " The base color for this Player.\n" + " In team games this will match the ba.SessionTeam's color.\n" + "\n" + " " ATTR_HIGHLIGHT ": Sequence[float]\n" + " A secondary color for this player.\n" + " This is used for minor highlights and accents\n" + " to allow a player to stand apart from his teammates\n" + " who may all share the same team (primary) color.\n" + "\n" + " " ATTR_CHARACTER ": str\n" + " The character this player has selected in their profile.\n" + "\n" + " " ATTR_ACTIVITYPLAYER ": Optional[ba.Player]\n" + " The current game-specific instance for this player.\n"; + + // clang-format on + + obj->tp_new = tp_new; + obj->tp_repr = (reprfunc)tp_repr; + obj->tp_methods = tp_methods; + obj->tp_dealloc = (destructor)tp_dealloc; + obj->tp_getattro = (getattrofunc)tp_getattro; + obj->tp_setattro = (setattrofunc)tp_setattro; + + // We provide number methods only for bool functionality. + memset(&as_number_, 0, sizeof(as_number_)); + as_number_.nb_bool = (inquiry)nb_bool; + obj->tp_as_number = &as_number_; +} + +auto PythonClassSessionPlayer::Create(Player* player) -> PyObject* { + // Make sure we only have one python ref per material. + if (player) { + assert(!player->has_py_ref()); + } + s_create_empty_ = true; // Prevent class from erroring on create. + auto* py_player = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + s_create_empty_ = false; + if (!py_player) { + throw Exception("ba.Player creation failed."); + } + + *(py_player->player_) = player; + return reinterpret_cast(py_player); +} + +auto PythonClassSessionPlayer::GetPlayer(bool doraise) const -> Player* { + Player* player = player_->get(); + if ((!player) && doraise) { + throw Exception("Invalid SessionPlayer.", + PyExcType::kSessionPlayerNotFound); + } + return player; +} + +auto PythonClassSessionPlayer::tp_repr(PythonClassSessionPlayer* self) + -> PyObject* { + BA_PYTHON_TRY; + Player* p = self->player_->get(); + int player_id = p ? p->id() : -1; + std::string p_name = p ? p->GetName() : "invalid"; + return Py_BuildValue("s", + (std::string("") + .c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassSessionPlayer::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = + reinterpret_cast(type->tp_alloc(type, 0)); + if (self) { + BA_PYTHON_TRY; + if (!InGameThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the game thread (current is (" + + GetCurrentThreadName() + ")."); + } + + // If the user is creating one, make sure they passed None to get an + // invalid ref. + if (!s_create_empty_) { + if (!PyTuple_Check(args) || (PyTuple_GET_SIZE(args) != 1) + || (keywds != nullptr) || (PyTuple_GET_ITEM(args, 0) != Py_None)) + throw Exception( + "Can't instantiate SessionPlayers. To create an invalid" + " SessionPlayer reference, call ba.SessionPlayer(None)."); + } + self->player_ = new Object::WeakRef(); + BA_PYTHON_NEW_CATCH; + } + return reinterpret_cast(self); +} + +void PythonClassSessionPlayer::tp_dealloc(PythonClassSessionPlayer* self) { + BA_PYTHON_TRY; + + // These have to be deleted in the game thread - send the ptr along if need + // be; otherwise do it immediately. + if (!InGameThread()) { + Object::WeakRef* p = self->player_; + g_game->PushCall([p] { delete p; }); + } else { + delete self->player_; + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +auto PythonClassSessionPlayer::tp_getattro(PythonClassSessionPlayer* self, + PyObject* attr) -> PyObject* { + BA_PYTHON_TRY; + + assert(InGameThread()); + + // Assuming this will always be a str? + assert(PyUnicode_Check(attr)); + + const char* s = PyUnicode_AsUTF8(attr); + if (!strcmp(s, ATTR_IN_GAME)) { + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + + // We get placed on a team as soon as we finish in the lobby + // so lets use that as whether we're in-game or not. + PyObject* team = p->GetPyTeam(); + assert(team != nullptr); + if (team == Py_None) { + Py_RETURN_FALSE; + } else { + Py_RETURN_TRUE; + } + } else if (!strcmp(s, ATTR_ID)) { + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + return PyLong_FromLong(p->id()); + } else if (!strcmp(s, ATTR_INPUT_DEVICE)) { + Player* player = self->player_->get(); + if (!player) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + InputDevice* input_device = player->GetInputDevice(); + if (input_device) { + return input_device->NewPyRef(); + } + throw Exception(PyExcType::kInputDeviceNotFound); + } else if (!strcmp(s, ATTR_SESSIONTEAM)) { + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + PyObject* team = p->GetPyTeam(); + assert(team != nullptr); + if (team == Py_None) { + PyErr_SetString( + g_python->obj(Python::ObjID::kSessionTeamNotFoundError).get(), + "SessionTeam does not exist."); + return nullptr; + } + Py_INCREF(team); + return team; + } else if (!strcmp(s, ATTR_CHARACTER)) { + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + if (!p->has_py_data()) { + BA_LOG_ONCE("Error: Calling getAttr for player attr '" + std::string(s) + + "' without data set."); + } + PyObject* obj = p->GetPyCharacter(); + Py_INCREF(obj); + return obj; + } else if (!strcmp(s, ATTR_COLOR)) { + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + if (!p->has_py_data()) { + BA_LOG_ONCE("Error: Calling getAttr for player attr '" + std::string(s) + + "' without data set."); + } + PyObject* obj = p->GetPyColor(); + Py_INCREF(obj); + return obj; + } else if (!strcmp(s, ATTR_HIGHLIGHT)) { + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + if (!p->has_py_data()) { + BA_LOG_ONCE("Error: Calling getAttr for player attr '" + std::string(s) + + "' without data set."); + } + PyObject* obj = p->GetPyHighlight(); + Py_INCREF(obj); + return obj; + } else if (!strcmp(s, ATTR_ACTIVITYPLAYER)) { + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + if (!p->has_py_data()) { + BA_LOG_ONCE("Error: Calling getAttr for player attr '" + std::string(s) + + "' without data set."); + } + PyObject* obj = p->GetPyActivityPlayer(); + Py_INCREF(obj); + return obj; + } + + // Fall back to generic behavior. + PyObject* val; + val = PyObject_GenericGetAttr(reinterpret_cast(self), attr); + return val; + BA_PYTHON_CATCH; +} + +auto PythonClassSessionPlayer::tp_setattro(PythonClassSessionPlayer* self, + PyObject* attr, PyObject* val) + -> int { + BA_PYTHON_TRY; + // Assuming this will always be a str? + assert(PyUnicode_Check(attr)); + const char* s = PyUnicode_AsUTF8(attr); + + if (!strcmp(s, ATTR_ACTIVITYPLAYER)) { + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + p->SetPyActivityPlayer(val); + return 0; + } + throw Exception("Attr '" + std::string(PyUnicode_AsUTF8(attr)) + + "' is not settable on SessionPlayer objects.", + PyExcType::kAttribute); + // return PyObject_GenericSetAttr(reinterpret_cast(self), attr, + // val); + BA_PYTHON_INT_CATCH; +} + +auto PythonClassSessionPlayer::GetName(PythonClassSessionPlayer* self, + PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + int full = false; + int icon = true; + static const char* kwlist[] = {"full", "icon", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|pp", + const_cast(kwlist), &full, &icon)) { + return nullptr; + } + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + return PyUnicode_FromString( + p->GetName(static_cast(full), static_cast(icon)).c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassSessionPlayer::Exists(PythonClassSessionPlayer* self) + -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + if (self->player_->exists()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +auto PythonClassSessionPlayer::SetName(PythonClassSessionPlayer* self, + PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + PyObject* name_obj; + PyObject* full_name_obj = Py_None; + + // This should be false for temp names like . + int real = 1; + static const char* kwlist[] = {"name", "full_name", "real", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|Op", + const_cast(kwlist), &name_obj, + &full_name_obj, &real)) { + return nullptr; + } + std::string name = Python::GetPyString(name_obj); + std::string full_name = + (full_name_obj == Py_None) ? name : Python::GetPyString(full_name_obj); + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + p->SetName(name, full_name, static_cast(real)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassSessionPlayer::ResetInput(PythonClassSessionPlayer* self) + -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + p->ResetInput(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassSessionPlayer::AssignInputCall(PythonClassSessionPlayer* self, + PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + PyObject* input_type_obj; + PyObject* call_obj; + static const char* kwlist[] = {"type", "call", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "OO", + const_cast(kwlist), &input_type_obj, + &call_obj)) { + return nullptr; + } + Player* player = self->player_->get(); + if (!player) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + if (Python::IsPyEnum_InputType(input_type_obj)) { + InputType input_type = Python::GetPyEnum_InputType(input_type_obj); + player->AssignInputCall(input_type, call_obj); + } else { + if (!PyTuple_Check(input_type_obj)) { + PyErr_SetString(PyExc_TypeError, + "Expected InputType or tuple for type arg."); + return nullptr; + } + Py_ssize_t tuple_size = PyTuple_GET_SIZE(input_type_obj); + for (Py_ssize_t i = 0; i < tuple_size; i++) { + PyObject* obj = PyTuple_GET_ITEM(input_type_obj, i); + if (!Python::IsPyEnum_InputType(obj)) { + PyErr_SetString(PyExc_TypeError, "Expected tuple of InputTypes."); + return nullptr; + } + InputType input_type = Python::GetPyEnum_InputType(obj); + player->AssignInputCall(input_type, call_obj); + } + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassSessionPlayer::RemoveFromGame(PythonClassSessionPlayer* self) + -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + Player* player = self->player_->get(); + if (!player) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } else { + HostSession* host_session = player->GetHostSession(); + if (!host_session) { + throw Exception("Player's host-session not found.", + PyExcType::kSessionNotFound); + } + host_session->RemovePlayer(player); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassSessionPlayer::GetTeam(PythonClassSessionPlayer* self) + -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + PyObject* team = p->GetPyTeam(); + Py_INCREF(team); + return team; + BA_PYTHON_CATCH; +} + +// NOTE: this returns their PUBLIC account-id; we want to keep +// actual account-ids as hidden as possible for now. +auto PythonClassSessionPlayer::GetAccountID(PythonClassSessionPlayer* self) + -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + std::string account_id = p->GetPublicAccountID(); + if (account_id.empty()) { + Py_RETURN_NONE; + } + return PyUnicode_FromString(account_id.c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassSessionPlayer::SetData(PythonClassSessionPlayer* self, + PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + PyObject* team_obj; + PyObject* character_obj; + PyObject* color_obj; + PyObject* highlight_obj; + static const char* kwlist[] = {"team", "character", "color", "highlight", + nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "OOOO", const_cast(kwlist), &team_obj, + &character_obj, &color_obj, &highlight_obj)) { + return nullptr; + } + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + p->set_has_py_data(true); + p->SetPyTeam(team_obj); + p->SetPyCharacter(character_obj); + p->SetPyColor(color_obj); + p->SetPyHighlight(highlight_obj); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassSessionPlayer::GetIconInfo(PythonClassSessionPlayer* self) + -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + std::vector color = p->icon_tint_color(); + std::vector color2 = p->icon_tint2_color(); + return Py_BuildValue( + "{sssss(fff)s(fff)}", "texture", p->icon_tex_name().c_str(), + "tint_texture", p->icon_tint_tex_name().c_str(), "tint_color", color[0], + color[1], color[2], "tint2_color", color2[0], color2[1], color2[2]); + BA_PYTHON_CATCH; +} + +auto PythonClassSessionPlayer::SetIconInfo(PythonClassSessionPlayer* self, + PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + PyObject* texture_name_obj; + PyObject* tint_texture_name_obj; + PyObject* tint_color_obj; + PyObject* tint2_color_obj; + static const char* kwlist[] = {"texture", "tint_texture", "tint_color", + "tint2_color", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "OOOO", const_cast(kwlist), &texture_name_obj, + &tint_texture_name_obj, &tint_color_obj, &tint2_color_obj)) { + return nullptr; + } + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + std::string texture_name = Python::GetPyString(texture_name_obj); + std::string tint_texture_name = Python::GetPyString(tint_texture_name_obj); + std::vector tint_color = Python::GetPyFloats(tint_color_obj); + if (tint_color.size() != 3) { + throw Exception("Expected 3 floats for tint-color.", PyExcType::kValue); + } + std::vector tint2_color = Python::GetPyFloats(tint2_color_obj); + if (tint2_color.size() != 3) { + throw Exception("Expected 3 floats for tint-color.", PyExcType::kValue); + } + p->SetIcon(texture_name, tint_texture_name, tint_color, tint2_color); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassSessionPlayer::SetActivity(PythonClassSessionPlayer* self, + PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + PyObject* activity_obj; + static const char* kwlist[] = {"activity", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &activity_obj)) { + return nullptr; + } + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + HostActivity* a; + if (activity_obj == Py_None) { + a = nullptr; + } else { + a = Python::GetPyHostActivity(activity_obj); + } + p->SetHostActivity(a); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassSessionPlayer::SetNode(PythonClassSessionPlayer* self, + PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + PyObject* node_obj; + static const char* kwlist[] = {"node", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &node_obj)) { + return nullptr; + } + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + Node* node; + if (node_obj == Py_None) { + node = nullptr; + } else { + node = Python::GetPyNode(node_obj); + } + p->set_node(node); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassSessionPlayer::GetIcon(PythonClassSessionPlayer* self) + -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + Player* p = self->player_->get(); + if (!p) { + throw Exception(PyExcType::kSessionPlayerNotFound); + } + + // Now kindly ask the activity to load/return an icon for us. + PythonRef args(Py_BuildValue("(O)", p->BorrowPyRef()), PythonRef::kSteal); + PythonRef results; + { + Python::ScopedCallLabel label("get_player_icon"); + results = g_python->obj(Python::ObjID::kGetPlayerIconCall).Call(args); + } + return results.NewRef(); + BA_PYTHON_CATCH; +} + +auto PythonClassSessionPlayer::Dir(PythonClassSessionPlayer* self) + -> PyObject* { + BA_PYTHON_TRY; + + // Start with the standard python dir listing. + PyObject* dir_list = Python::generic_dir(reinterpret_cast(self)); + assert(PyList_Check(dir_list)); + + // ..and add in our custom attr names. + for (const char** name = extra_dir_attrs; *name != nullptr; name++) { + PyList_Append( + dir_list, + PythonRef(PyUnicode_FromString(*name), PythonRef::kSteal).get()); + } + PyList_Sort(dir_list); + return dir_list; + + BA_PYTHON_CATCH; +} + +bool PythonClassSessionPlayer::s_create_empty_ = false; +PyTypeObject PythonClassSessionPlayer::type_obj; +PyMethodDef PythonClassSessionPlayer::tp_methods[] = { + {"getname", (PyCFunction)GetName, METH_VARARGS | METH_KEYWORDS, + "getname(full: bool = False, icon: bool = True) -> str\n" + "\n" + "Returns the player's name. If icon is True, the long version of the\n" + "name may include an icon."}, + {"setname", (PyCFunction)SetName, METH_VARARGS | METH_KEYWORDS, + "setname(name: str, full_name: str = None, real: bool = True)\n" + " -> None\n" + "\n" + "Set the player's name to the provided string.\n" + "A number will automatically be appended if the name is not unique from\n" + "other players."}, + {"resetinput", (PyCFunction)ResetInput, METH_NOARGS, + "resetinput() -> None\n" + "\n" + "Clears out the player's assigned input actions."}, + {"exists", (PyCFunction)Exists, METH_NOARGS, + "exists() -> bool\n" + "\n" + "Return whether the underlying player is still in the game."}, + {"assigninput", (PyCFunction)AssignInputCall, METH_VARARGS | METH_KEYWORDS, + "assigninput(type: Union[ba.InputType, Tuple[ba.InputType, ...]],\n" + " call: Callable) -> None\n" + "\n" + "Set the python callable to be run for one or more types of input."}, + {"remove_from_game", (PyCFunction)RemoveFromGame, METH_NOARGS, + "remove_from_game() -> None\n" + "\n" + "Removes the player from the game."}, + {"get_account_id", (PyCFunction)GetAccountID, METH_VARARGS | METH_KEYWORDS, + "get_account_id() -> str\n" + "\n" + "Return the Account ID this player is signed in under, if\n" + "there is one and it can be determined with relative certainty.\n" + "Returns None otherwise. Note that this may require an active\n" + "internet connection (especially for network-connected players)\n" + "and may return None for a short while after a player initially\n" + "joins (while verification occurs)."}, + {"setdata", (PyCFunction)SetData, METH_VARARGS | METH_KEYWORDS, + "setdata(team: ba.SessionTeam, character: str,\n" + " color: Sequence[float], highlight: Sequence[float]) -> None\n" + "\n" + "(internal)"}, + {"set_icon_info", (PyCFunction)SetIconInfo, METH_VARARGS | METH_KEYWORDS, + "set_icon_info(texture: str, tint_texture: str,\n" + " tint_color: Sequence[float], tint2_color: Sequence[float]) -> None\n" + "\n" + "(internal)"}, + {"setactivity", (PyCFunction)SetActivity, METH_VARARGS | METH_KEYWORDS, + "setactivity(activity: Optional[ba.Activity]) -> None\n" + "\n" + "(internal)"}, + {"setnode", (PyCFunction)SetNode, METH_VARARGS | METH_KEYWORDS, + "setnode(node: Optional[Node]) -> None\n" + "\n" + "(internal)"}, + {"get_icon", (PyCFunction)GetIcon, METH_NOARGS, + "get_icon() -> Dict[str, Any]\n" + "\n" + "Returns the character's icon (images, colors, etc contained in a dict)"}, + {"get_icon_info", (PyCFunction)GetIconInfo, METH_NOARGS, + "get_icon_info() -> Dict[str, Any]\n" + "\n" + "(internal)"}, + {"__dir__", (PyCFunction)Dir, METH_NOARGS, + "allows inclusion of our custom attrs in standard python dir()"}, + {nullptr}}; + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_session_player.h b/src/ballistica/python/class/python_class_session_player.h new file mode 100644 index 00000000..bbfb8223 --- /dev/null +++ b/src/ballistica/python/class/python_class_session_player.h @@ -0,0 +1,62 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SESSION_PLAYER_H_ +#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SESSION_PLAYER_H_ + +#include "ballistica/core/object.h" +#include "ballistica/python/class/python_class.h" + +namespace ballistica { + +class PythonClassSessionPlayer : public PythonClass { + public: + static auto type_name() -> const char* { return "SessionPlayer"; } + static void SetupType(PyTypeObject* obj); + static auto Create(Player* player) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + static PyTypeObject type_obj; + auto GetPlayer(bool doraise) const -> Player*; + + private: + static bool s_create_empty_; + static PyMethodDef tp_methods[]; + static auto tp_repr(PythonClassSessionPlayer* self) -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassSessionPlayer* self); + static auto tp_getattro(PythonClassSessionPlayer* self, PyObject* attr) + -> PyObject*; + static auto tp_setattro(PythonClassSessionPlayer* self, PyObject* attr, + PyObject* val) -> int; + static auto GetName(PythonClassSessionPlayer* self, PyObject* args, + PyObject* keywds) -> PyObject*; + static auto Exists(PythonClassSessionPlayer* self) -> PyObject*; + static auto SetName(PythonClassSessionPlayer* self, PyObject* args, + PyObject* keywds) -> PyObject*; + static auto ResetInput(PythonClassSessionPlayer* self) -> PyObject*; + static auto AssignInputCall(PythonClassSessionPlayer* self, PyObject* args, + PyObject* keywds) -> PyObject*; + static auto RemoveFromGame(PythonClassSessionPlayer* self) -> PyObject*; + static auto GetTeam(PythonClassSessionPlayer* self) -> PyObject*; + static auto GetAccountID(PythonClassSessionPlayer* self) -> PyObject*; + static auto SetData(PythonClassSessionPlayer* self, PyObject* args, + PyObject* keywds) -> PyObject*; + static auto GetIconInfo(PythonClassSessionPlayer* self) -> PyObject*; + static auto SetIconInfo(PythonClassSessionPlayer* self, PyObject* args, + PyObject* keywds) -> PyObject*; + static auto SetActivity(PythonClassSessionPlayer* self, PyObject* args, + PyObject* keywds) -> PyObject*; + static auto SetNode(PythonClassSessionPlayer* self, PyObject* args, + PyObject* keywds) -> PyObject*; + static auto GetIcon(PythonClassSessionPlayer* self) -> PyObject*; + static auto Dir(PythonClassSessionPlayer* self) -> PyObject*; + Object::WeakRef* player_; + static auto nb_bool(PythonClassSessionPlayer* self) -> int; + static PyNumberMethods as_number_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SESSION_PLAYER_H_ diff --git a/src/ballistica/python/class/python_class_sound.cc b/src/ballistica/python/class/python_class_sound.cc new file mode 100644 index 00000000..960fa0dc --- /dev/null +++ b/src/ballistica/python/class/python_class_sound.cc @@ -0,0 +1,108 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/class/python_class_sound.h" + +#include + +#include "ballistica/game/game.h" +#include "ballistica/media/component/sound.h" + +namespace ballistica { + +auto PythonClassSound::tp_repr(PythonClassSound* self) -> PyObject* { + BA_PYTHON_TRY; + Object::Ref m = *(self->sound_); + return Py_BuildValue( + "s", (std::string("name() + "\"") : "(empty ref)") + ">") + .c_str()); + BA_PYTHON_CATCH; +} + +void PythonClassSound::SetupType(PyTypeObject* obj) { + PythonClass::SetupType(obj); + obj->tp_name = "ba.Sound"; + obj->tp_basicsize = sizeof(PythonClassSound); + obj->tp_doc = + "A reference to a sound.\n" + "\n" + "Category: Asset Classes\n" + "\n" + "Use ba.getsound() to instantiate one."; + obj->tp_repr = (reprfunc)tp_repr; + obj->tp_new = tp_new; + obj->tp_dealloc = (destructor)tp_dealloc; +} + +auto PythonClassSound::Create(Sound* sound) -> PyObject* { + s_create_empty_ = true; // prevent class from erroring on create + auto* t = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + s_create_empty_ = false; + if (!t) { + throw Exception("ba.Sound creation failed."); + } + *(t->sound_) = sound; + return reinterpret_cast(t); +} + +auto PythonClassSound::GetSound(bool doraise) const -> Sound* { + Sound* sound = sound_->get(); + if (!sound && doraise) { + throw Exception("Invalid Sound.", PyExcType::kNotFound); + } + return sound; +} + +auto PythonClassSound::tp_new(PyTypeObject* type, PyObject* args, + PyObject* kwds) -> PyObject* { + auto* self = reinterpret_cast(type->tp_alloc(type, 0)); + if (self) { + BA_PYTHON_TRY; + if (!InGameThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the game thread (current is (" + + GetCurrentThreadName() + ")."); + } + if (!s_create_empty_) { + throw Exception( + "Can't instantiate Sounds directly; use ba.getsound() to get " + "them."); + } + self->sound_ = new Object::Ref(); + BA_PYTHON_NEW_CATCH; + } + return reinterpret_cast(self); +} + +void PythonClassSound::Delete(Object::Ref* ref) { + assert(InGameThread()); + + // if we're the py-object for a sound, clear them out + // (FIXME - wej should pass the old pointer in here to sanity-test that we + // were their ref) + if (ref->exists()) { + (*ref)->ClearPyObject(); + } + delete ref; +} + +void PythonClassSound::tp_dealloc(PythonClassSound* self) { + BA_PYTHON_TRY; + // these have to be deleted in the game thread - send the ptr along if need + // be; otherwise do it immediately + if (!InGameThread()) { + Object::Ref* s = self->sound_; + g_game->PushCall([s] { Delete(s); }); + } else { + Delete(self->sound_); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +bool PythonClassSound::s_create_empty_ = false; +PyTypeObject PythonClassSound::type_obj; + +} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_sound.h b/src/ballistica/python/class/python_class_sound.h new file mode 100644 index 00000000..ccb68bb7 --- /dev/null +++ b/src/ballistica/python/class/python_class_sound.h @@ -0,0 +1,34 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SOUND_H_ +#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SOUND_H_ + +#include "ballistica/core/object.h" +#include "ballistica/python/class/python_class.h" + +namespace ballistica { + +class PythonClassSound : public PythonClass { + public: + static auto type_name() -> const char* { return "Sound"; } + static PyTypeObject type_obj; + static auto tp_repr(PythonClassSound* self) -> PyObject*; + static void SetupType(PyTypeObject* obj); + static auto Create(Sound* sound) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + auto GetSound(bool doraise = true) const -> Sound*; + + private: + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* kwds) + -> PyObject*; + static void tp_dealloc(PythonClassSound* self); + static void Delete(Object::Ref* ref); + static bool s_create_empty_; + Object::Ref* sound_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_SOUND_H_ diff --git a/src/ballistica/python/class/python_class_texture.cc b/src/ballistica/python/class/python_class_texture.cc new file mode 100644 index 00000000..3a366036 --- /dev/null +++ b/src/ballistica/python/class/python_class_texture.cc @@ -0,0 +1,109 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/class/python_class_texture.h" + +#include + +#include "ballistica/game/game.h" +#include "ballistica/media/component/texture.h" + +namespace ballistica { + +auto PythonClassTexture::tp_repr(PythonClassTexture* self) -> PyObject* { + BA_PYTHON_TRY; + Object::Ref t = *(self->texture_); + return Py_BuildValue( + "s", (std::string("name() + "\"") : "(empty ref)") + ">") + .c_str()); + BA_PYTHON_CATCH; +} + +void PythonClassTexture::SetupType(PyTypeObject* obj) { + PythonClass::SetupType(obj); + obj->tp_name = "ba.Texture"; + obj->tp_basicsize = sizeof(PythonClassTexture); + obj->tp_doc = + "A reference to a texture.\n" + "\n" + "Category: Asset Classes\n" + "\n" + "Use ba.gettexture() to instantiate one."; + obj->tp_repr = (reprfunc)tp_repr; + obj->tp_new = tp_new; + obj->tp_dealloc = (destructor)tp_dealloc; +} + +auto PythonClassTexture::Create(Texture* texture) -> PyObject* { + s_create_empty_ = true; // prevent class from erroring on create + assert(texture != nullptr); + auto* t = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + s_create_empty_ = false; + if (!t) { + throw Exception("ba.Texture creation failed."); + } + *(t->texture_) = texture; + return reinterpret_cast(t); +} + +auto PythonClassTexture::GetTexture(bool doraise) const -> Texture* { + Texture* texture = texture_->get(); + if (!texture && doraise) { + throw Exception("Invalid Texture.", PyExcType::kNotFound); + } + return texture; +} + +auto PythonClassTexture::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = reinterpret_cast(type->tp_alloc(type, 0)); + if (self) { + BA_PYTHON_TRY; + if (!InGameThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the game thread (current is (" + + GetCurrentThreadName() + ")."); + } + if (!s_create_empty_) { + throw Exception( + "Can't instantiate Textures directly; use ba.gettexture() to get " + "them."); + } + self->texture_ = new Object::Ref(); + BA_PYTHON_NEW_CATCH; + } + return reinterpret_cast(self); +} + +void PythonClassTexture::Delete(Object::Ref* ref) { + assert(InGameThread()); + + // If we're the py-object for a texture, kill our reference to it. + // (FIXME - we should pass the old py obj pointer in here to + // make sure that we were their python obj as a sanity test) + if (ref->exists()) { + (*ref)->ClearPyObject(); + } + delete ref; +} + +void PythonClassTexture::tp_dealloc(PythonClassTexture* self) { + BA_PYTHON_TRY; + // These have to be deleted in the game thread - send the ptr along if need + // be; otherwise do it immediately. + if (!InGameThread()) { + Object::Ref* t = self->texture_; + g_game->PushCall([t] { Delete(t); }); + } else { + Delete(self->texture_); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +bool PythonClassTexture::s_create_empty_ = false; +PyTypeObject PythonClassTexture::type_obj; + +} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_texture.h b/src/ballistica/python/class/python_class_texture.h new file mode 100644 index 00000000..e0cb92c8 --- /dev/null +++ b/src/ballistica/python/class/python_class_texture.h @@ -0,0 +1,34 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_TEXTURE_H_ +#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_TEXTURE_H_ + +#include "ballistica/core/object.h" +#include "ballistica/python/class/python_class.h" + +namespace ballistica { + +class PythonClassTexture : public PythonClass { + public: + static auto type_name() -> const char* { return "Texture"; } + static auto tp_repr(PythonClassTexture* self) -> PyObject*; + static void SetupType(PyTypeObject* obj); + static PyTypeObject type_obj; + static auto Create(Texture* texture) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + auto GetTexture(bool doraise = true) const -> Texture*; + + private: + static bool s_create_empty_; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassTexture* self); + static void Delete(Object::Ref* ref); + Object::Ref* texture_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_TEXTURE_H_ diff --git a/src/ballistica/python/class/python_class_timer.cc b/src/ballistica/python/class/python_class_timer.cc new file mode 100644 index 00000000..5dc2b7ce --- /dev/null +++ b/src/ballistica/python/class/python_class_timer.cc @@ -0,0 +1,195 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/class/python_class_timer.h" + +#include + +#include "ballistica/game/game.h" +#include "ballistica/game/host_activity.h" +#include "ballistica/game/session/host_session.h" +#include "ballistica/python/python_context_call_runnable.h" + +namespace ballistica { + +void PythonClassTimer::SetupType(PyTypeObject* obj) { + PythonClass::SetupType(obj); + obj->tp_name = "ba.Timer"; + obj->tp_basicsize = sizeof(PythonClassTimer); + obj->tp_doc = + "Timer(time: float, call: Callable[[], Any], repeat: bool = False,\n" + " timetype: ba.TimeType = TimeType.SIM,\n" + " timeformat: ba.TimeFormat = TimeFormat.SECONDS,\n" + " suppress_format_warning: bool = False)\n" + "\n" + "Timers are used to run code at later points in time.\n" + "\n" + "Category: General Utility Classes\n" + "\n" + "This class encapsulates a timer in the current ba.Context.\n" + "The underlying timer will be destroyed when either this object is\n" + "no longer referenced or when its Context (Activity, etc.) dies. If you\n" + "do not want to worry about keeping a reference to your timer around,\n" + "you should use the ba.timer() function instead.\n" + "\n" + "time: length of time (in seconds by default) that the timer will wait\n" + "before firing. Note that the actual delay experienced may vary\n " + "depending on the timetype. (see below)\n" + "\n" + "call: A callable Python object. Note that the timer will retain a\n" + "strong reference to the callable for as long as it exists, so you\n" + "may want to look into concepts such as ba.WeakCall if that is not\n" + "desired.\n" + "\n" + "repeat: if True, the timer will fire repeatedly, with each successive\n" + "firing having the same delay as the first.\n" + "\n" + "timetype can be either 'sim', 'base', or 'real'. It defaults to\n" + "'sim'. Types are explained below:\n" + "\n" + "'sim' time maps to local simulation time in ba.Activity or ba.Session\n" + "Contexts. This means that it may progress slower in slow-motion play\n" + "modes, stop when the game is paused, etc. This time type is not\n" + "available in UI contexts.\n" + "\n" + "'base' time is also linked to gameplay in ba.Activity or ba.Session\n" + "Contexts, but it progresses at a constant rate regardless of\n " + "slow-motion states or pausing. It can, however, slow down or stop\n" + "in certain cases such as network outages or game slowdowns due to\n" + "cpu load. Like 'sim' time, this is unavailable in UI contexts.\n" + "\n" + "'real' time always maps to actual clock time with a bit of filtering\n" + "added, regardless of Context. (the filtering prevents it from going\n" + "backwards or jumping forward by large amounts due to the app being\n" + "backgrounded, system time changing, etc.)\n" + "Real time timers are currently only available in the UI context.\n" + "\n" + "the 'timeformat' arg defaults to SECONDS but can also be MILLISECONDS\n" + "if you want to pass time as milliseconds.\n" + "\n" + "# Example: use a Timer object to print repeatedly for a few seconds:\n" + "def say_it():\n" + " ba.screenmessage('BADGER!')\n" + "def stop_saying_it():\n" + " self.t = None\n" + " ba.screenmessage('MUSHROOM MUSHROOM!')\n" + "# create our timer; it will run as long as we hold self.t\n" + "self.t = ba.Timer(0.3, say_it, repeat=True)\n" + "# now fire off a one-shot timer to kill it\n" + "ba.timer(3.89, stop_saying_it)"; + obj->tp_new = tp_new; + obj->tp_dealloc = (destructor)tp_dealloc; +} + +auto PythonClassTimer::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = reinterpret_cast(type->tp_alloc(type, 0)); + if (self) { + BA_PYTHON_TRY; + + if (!InGameThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the game thread (current is (" + + GetCurrentThreadName() + ")."); + } + + self->context_ = new Context(); + + PyObject* length_obj{}; + int64_t length; + int repeat{}; + int suppress_format_warning{}; + PyObject* call_obj{}; + PyObject* time_type_obj{}; + PyObject* time_format_obj{}; + static const char* kwlist[] = {"time", "call", + "repeat", "timetype", + "timeformat", "suppress_format_warning", + nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "OO|pOOp", const_cast(kwlist), &length_obj, + &call_obj, &repeat, &time_type_obj, &time_format_obj, + &suppress_format_warning)) { + return nullptr; + } + + auto time_type = TimeType::kSim; + if (time_type_obj != nullptr) { + time_type = Python::GetPyEnum_TimeType(time_type_obj); + } + auto time_format = TimeFormat::kSeconds; + if (time_format_obj != nullptr) { + time_format = Python::GetPyEnum_TimeFormat(time_format_obj); + } + +#if BA_TEST_BUILD || BA_DEBUG_BUILD + if (!suppress_format_warning) { + g_python->TimeFormatCheck(time_format, length_obj); + } +#endif + + // We currently work with integer milliseconds internally. + if (time_format == TimeFormat::kSeconds) { + length = static_cast(Python::GetPyDouble(length_obj) * 1000.0); + } else if (time_format == TimeFormat::kMilliseconds) { + length = Python::GetPyInt64(length_obj); + } else { + throw Exception("Invalid timeformat: '" + + std::to_string(static_cast(time_format)) + + "'.", + PyExcType::kValue); + } + if (length < 0) { + throw Exception("Timer length < 0.", PyExcType::kValue); + } + + auto runnable(Object::New(call_obj)); + + self->time_type_ = time_type; + + // Now just make sure we've got a valid context-target and ask us to + // make us a timer. + if (!self->context_->target.exists()) { + throw Exception("Invalid current context.", PyExcType::kContext); + } + self->timer_id_ = self->context_->target->NewTimer( + self->time_type_, length, static_cast(repeat), runnable); + self->have_timer_ = true; + + BA_PYTHON_NEW_CATCH; + } + return reinterpret_cast(self); +} +void PythonClassTimer::DoDelete(bool have_timer, TimeType time_type, + int timer_id, Context* context) { + assert(InGameThread()); + if (!context) { + return; + } + if (context->target.exists() && have_timer) { + context->target->DeleteTimer(time_type, timer_id); + } + delete context; +} + +void PythonClassTimer::tp_dealloc(PythonClassTimer* self) { + BA_PYTHON_TRY; + // These have to be deleted in the game thread. + if (!InGameThread()) { + auto a0 = self->have_timer_; + auto a1 = self->time_type_; + auto a2 = self->timer_id_; + auto a3 = self->context_; + g_game->PushCall( + [a0, a1, a2, a3] { PythonClassTimer::DoDelete(a0, a1, a2, a3); }); + } else { + DoDelete(self->have_timer_, self->time_type_, self->timer_id_, + self->context_); + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +PyTypeObject PythonClassTimer::type_obj; + +} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_timer.h b/src/ballistica/python/class/python_class_timer.h new file mode 100644 index 00000000..7c04f93d --- /dev/null +++ b/src/ballistica/python/class/python_class_timer.h @@ -0,0 +1,35 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_TIMER_H_ +#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_TIMER_H_ + +#include "ballistica/ballistica.h" +#include "ballistica/python/class/python_class.h" +#include "ballistica/python/python.h" + +namespace ballistica { + +class PythonClassTimer : public PythonClass { + public: + static auto type_name() -> const char* { return "Timer"; } + static void SetupType(PyTypeObject* obj); + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + static PyTypeObject type_obj; + + private: + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassTimer* self); + static void DoDelete(bool have_timer, TimeType time_type, int timer_id, + Context* context); + TimeType time_type_; + int timer_id_; + Context* context_; + bool have_timer_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_TIMER_H_ diff --git a/src/ballistica/python/class/python_class_vec3.cc b/src/ballistica/python/class/python_class_vec3.cc new file mode 100644 index 00000000..1f396f5d --- /dev/null +++ b/src/ballistica/python/class/python_class_vec3.cc @@ -0,0 +1,350 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/class/python_class_vec3.h" + +#include + +#include "ballistica/ballistica.h" +#include "ballistica/python/python.h" + +// FIXME: +// We currently call abc.Sequence.register(_ba.Vec3) which registers us as +// a Sequence type (so that isinstance(ba.Vec3(), abc.Sequence) == True). +// However the abc module lists a few things as part of the Sequence interface +// that we don't currently provide: index() and count() +namespace ballistica { + +// Ignore a few things that python macros do. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +#pragma ide diagnostic ignored "RedundantCast" + +static const int kMemberCount = 3; + +PyTypeObject PythonClassVec3::type_obj; +PySequenceMethods PythonClassVec3::as_sequence_; +PyNumberMethods PythonClassVec3::as_number_; + +void PythonClassVec3::SetupType(PyTypeObject* obj) { + PythonClass::SetupType(obj); + obj->tp_name = "ba.Vec3"; + obj->tp_basicsize = sizeof(PythonClassVec3); + obj->tp_doc = + "A vector of 3 floats.\n" + "\n" + "Category: General Utility Classes\n" + "\n" + "These can be created the following ways (checked in this order):\n" + "- with no args, all values are set to 0\n" + "- with a single numeric arg, all values are set to that value\n" + "- with a single three-member sequence arg, sequence values are copied\n" + "- otherwise assumes individual x/y/z args (positional or keywords)" + "\n" + "Attributes:\n" + "\n" + " x: float\n" + " The vector's X component.\n" + "\n" + " y: float\n" + " The vector's Y component.\n" + "\n" + " z: float\n" + " The vector's Z component.\n"; + + obj->tp_new = tp_new; + obj->tp_repr = (reprfunc)tp_repr; + obj->tp_methods = tp_methods; + obj->tp_getattro = (getattrofunc)tp_getattro; + obj->tp_setattro = (setattrofunc)tp_setattro; + obj->tp_richcompare = (richcmpfunc)tp_richcompare; + + // Sequence functionality. + memset(&as_sequence_, 0, sizeof(as_sequence_)); + as_sequence_.sq_length = (lenfunc)sq_length; + as_sequence_.sq_item = (ssizeargfunc)sq_item; + as_sequence_.sq_ass_item = (ssizeobjargproc)sq_ass_item; + obj->tp_as_sequence = &as_sequence_; + + // Number functionality. + memset(&as_number_, 0, sizeof(as_number_)); + as_number_.nb_add = (binaryfunc)nb_add; + as_number_.nb_subtract = (binaryfunc)nb_subtract; + as_number_.nb_multiply = (binaryfunc)nb_multiply; + as_number_.nb_negative = (unaryfunc)nb_negative; + obj->tp_as_number = &as_number_; + + // Note: we could fill out the in-place versions of these + // if we're not going for immutability.. +} + +auto PythonClassVec3::Create(const Vector3f& val) -> PyObject* { + auto obj = + reinterpret_cast(type_obj.tp_alloc(&type_obj, 0)); + if (obj) { + obj->value = val; + } + return reinterpret_cast(obj); +} + +auto PythonClassVec3::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto self = reinterpret_cast(type->tp_alloc(type, 0)); + if (self) { + BA_PYTHON_TRY; + + // Accept a numeric sequence of length 3. + assert(args != nullptr); + assert(PyTuple_Check(args)); + Py_ssize_t numargs = PyTuple_GET_SIZE(args); + if (numargs == 1 && PySequence_Check(PyTuple_GET_ITEM(args, 0))) { + auto vals = Python::GetPyFloats(PyTuple_GET_ITEM(args, 0)); + if (vals.size() != 3) { + throw Exception("Expected a 3 member numeric sequence.", + PyExcType::kValue); + } + self->value.x = vals[0]; + self->value.y = vals[1]; + self->value.z = vals[2]; + } else if (numargs == 1 + && Python::CanGetPyDouble(PyTuple_GET_ITEM(args, 0))) { + float val = Python::GetPyFloat(PyTuple_GET_ITEM(args, 0)); + self->value.x = self->value.y = self->value.z = val; + } else { + // Otherwise interpret as individual x, y, z float vals defaulting to 0. + static const char* kwlist[] = {"x", "y", "z", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "|fff", const_cast(kwlist), &self->value.x, + &self->value.y, &self->value.z)) { + Py_TYPE(self)->tp_free(reinterpret_cast(self)); + return nullptr; + } + } + BA_PYTHON_NEW_CATCH; + } + return reinterpret_cast(self); +} + +auto PythonClassVec3::tp_repr(PythonClassVec3* self) -> PyObject* { + BA_PYTHON_TRY; + char buffer[128]; + snprintf(buffer, sizeof(buffer), "ba.Vec3(%f, %f, %f)", self->value.x, + self->value.y, self->value.z); + return Py_BuildValue("s", buffer); + BA_PYTHON_CATCH; +} + +auto PythonClassVec3::sq_length(PythonClassVec3* self) -> Py_ssize_t { + return kMemberCount; +} + +auto PythonClassVec3::sq_item(PythonClassVec3* self, Py_ssize_t i) + -> PyObject* { + if (i < 0 || i >= kMemberCount) { + PyErr_SetString(PyExc_IndexError, "Vec3 index out of range"); + return nullptr; + } + return PyFloat_FromDouble(self->value.v[i]); +} + +auto PythonClassVec3::sq_ass_item(PythonClassVec3* self, Py_ssize_t i, + PyObject* valobj) -> int { + BA_PYTHON_TRY; + if (i < 0 || i >= kMemberCount) { + throw Exception("Vec3 index out of range.", PyExcType::kValue); + } + float val = Python::GetPyFloat(valobj); + self->value.v[i] = val; + return 0; + BA_PYTHON_INT_CATCH; +} + +auto PythonClassVec3::nb_add(PythonClassVec3* l, PythonClassVec3* r) + -> PyObject* { + BA_PYTHON_TRY; + + // We can add if both sides are Vec3. + if (Check(reinterpret_cast(l)) + && Check(reinterpret_cast(r))) { + return Create(l->value + r->value); + } + + // Otherwise we got nothin'. + Py_INCREF(Py_NotImplemented); + return Py_NotImplemented; + BA_PYTHON_CATCH; +} + +auto PythonClassVec3::nb_subtract(PythonClassVec3* l, PythonClassVec3* r) + -> PyObject* { + BA_PYTHON_TRY; + + // We can subtract if both sides are Vec3. + if (Check(reinterpret_cast(l)) + && Check(reinterpret_cast(r))) { + return Create(l->value - r->value); + } + + // Otherwise we got nothin'. + Py_INCREF(Py_NotImplemented); + return Py_NotImplemented; + BA_PYTHON_CATCH; +} + +auto PythonClassVec3::nb_negative(PythonClassVec3* self) -> PyObject* { + return Create(-self->value); +} + +auto PythonClassVec3::nb_multiply(PyObject* l, PyObject* r) -> PyObject* { + BA_PYTHON_TRY; + + // If left side is vec3. + if (Check(l)) { + // Try right as single number. + if (Python::CanGetPyDouble(r)) { + assert(Check(l)); + return Create(reinterpret_cast(l)->value + * Python::GetPyFloat(r)); + } + + // Try right as a vec3-able value. + if (Python::CanGetPyVector3f(r)) { + Vector3f& lvec(reinterpret_cast(l)->value); + Vector3f rvec(Python::GetPyVector3f(r)); + return Create( + Vector3f(lvec.x * rvec.x, lvec.y * rvec.y, lvec.z * rvec.z)); + } + } else { + // Ok, right must be vec3 (by definition). + assert(Check(r)); + + // Try left as single value. + if (Python::CanGetPyDouble(l)) { + assert(Check(r)); + return Create(Python::GetPyFloat(l) + * reinterpret_cast(r)->value); + } + + // Try left as a vec3-able value. + if (Python::CanGetPyVector3f(l)) { + Vector3f lvec(Python::GetPyVector3f(l)); + Vector3f& rvec(reinterpret_cast(r)->value); + return Create( + Vector3f(lvec.x * rvec.x, lvec.y * rvec.y, lvec.z * rvec.z)); + } + } + + // Ok we got nothin'. + Py_INCREF(Py_NotImplemented); + return Py_NotImplemented; + BA_PYTHON_CATCH; +} + +auto PythonClassVec3::tp_richcompare(PythonClassVec3* c1, PyObject* c2, int op) + -> PyObject* { + // Always return false against other types. + if (!Check(c2)) { + Py_RETURN_FALSE; + } + bool eq = (c1->value == (reinterpret_cast(c2))->value); + if (op == Py_EQ) { + if (eq) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + } else if (op == Py_NE) { + if (!eq) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + } else { + // Don't support other ops. + Py_RETURN_NOTIMPLEMENTED; + } +} + +auto PythonClassVec3::Length(PythonClassVec3* self) -> PyObject* { + BA_PYTHON_TRY; + return PyFloat_FromDouble(self->value.Length()); + BA_PYTHON_CATCH; +} + +auto PythonClassVec3::Normalized(PythonClassVec3* self) -> PyObject* { + BA_PYTHON_TRY; + return Create(self->value.Normalized()); + BA_PYTHON_CATCH; +} + +auto PythonClassVec3::Dot(PythonClassVec3* self, PyObject* other) -> PyObject* { + BA_PYTHON_TRY; + return PyFloat_FromDouble(self->value.Dot(Python::GetPyVector3f(other))); + BA_PYTHON_CATCH; +} + +auto PythonClassVec3::Cross(PythonClassVec3* self, PyObject* other) + -> PyObject* { + BA_PYTHON_TRY; + return Create(Vector3f::Cross(self->value, Python::GetPyVector3f(other))); + BA_PYTHON_CATCH; +} + +PyMethodDef PythonClassVec3::tp_methods[] = { + {"length", (PyCFunction)Length, METH_NOARGS, + "length() -> float\n" + "\n" + "Returns the length of the vector."}, + {"normalized", (PyCFunction)Normalized, METH_NOARGS, + "normalized() -> Vec3\n" + "\n" + "Returns a normalized version of the vector."}, + {"dot", (PyCFunction)Dot, METH_O, + "dot(other: Vec3) -> float\n" + "\n" + "Returns the dot product of this vector and another."}, + {"cross", (PyCFunction)Cross, METH_O, + "cross(other: Vec3) -> Vec3\n" + "\n" + "Returns the cross product of this vector and another."}, + {nullptr}}; + +auto PythonClassVec3::tp_getattro(PythonClassVec3* self, PyObject* attr) + -> PyObject* { + BA_PYTHON_TRY; + assert(PyUnicode_Check(attr)); + + const char* s = PyUnicode_AsUTF8(attr); + if (!strcmp(s, "x")) { + return PyFloat_FromDouble(self->value.x); + } else if (!strcmp(s, "y")) { + return PyFloat_FromDouble(self->value.y); + } else if (!strcmp(s, "z")) { + return PyFloat_FromDouble(self->value.z); + } + return PyObject_GenericGetAttr(reinterpret_cast(self), attr); + BA_PYTHON_CATCH; +} + +auto PythonClassVec3::tp_setattro(PythonClassVec3* self, PyObject* attrobj, + PyObject* valobj) -> int { + BA_PYTHON_TRY; + assert(PyUnicode_Check(attrobj)); + const char* attr = PyUnicode_AsUTF8(attrobj); + float val = Python::GetPyFloat(valobj); + if (!strcmp(attr, "x")) { + self->value.x = val; + } else if (!strcmp(attr, "y")) { + self->value.y = val; + } else if (!strcmp(attr, "z")) { + self->value.z = val; + } else { + throw Exception("Attr '" + std::string(attr) + "' is not settable.", + PyExcType::kAttribute); + } + return 0; + BA_PYTHON_INT_CATCH; +} + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_vec3.h b/src/ballistica/python/class/python_class_vec3.h new file mode 100644 index 00000000..7cbb02c5 --- /dev/null +++ b/src/ballistica/python/class/python_class_vec3.h @@ -0,0 +1,49 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_VEC3_H_ +#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_VEC3_H_ + +#include "ballistica/math/vector3f.h" +#include "ballistica/python/class/python_class.h" + +namespace ballistica { + +class PythonClassVec3 : public PythonClass { + public: + static auto type_name() -> const char* { return "Vec3"; } + static void SetupType(PyTypeObject* obj); + static auto Create(const Vector3f& val) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + static auto Length(PythonClassVec3* self) -> PyObject*; + static auto Normalized(PythonClassVec3* self) -> PyObject*; + static auto Dot(PythonClassVec3* self, PyObject* other) -> PyObject*; + static auto Cross(PythonClassVec3* self, PyObject* other) -> PyObject*; + static PyTypeObject type_obj; + Vector3f value; + + private: + static PyMethodDef tp_methods[]; + static PySequenceMethods as_sequence_; + static PyNumberMethods as_number_; + static auto tp_repr(PythonClassVec3* self) -> PyObject*; + static auto sq_length(PythonClassVec3* self) -> Py_ssize_t; + static auto sq_item(PythonClassVec3* self, Py_ssize_t i) -> PyObject*; + static auto sq_ass_item(PythonClassVec3* self, Py_ssize_t i, PyObject* val) + -> int; + static auto nb_add(PythonClassVec3* l, PythonClassVec3* r) -> PyObject*; + static auto nb_subtract(PythonClassVec3* l, PythonClassVec3* r) -> PyObject*; + static auto nb_multiply(PyObject* l, PyObject* r) -> PyObject*; + static auto nb_negative(PythonClassVec3* self) -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static auto tp_getattro(PythonClassVec3* self, PyObject* attr) -> PyObject*; + static auto tp_richcompare(PythonClassVec3* c1, PyObject* c2, int op) + -> PyObject*; + static auto tp_setattro(PythonClassVec3* self, PyObject* attr, PyObject* val) + -> int; +}; + +} // namespace ballistica +#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_VEC3_H_ diff --git a/src/ballistica/python/class/python_class_widget.cc b/src/ballistica/python/class/python_class_widget.cc new file mode 100644 index 00000000..dde68f46 --- /dev/null +++ b/src/ballistica/python/class/python_class_widget.cc @@ -0,0 +1,288 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/class/python_class_widget.h" + +#include + +#include "ballistica/game/game.h" +#include "ballistica/generic/utils.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/ui/widget/container_widget.h" +#include "ballistica/ui/widget/widget.h" + +namespace ballistica { + +auto PythonClassWidget::nb_bool(PythonClassWidget* self) -> int { + return self->widget_->exists(); +} + +PyNumberMethods PythonClassWidget::as_number_; + +void PythonClassWidget::SetupType(PyTypeObject* obj) { + PythonClass::SetupType(obj); + obj->tp_name = "ba.Widget"; + obj->tp_basicsize = sizeof(PythonClassWidget); + obj->tp_doc = + "Internal type for low level UI elements; buttons, windows, etc.\n" + "\n" + "Category: User Interface Classes\n" + "\n" + "This class represents a weak reference to a widget object\n" + "in the internal c++ layer. Currently, functions such as\n" + "ba.buttonwidget() must be used to instantiate or edit these."; + obj->tp_new = tp_new; + obj->tp_dealloc = (destructor)tp_dealloc; + obj->tp_repr = (reprfunc)tp_repr; + obj->tp_methods = tp_methods; + + // we provide number methods only for bool functionality + memset(&as_number_, 0, sizeof(as_number_)); + as_number_.nb_bool = (inquiry)nb_bool; + obj->tp_as_number = &as_number_; +} + +auto PythonClassWidget::Create(Widget* widget) -> PyObject* { + // Make sure we only have one python ref per widget. + if (widget) { + assert(!widget->has_py_ref()); + } + + auto* py_widget = reinterpret_cast( + PyObject_CallObject(reinterpret_cast(&type_obj), nullptr)); + if (!py_widget) throw Exception("ba.Widget creation failed"); + + *(py_widget->widget_) = widget; + return reinterpret_cast(py_widget); +} + +auto PythonClassWidget::GetWidget() const -> Widget* { + Widget* w = widget_->get(); + if (!w) throw Exception("Invalid widget"); + return w; +} + +auto PythonClassWidget::tp_repr(PythonClassWidget* self) -> PyObject* { + BA_PYTHON_TRY; + Widget* w = self->widget_->get(); + return Py_BuildValue("s", (std::string("GetWidgetTypeName() : "") + + "' widget " + Utils::PtrToString(w) + ">") + .c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassWidget::tp_new(PyTypeObject* type, PyObject* args, + PyObject* keywds) -> PyObject* { + auto* self = reinterpret_cast(type->tp_alloc(type, 0)); + if (self) { + BA_PYTHON_TRY; + if (!InGameThread()) { + throw Exception( + "ERROR: " + std::string(type_obj.tp_name) + + " objects must only be created in the game thread (current is (" + + GetCurrentThreadName() + ")."); + } + self->widget_ = new Object::WeakRef(); + BA_PYTHON_NEW_CATCH; + } + return reinterpret_cast(self); +} + +void PythonClassWidget::tp_dealloc(PythonClassWidget* self) { + BA_PYTHON_TRY; + // these have to be destructed in the game thread - send them along to it if + // need be + if (!InGameThread()) { + Object::WeakRef* w = self->widget_; + g_game->PushCall([w] { delete w; }); + } else { + delete self->widget_; + } + BA_PYTHON_DEALLOC_CATCH; + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +auto PythonClassWidget::Exists(PythonClassWidget* self) -> PyObject* { + BA_PYTHON_TRY; + Widget* w = self->widget_->get(); + if (w) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +auto PythonClassWidget::GetWidgetType(PythonClassWidget* self) -> PyObject* { + BA_PYTHON_TRY; + Widget* w = self->widget_->get(); + if (!w) { + throw Exception(PyExcType::kWidgetNotFound); + } + return PyUnicode_FromString(w->GetWidgetTypeName().c_str()); + BA_PYTHON_CATCH; +} + +auto PythonClassWidget::Activate(PythonClassWidget* self) -> PyObject* { + BA_PYTHON_TRY; + Widget* w = self->widget_->get(); + if (!w) { + throw Exception(PyExcType::kWidgetNotFound); + } + w->Activate(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassWidget::GetChildren(PythonClassWidget* self) -> PyObject* { + BA_PYTHON_TRY; + Widget* w = self->widget_->get(); + if (!w) { + throw Exception(PyExcType::kWidgetNotFound); + } + PyObject* py_list = PyList_New(0); + auto* cw = dynamic_cast(w); + if (cw) { + for (auto&& i : cw->widgets()) { + assert(i.exists()); + PyList_Append(py_list, i->BorrowPyRef()); + } + } + return py_list; + BA_PYTHON_CATCH; +} + +auto PythonClassWidget::GetSelectedChild(PythonClassWidget* self) -> PyObject* { + BA_PYTHON_TRY; + Widget* w = self->widget_->get(); + if (!w) { + throw Exception(PyExcType::kWidgetNotFound); + } + auto* cw = dynamic_cast(w); + if (cw) { + Widget* selected_widget = cw->selected_widget(); + if (selected_widget) return selected_widget->NewPyRef(); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassWidget::GetScreenSpaceCenter(PythonClassWidget* self) + -> PyObject* { + BA_PYTHON_TRY; + Widget* w = self->widget_->get(); + if (!w) { + throw Exception(PyExcType::kWidgetNotFound); + } + float x, y; + w->GetCenter(&x, &y); + + // this gives us coords in the widget's parent's space; translate from that + // to screen space + if (ContainerWidget* parent = w->parent_widget()) { + parent->WidgetPointToScreen(&x, &y); + } + // ..but we actually want to return points relative to the center of the + // screen (so they're useful as stack-offset values) + float screen_width = g_graphics->screen_virtual_width(); + float screen_height = g_graphics->screen_virtual_height(); + x -= screen_width * 0.5f; + y -= screen_height * 0.5f; + return Py_BuildValue("(ff)", x, y); + BA_PYTHON_CATCH; +} + +auto PythonClassWidget::Delete(PythonClassWidget* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + int ignore_missing = true; + static const char* kwlist[] = {"ignore_missing", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "|i", const_cast(kwlist), &ignore_missing)) { + return nullptr; + } + Widget* w = self->widget_->get(); + if (!w) { + if (!ignore_missing) { + throw Exception(PyExcType::kWidgetNotFound); + } + } else { + ContainerWidget* p = w->parent_widget(); + if (p) { + p->DeleteWidget(w); + } else { + Log("Error: Can't delete widget: no parent."); + } + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PythonClassWidget::AddDeleteCallback(PythonClassWidget* self, + PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + PyObject* call_obj; + static const char* kwlist[] = {"call", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &call_obj)) { + return nullptr; + } + Widget* w = self->widget_->get(); + if (!w) { + throw Exception(PyExcType::kWidgetNotFound); + } + w->AddOnDeleteCall(call_obj); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +PyTypeObject PythonClassWidget::type_obj; +PyMethodDef PythonClassWidget::tp_methods[] = { + {"exists", (PyCFunction)Exists, METH_NOARGS, + "exists() -> bool\n" + "\n" + "Returns whether the Widget still exists.\n" + "Most functionality will fail on a nonexistent widget.\n" + "\n" + "Note that you can also use the boolean operator for this same\n" + "functionality, so a statement such as \"if mywidget\" will do\n" + "the right thing both for Widget objects and values of None."}, + {"get_widget_type", (PyCFunction)GetWidgetType, METH_NOARGS, + "get_widget_type() -> str\n" + "\n" + "Return the internal type of the Widget as a string. Note that this is\n" + "different from the Python ba.Widget type, which is the same for all\n" + "widgets."}, + {"activate", (PyCFunction)Activate, METH_NOARGS, + "activate() -> None\n" + "\n" + "Activates a widget; the same as if it had been clicked."}, + {"get_children", (PyCFunction)GetChildren, METH_NOARGS, + "get_children() -> List[ba.Widget]\n" + "\n" + "Returns any child Widgets of this Widget."}, + {"get_screen_space_center", (PyCFunction)GetScreenSpaceCenter, METH_NOARGS, + "get_screen_space_center() -> Tuple[float, float]\n" + "\n" + "Returns the coords of the Widget center relative to the center of the\n" + "screen. This can be useful for placing pop-up windows and other special\n" + "cases."}, + {"get_selected_child", (PyCFunction)GetSelectedChild, METH_NOARGS, + "get_selected_child() -> Optional[ba.Widget]\n" + "\n" + "Returns the selected child Widget or None if nothing is selected."}, + // NOLINTNEXTLINE (signed bitwise stuff) + {"delete", (PyCFunction)Delete, METH_VARARGS | METH_KEYWORDS, + "delete(ignore_missing: bool = True) -> None\n" + "\n" + "Delete the Widget. Ignores already-deleted Widgets if ignore_missing\n" + " is True; otherwise an Exception is thrown."}, + {"add_delete_callback", (PyCFunction)AddDeleteCallback, + METH_VARARGS | METH_KEYWORDS, // NOLINT (signed bitwise stuff) + "add_delete_callback(call: Callable) -> None\n" + "\n" + "Add a call to be run immediately after this widget is destroyed."}, + {nullptr}}; + +} // namespace ballistica diff --git a/src/ballistica/python/class/python_class_widget.h b/src/ballistica/python/class/python_class_widget.h new file mode 100644 index 00000000..5b74f127 --- /dev/null +++ b/src/ballistica/python/class/python_class_widget.h @@ -0,0 +1,45 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_WIDGET_H_ +#define BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_WIDGET_H_ + +#include "ballistica/core/object.h" +#include "ballistica/python/class/python_class.h" + +namespace ballistica { + +class PythonClassWidget : public PythonClass { + public: + static void SetupType(PyTypeObject* obj); + static auto type_name() -> const char* { return "Widget"; } + static auto Create(Widget* widget) -> PyObject*; + static auto Check(PyObject* o) -> bool { + return PyObject_TypeCheck(o, &type_obj); + } + static PyTypeObject type_obj; + auto GetWidget() const -> Widget*; + + private: + static PyMethodDef tp_methods[]; + static auto tp_repr(PythonClassWidget* self) -> PyObject*; + static auto tp_new(PyTypeObject* type, PyObject* args, PyObject* keywds) + -> PyObject*; + static void tp_dealloc(PythonClassWidget* self); + static auto Exists(PythonClassWidget* self) -> PyObject*; + static auto GetWidgetType(PythonClassWidget* self) -> PyObject*; + static auto Activate(PythonClassWidget* self) -> PyObject*; + static auto GetChildren(PythonClassWidget* self) -> PyObject*; + static auto GetSelectedChild(PythonClassWidget* self) -> PyObject*; + static auto GetScreenSpaceCenter(PythonClassWidget* self) -> PyObject*; + static auto Delete(PythonClassWidget* self, PyObject* args, PyObject* keywds) + -> PyObject*; + static auto AddDeleteCallback(PythonClassWidget* self, PyObject* args, + PyObject* keywds) -> PyObject*; + Object::WeakRef* widget_; + static auto nb_bool(PythonClassWidget* self) -> int; + static PyNumberMethods as_number_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_CLASS_PYTHON_CLASS_WIDGET_H_ diff --git a/src/ballistica/python/methods/python_methods_app.cc b/src/ballistica/python/methods/python_methods_app.cc new file mode 100644 index 00000000..c5d8c3af --- /dev/null +++ b/src/ballistica/python/methods/python_methods_app.cc @@ -0,0 +1,1196 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/methods/python_methods_app.h" + +#include +#include + +#include "ballistica/app/app.h" +#include "ballistica/app/app_globals.h" +#include "ballistica/core/logging.h" +#include "ballistica/game/game_stream.h" +#include "ballistica/game/host_activity.h" +#include "ballistica/game/session/host_session.h" +#include "ballistica/game/session/replay_client_session.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/media/component/texture.h" +#include "ballistica/python/class/python_class_activity_data.h" +#include "ballistica/python/class/python_class_session_data.h" +#include "ballistica/python/python.h" +#include "ballistica/python/python_context_call_runnable.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +// Python does lots of signed bitwise stuff; turn off those warnings here. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +#pragma ide diagnostic ignored "RedundantCast" + +auto PyAppName(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("app_name"); + + // This will get subbed out by standard filtering. + return PyUnicode_FromString("ballisticacore"); + BA_PYTHON_CATCH; +} + +auto PyAppNameUpper(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("app_name_upper"); + + // This will get subbed out by standard filtering. + return PyUnicode_FromString("BallisticaCore"); + BA_PYTHON_CATCH; +} + +auto PyIsXCodeBuild(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("is_xcode_build"); + if (g_buildconfig.xcode_build()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +auto PyCanDisplayFullUnicode(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("can_display_full_unicode"); + if (g_buildconfig.enable_os_font_rendering()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +auto PyGetSession(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_session"); + int raise = true; + static const char* kwlist[] = {"doraise", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|i", + const_cast(kwlist), &raise)) { + return nullptr; + } + if (HostSession* hs = Context::current().GetHostSession()) { + PyObject* obj = hs->GetSessionPyObj(); + if (obj) { + Py_INCREF(obj); + return obj; + } + } else { + if (raise) { + throw Exception(PyExcType::kSessionNotFound); + } + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyNewHostSession(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("new_host_session"); + const char* benchmark_type_str = nullptr; + static const char* kwlist[] = {"sessiontype", "benchmark_type", nullptr}; + PyObject* sessiontype_obj; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|s", + const_cast(kwlist), &sessiontype_obj, + &benchmark_type_str)) { + return nullptr; + } + BenchmarkType benchmark_type = BenchmarkType::kNone; + if (benchmark_type_str != nullptr) { + if (!strcmp(benchmark_type_str, "cpu")) { + benchmark_type = BenchmarkType::kCPU; + } else if (!strcmp(benchmark_type_str, "gpu")) { + benchmark_type = BenchmarkType::kGPU; + } else { + throw Exception( + "Invalid benchmark type: '" + std::string(benchmark_type_str) + "'", + PyExcType::kValue); + } + } + g_game->LaunchHostSession(sessiontype_obj, benchmark_type); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyNewReplaySession(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("new_replay_session"); + std::string file_name; + PyObject* file_name_obj; + static const char* kwlist[] = {"file_name", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "O", const_cast(kwlist), &file_name_obj)) { + return nullptr; + } + file_name = Python::GetPyString(file_name_obj); + g_game->LaunchReplaySession(file_name); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyIsInReplay(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("is_in_replay"); + BA_PRECONDITION(InGameThread()); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + if (dynamic_cast(g_game->GetForegroundSession())) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +auto PyRegisterSession(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("register_session"); + assert(InGameThread()); + PyObject* session_obj; + static const char* kwlist[] = {"session", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &session_obj)) { + return nullptr; + } + HostSession* hsc = Context::current().GetHostSession(); + if (!hsc) { + throw Exception("No HostSession found."); + } + + // Store our py obj with our HostSession and return + // the HostSession to be stored with our py obj. + hsc->RegisterPySession(session_obj); + return PythonClassSessionData::Create(hsc); + BA_PYTHON_CATCH; +} + +auto PyRegisterActivity(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("register_activity"); + assert(InGameThread()); + PyObject* activity_obj; + static const char* kwlist[] = {"activity", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &activity_obj)) { + return nullptr; + } + HostSession* hs = Context::current().GetHostSession(); + if (!hs) { + throw Exception("No HostSession found"); + } + + // Generate and return an ActivityData for this guy.. + // (basically just a link to its C++ equivalent). + return PythonClassActivityData::Create(hs->RegisterPyActivity(activity_obj)); + BA_PYTHON_CATCH; +} + +auto PyGetForegroundHostSession(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_foreground_host_session"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + + // Note: we return None if not in the game thread. + HostSession* s = InGameThread() + ? g_game->GetForegroundContext().GetHostSession() + : nullptr; + if (s != nullptr) { + PyObject* obj = s->GetSessionPyObj(); + Py_INCREF(obj); + return obj; + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyNewActivity(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("new_activity"); + + static const char* kwlist[] = {"activity_type", "settings", nullptr}; + PyObject* activity_type_obj; + PyObject* settings_obj = Py_None; + PythonRef settings; + + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|O", + const_cast(kwlist), + &activity_type_obj, &settings_obj)) { + return nullptr; + } + + // If they passed a settings dict, make a shallow copy of it (so we dont + // inadvertently mess up level lists or whatever the settings came from). + if (settings_obj != Py_None) { + if (!PyDict_Check(settings_obj)) { + throw Exception("Expected a dict for settings.", PyExcType::kType); + } + PythonRef args2(Py_BuildValue("(O)", settings_obj), PythonRef::kSteal); + settings = g_python->obj(Python::ObjID::kShallowCopyCall).Call(args2); + if (!settings.exists()) { + throw Exception("Unable to shallow-copy settings."); + } + } else { + settings.Acquire(settings_obj); + } + + HostSession* hs = Context::current().GetHostSession(); + if (!hs) { + throw Exception("No HostSession found.", PyExcType::kContext); + } + return hs->NewHostActivity(activity_type_obj, settings.get()); + + BA_PYTHON_CATCH; +} + +auto PyGetActivity(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_activity"); + int raise = true; + static const char* kwlist[] = {"doraise", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|i", + const_cast(kwlist), &raise)) { + return nullptr; + } + + // Fail gracefully if called from outside the game thread. + if (!InGameThread()) { + Py_RETURN_NONE; + } + + if (HostActivity* hostactivity = Context::current().GetHostActivity()) { + PyObject* obj = hostactivity->GetPyActivity(); + Py_INCREF(obj); + return obj; + } else { + if (raise) { + throw Exception(PyExcType::kActivityNotFound); + } + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyPushCall(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("push_call"); + PyObject* call_obj; + int from_other_thread{}; + int suppress_warning{}; + static const char* kwlist[] = {"call", "from_other_thread", + "suppress_other_thread_warning", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|ip", + const_cast(kwlist), &call_obj, + &from_other_thread, &suppress_warning)) { + return nullptr; + } + + // The from-other-thread case is basically a different call. + if (from_other_thread) { + // Warn the user not to use this from the game thread since it doesnt + // save/restore context. + if (!suppress_warning && InGameThread()) { + g_python->IssueCallInGameThreadWarning(call_obj); + } + + // This gets called from other python threads so we can't construct + // Objects and things here or we'll trip our thread-checks. Instead we + // just increment the python object's refcount and pass it along raw; + // the game thread decrements it on the other end. + Py_INCREF(call_obj); + g_game->PushPythonRawCallable(call_obj); + } else { + if (!InGameThread()) { + throw Exception("You must use from_other_thread mode."); + } + g_game->PushPythonCall(Object::New(call_obj)); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyTime(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("time"); + + PyObject* time_type_obj = nullptr; + PyObject* time_format_obj = nullptr; + static const char* kwlist[] = {"timetype", "timeformat", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|OO", + const_cast(kwlist), &time_type_obj, + &time_format_obj)) { + return nullptr; + } + + auto time_type = TimeType::kSim; + if (time_type_obj != nullptr) { + time_type = Python::GetPyEnum_TimeType(time_type_obj); + } + auto time_format = TimeFormat::kSeconds; + if (time_format_obj != nullptr) { + time_format = Python::GetPyEnum_TimeFormat(time_format_obj); + } + + millisecs_t timeval; + if (time_type == TimeType::kReal) { + // Special case; we don't require a context for 'real'. + timeval = GetRealTime(); + } else { + // Make sure we've got a valid context-target and ask it for + // this type of time. + if (!Context::current().target.exists()) { + throw Exception(PyExcType::kContext); + } + timeval = Context::current().target->GetTime(time_type); + } + + if (time_format == TimeFormat::kSeconds) { + return PyFloat_FromDouble(0.001 * timeval); + } else if (time_format == TimeFormat::kMilliseconds) { + return PyLong_FromLong(static_cast_check_fit(timeval)); // NOLINT + } else { + throw Exception( + "Invalid timeformat: " + std::to_string(static_cast(time_format)), + PyExcType::kValue); + } + BA_PYTHON_CATCH; +} + +auto PyTimer(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + assert(InGameThread()); + Platform::SetLastPyCall("timer"); + + PyObject* length_obj; + int64_t length; + int repeat = 0; + int suppress_format_warning = 0; + PyObject* call_obj; + PyObject* time_type_obj = nullptr; + PyObject* time_format_obj = nullptr; + static const char* kwlist[] = {"time", "call", + "repeat", "timetype", + "timeformat", "suppress_format_warning", + nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "OO|pOOp", const_cast(kwlist), &length_obj, + &call_obj, &repeat, &time_type_obj, &time_format_obj, + &suppress_format_warning)) { + return nullptr; + } + + auto time_type = TimeType::kSim; + if (time_type_obj != nullptr) { + time_type = Python::GetPyEnum_TimeType(time_type_obj); + } + auto time_format = TimeFormat::kSeconds; + if (time_format_obj != nullptr) { + time_format = Python::GetPyEnum_TimeFormat(time_format_obj); + } + +#if BA_TEST_BUILD || BA_DEBUG_BUILD + if (!suppress_format_warning) { + g_python->TimeFormatCheck(time_format, length_obj); + } +#endif + + // We currently work with integer milliseconds internally. + if (time_format == TimeFormat::kSeconds) { + length = static_cast(Python::GetPyDouble(length_obj) * 1000.0); + } else if (time_format == TimeFormat::kMilliseconds) { + length = Python::GetPyInt64(length_obj); + } else { + throw Exception("invalid timeformat: '" + + std::to_string(static_cast(time_format)) + "'", + PyExcType::kValue); + } + if (length < 0) { + throw Exception("Timer length < 0", PyExcType::kValue); + } + + // Grab a ref to this here so it doesn't leak on exceptions. + auto runnable(Object::New(call_obj)); + + // Special case; we disallow repeating real timers currently. + if (time_type == TimeType::kReal && repeat) { + throw Exception("Repeating real timers not allowed here; use ba.Timer().", + PyExcType::kValue); + } + + // Now just make sure we've got a valid context-target and ask us to + // make us a timer. + if (!Context::current().target.exists()) { + throw Exception(PyExcType::kContext); + } + Context::current().target->NewTimer(time_type, length, + static_cast(repeat), runnable); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyScreenMessage(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("screen_message"); + const char* message = nullptr; + PyObject* color_obj = Py_None; + int top = 0; + int transient = 0; + PyObject* image_obj = Py_None; + PyObject* message_obj; + PyObject* clients_obj = Py_None; + int log = 0; + static const char* kwlist[] = {"message", "color", "top", "image", + "log", "clients", "transient", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "O|OpOiOi", const_cast(kwlist), &message_obj, + &color_obj, &top, &image_obj, &log, &clients_obj, &transient)) { + return nullptr; + } + std::string message_str = Python::GetPyString(message_obj); + message = message_str.c_str(); + Vector3f color{1, 1, 1}; + if (color_obj != Py_None) { + color = Python::GetPyVector3f(color_obj); + } + if (message == nullptr) { + PyErr_SetString(PyExc_AttributeError, "No message provided"); + return nullptr; + } + if (log) { + Log(message); + } + + // Transient messages get sent to clients as high-level messages instead of + // being embedded into the game-stream. + if (transient) { + // This option doesn't support top or icons currently. + if (image_obj != Py_None) { + throw Exception( + "The 'image' option is not currently supported for transient mode " + "messages.", + PyExcType::kValue); + } + if (top) { + throw Exception( + "The 'top' option is not currently supported for transient mode " + "messages.", + PyExcType::kValue); + } + std::vector client_ids; + if (clients_obj != Py_None) { + std::vector client_ids2 = Python::GetPyInts(clients_obj); + g_game->SendScreenMessageToSpecificClients(message, color.x, color.y, + color.z, client_ids2); + } else { + g_game->SendScreenMessageToAll(message, color.x, color.y, color.z); + } + } else { + // Currently specifying client_ids only works for transient messages; we'd + // need a protocol change to support that in game output streams. + // (or maintaining separate streams per client; yuck) + if (clients_obj != Py_None) { + throw Exception( + "Specifying clients only works when using the 'transient' option", + PyExcType::kValue); + } + Scene* context_scene = Context::current().GetMutableScene(); + GameStream* output_stream = + context_scene ? context_scene->GetGameStream() : nullptr; + + Texture* texture = nullptr; + Texture* tint_texture = nullptr; + Vector3f tint_color{1.0f, 1.0f, 1.0f}; + Vector3f tint2_color{1.0f, 1.0f, 1.0f}; + if (image_obj != Py_None) { + if (PyDict_Check(image_obj)) { + PyObject* obj = PyDict_GetItemString(image_obj, "texture"); + if (!obj) + throw Exception("Provided image dict contains no 'texture' entry.", + PyExcType::kValue); + texture = Python::GetPyTexture(obj); + + obj = PyDict_GetItemString(image_obj, "tint_texture"); + if (!obj) + throw Exception( + "Provided image dict contains no 'tint_texture' entry.", + PyExcType::kValue); + tint_texture = Python::GetPyTexture(obj); + + obj = PyDict_GetItemString(image_obj, "tint_color"); + if (!obj) + throw Exception("Provided image dict contains no 'tint_color' entry", + PyExcType::kValue); + tint_color = Python::GetPyVector3f(obj); + obj = PyDict_GetItemString(image_obj, "tint2_color"); + if (!obj) + throw Exception("Provided image dict contains no 'tint2_color' entry", + PyExcType::kValue); + tint2_color = Python::GetPyVector3f(obj); + } else { + texture = Python::GetPyTexture(image_obj); + } + } + if (output_stream) { + // FIXME: for now we just do bottom messages. + if (texture == nullptr && !top) { + output_stream->ScreenMessageBottom(message, color.x, color.y, color.z); + } else if (top && texture != nullptr && tint_texture != nullptr) { + if (texture->scene() != context_scene) { + throw Exception("Texture is not from the current context.", + PyExcType::kContext); + } + if (tint_texture->scene() != context_scene) + throw Exception("Tint-texture is not from the current context.", + PyExcType::kContext); + output_stream->ScreenMessageTop( + message, color.x, color.y, color.z, texture, tint_texture, + tint_color.x, tint_color.y, tint_color.z, tint2_color.x, + tint2_color.y, tint2_color.z); + } else { + Log("Error: unhandled screenmessage output_stream case"); + } + } + + // Now display it locally. + g_graphics->AddScreenMessage(message, color, static_cast(top), + texture, tint_texture, tint_color, + tint2_color); + } + + Py_RETURN_NONE; + + BA_PYTHON_CATCH; +} + +auto PyQuit(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("quit"); + static const char* kwlist[] = {"soft", "back", nullptr}; + int soft = 0; + int back = 0; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|ii", + const_cast(kwlist), &soft, &back)) { + return nullptr; + } + + // FIXME this should all just go through platform + + if (g_buildconfig.ostype_ios_tvos()) { + // This should never be called on iOS + Log("Error: Quit called."); + } + + bool handled = false; + + // A few types get handled specially on android. + if (g_buildconfig.ostype_android()) { + if (!handled && back) { + // Back-quit simply synthesizes a back press. + // Note to self: I remember this behaved slightly differently than + // doing a soft quit but I should remind myself how... + g_platform->AndroidSynthesizeBackPress(); + handled = true; + } + if (!handled && soft) { + // Soft-quit just kills our activity but doesn't run app shutdown. + // Thus we'll be able to spin back up (reset to the main menu) + // if the user re-launches us. + g_platform->AndroidQuitActivity(); + handled = true; + } + } + if (!handled) { + g_game->PushShutdownCall(false); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +#if BA_DEBUG_BUILD +auto PyBless(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("bless"); + ScreenMessage("WOULD BLESS BUILD " + std::to_string(kAppBuildNumber)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} +#endif // BA_DEBUG_BUILD + +auto PyApplyConfig(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("apply_config"); + + // Hmm; python runs in the game thread; technically we could just run + // ApplyConfig() immediately (though pushing is probably safer). + g_game->PushApplyConfigCall(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyCommitConfig(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("commit_config"); + PyObject* config_obj; + static const char* kwlist[] = {"config", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &config_obj)) { + return nullptr; + } + if (config_obj == nullptr || !Python::IsPyString(config_obj)) { + throw Exception("ERROR ON JSON DUMP"); + } + std::string final_str = Python::GetPyString(config_obj); + std::string path = g_platform->GetConfigFilePath(); + std::string path_temp = path + ".tmp"; + std::string path_prev = path + ".prev"; + if (explicit_bool(true)) { + FILE* f_out = g_platform->FOpen(path_temp.c_str(), "wb"); + if (f_out == nullptr) { + throw Exception("Error opening config file for writing: '" + path_temp + + "': " + g_platform->GetErrnoString()); + } + + // Write to temp file. + size_t result = fwrite(&final_str[0], final_str.size(), 1, f_out); + if (result != 1) { + fclose(f_out); + throw Exception("Error writing config file to '" + path_temp + + "': " + g_platform->GetErrnoString()); + } + fclose(f_out); + + // Now backup any existing config to .prev. + if (g_platform->FilePathExists(path)) { + // On windows, rename doesn't overwrite existing files.. need to kill + // the old explicitly. + // (hmm; should we just do this everywhere for consistency?) + if (g_buildconfig.ostype_windows()) { + if (g_platform->FilePathExists(path_prev)) { + int result2 = g_platform->Remove(path_prev.c_str()); + if (result2 != 0) { + throw Exception("Error removing prev config file '" + path_prev + + "': " + g_platform->GetErrnoString()); + } + } + } + int result2 = g_platform->Rename(path.c_str(), path_prev.c_str()); + if (result2 != 0) { + throw Exception("Error backing up config file to '" + path_prev + + "': " + g_platform->GetErrnoString()); + } + } + + // Now move temp into place. + int result2 = g_platform->Rename(path_temp.c_str(), path.c_str()); + if (result2 != 0) { + throw Exception("Error renaming temp config file to final '" + path + + "': " + g_platform->GetErrnoString()); + } + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyEnv(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("env"); + + static PyObject* env_obj = nullptr; + + // Just build this once and recycle it. + if (env_obj == nullptr) { + std::string config_path = g_platform->GetConfigFilePath(); + PyObject* is_debug_build_obj; +#if BA_DEBUG_BUILD + is_debug_build_obj = Py_True; +#else + is_debug_build_obj = Py_False; +#endif + PyObject* is_test_build_obj; +#if BA_TEST_BUILD + is_test_build_obj = Py_True; +#else + is_test_build_obj = Py_False; +#endif + bool demo_mode{g_buildconfig.demo_build()}; + bool arcade_mode{g_buildconfig.arcade_build()}; + bool iircade_mode{g_buildconfig.arcade_build()}; + + const char* ui_scale; + switch (GetInterfaceType()) { + case UIScale::kLarge: + ui_scale = "large"; + break; + case UIScale::kMedium: + ui_scale = "medium"; + break; + case UIScale::kSmall: + ui_scale = "small"; + break; + default: + throw Exception(); + } + // clang-format off + env_obj = Py_BuildValue( + "{" + "si" // build_number + "ss" // config_file_path + "ss" // locale + "ss" // user_agent_string + "ss" // version + "sO" // debug_build + "sO" // test_build + "ss" // python_directory_user + "ss" // python_directory_app + "ss" // platform + "ss" // subplatform + "ss" // ui_scale + "sO" // on_tv + "sO" // vr_mode + "sO" // toolbar_test + "sO" // demo_mode + "sO" // arcade_mode + "sO" // iircade_mode + "si" // protocol_version + "sO" // headless_mode + "ss" // python_directory_app_site + "}", + "build_number", kAppBuildNumber, + "config_file_path", config_path.c_str(), + "locale", g_platform->GetLocale().c_str(), + "user_agent_string", g_app_globals->user_agent_string.c_str(), + "version", kAppVersion, + "debug_build", is_debug_build_obj, + "test_build", is_test_build_obj, + "python_directory_user", g_platform->GetUserPythonDirectory().c_str(), + "python_directory_app", g_platform->GetAppPythonDirectory().c_str(), + "platform", g_platform->GetPlatformName().c_str(), + "subplatform", g_platform->GetSubplatformName().c_str(), + "ui_scale", ui_scale, + "on_tv", g_platform->IsRunningOnTV() ? Py_True : Py_False, + "vr_mode", IsVRMode() ? Py_True : Py_False, + "toolbar_test", BA_TOOLBAR_TEST ? Py_True : Py_False, + "demo_mode", demo_mode ? Py_True : Py_False, + "arcade_mode", g_buildconfig.arcade_build() ? Py_True : Py_False, + "iircade_mode", g_buildconfig.iircade_build() ? Py_True: Py_False, + "protocol_version", kProtocolVersion, + "headless_mode", HeadlessMode() ? Py_True : Py_False, + "python_directory_app_site", + g_platform->GetSitePythonDirectory().c_str()); + // clang-format on + } + Py_INCREF(env_obj); + g_python->set_env_obj(env_obj); + return env_obj; + BA_PYTHON_CATCH; +} + +auto PySetStressTesting(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_stress_testing"); + int testing; + int player_count; + if (!PyArg_ParseTuple(args, "pi", &testing, &player_count)) { + return nullptr; + } + g_app->PushSetStressTestingCall(static_cast(testing), player_count); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyPrintStdout(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("print_stdout"); + const char* s; + if (!PyArg_ParseTuple(args, "s", &s)) { + return nullptr; + } + Logging::PrintStdout(s); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyPrintStderr(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("print_stderr"); + const char* s; + if (!PyArg_ParseTuple(args, "s", &s)) { + return nullptr; + } + Logging::PrintStderr(s); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyLog(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("log"); + static const char* kwlist[] = {"message", "to_stdout", "to_server", nullptr}; + int to_server = 1; + int to_stdout = 1; + std::string message; + PyObject* message_obj; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|pp", + const_cast(kwlist), &message_obj, + &to_stdout, &to_server)) { + return nullptr; + } + message = Python::GetPyString(message_obj); + Log(message, static_cast(to_stdout), static_cast(to_server)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyTimeFormatCheck(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("time_format_check"); + static const char* kwlist[] = {"time_format", "length", nullptr}; + PyObject* time_format_obj; + PyObject* length_obj; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "OO", + const_cast(kwlist), &time_format_obj, + &length_obj)) { + return nullptr; + } + auto time_format = Python::GetPyEnum_TimeFormat(time_format_obj); + + g_python->TimeFormatCheck(time_format, length_obj); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +PyMethodDef PythonMethodsApp::methods_def[] = { + {"appname", (PyCFunction)PyAppName, METH_NOARGS, + "appname() -> str\n" + "\n" + "(internal)\n"}, + {"appnameupper", (PyCFunction)PyAppNameUpper, METH_NOARGS, + "appnameupper() -> str\n" + "\n" + "(internal)\n" + "\n" + "Return whether this build of the game can display full unicode such as\n" + "Emoji, Asian languages, etc.\n"}, + {"is_xcode_build", (PyCFunction)PyIsXCodeBuild, METH_NOARGS, + "is_xcode_build() -> bool\n" + "\n" + "(internal)\n"}, + {"can_display_full_unicode", (PyCFunction)PyCanDisplayFullUnicode, + METH_NOARGS, + "can_display_full_unicode() -> bool\n" + "\n" + "(internal)\n"}, + {"time_format_check", (PyCFunction)PyTimeFormatCheck, + METH_VARARGS | METH_KEYWORDS, + "time_format_check(time_format: ba.TimeFormat, length: Union[float, " + "int])\n" + " -> None\n" + "\n" + "(internal)\n" + "\n" + "Logs suspicious time values for timers or animate calls.\n" + "\n" + "(for helping with the transition from milliseconds-based time calls\n" + "to seconds-based ones)"}, + + {"log", (PyCFunction)PyLog, METH_VARARGS | METH_KEYWORDS, + "log(message: str, to_stdout: bool = True,\n" + " to_server: bool = True) -> None\n" + "\n" + "Category: General Utility Functions\n" + "\n" + "Log a message. This goes to the default logging mechanism depending\n" + "on the platform (stdout on mac, android log on android, etc).\n" + "\n" + "Log messages also go to the in-game console unless 'to_console'\n" + "is False. They are also sent to the master-server for use in analyzing\n" + "issues unless to_server is False.\n" + "\n" + "Python's standard print() is wired to call this (with default values)\n" + "so in most cases you can just use that."}, + + {"print_stdout", PyPrintStdout, METH_VARARGS, + "print_stdout(message: str) -> None\n" + "\n" + "(internal)"}, + + {"print_stderr", PyPrintStderr, METH_VARARGS, + "print_stderr(message: str) -> None\n" + "\n" + "(internal)"}, + + {"set_stress_testing", PySetStressTesting, METH_VARARGS, + "set_stress_testing(testing: bool, player_count: int) -> None\n" + "\n" + "(internal)"}, + + {"env", (PyCFunction)PyEnv, METH_NOARGS, + "env() -> dict\n" + "\n" + "(internal)\n" + "\n" + "Returns a dict containing general info about the operating environment\n" + "such as version, platform, etc.\n" + "This info is now exposed through ba.App; refer to those docs for\n" + "info on specific elements."}, + + {"commit_config", (PyCFunction)PyCommitConfig, METH_VARARGS | METH_KEYWORDS, + "commit_config(config: str) -> None\n" + "\n" + "(internal)"}, + + {"apply_config", PyApplyConfig, METH_VARARGS, + "apply_config() -> None\n" + "\n" + "(internal)"}, + +#if BA_DEBUG_BUILD + {"bless", (PyCFunction)PyBless, METH_VARARGS | METH_KEYWORDS, + "bless() -> None\n" + "\n" + "(internal)"}, +#endif + + {"quit", (PyCFunction)PyQuit, METH_VARARGS | METH_KEYWORDS, + "quit(soft: bool = False, back: bool = False) -> None\n" + "\n" + "Quit the game.\n" + "\n" + "Category: General Utility Functions\n" + "\n" + "On systems like android, 'soft' will end the activity but keep the\n" + "app running."}, + + {"screenmessage", (PyCFunction)PyScreenMessage, + METH_VARARGS | METH_KEYWORDS, + "screenmessage(message: Union[str, ba.Lstr],\n" + " color: Sequence[float] = None, top: bool = False,\n" + " image: Dict[str, Any] = None, log: bool = False,\n" + " clients: Sequence[int] = None, transient: bool = False) -> None\n" + "\n" + "Print a message to the local client's screen, in a given color.\n" + "\n" + "Category: General Utility Functions\n" + "\n" + "If 'top' is True, the message will go to the top message area.\n" + "For 'top' messages, 'image' can be a texture to display alongside the\n" + "message.\n" + "If 'log' is True, the message will also be printed to the output log\n" + "'clients' can be a list of client-ids the message should be sent to,\n" + "or None to specify that everyone should receive it.\n" + "If 'transient' is True, the message will not be included in the\n" + "game-stream and thus will not show up when viewing replays.\n" + "Currently the 'clients' option only works for transient messages."}, + + {"timer", (PyCFunction)PyTimer, METH_VARARGS | METH_KEYWORDS, + "timer(time: float, call: Callable[[], Any], repeat: bool = False,\n" + " timetype: ba.TimeType = TimeType.SIM,\n" + " timeformat: ba.TimeFormat = TimeFormat.SECONDS,\n" + " suppress_format_warning: bool = False)\n" + " -> None\n" + "\n" + "Schedule a call to run at a later point in time.\n" + "\n" + "Category: General Utility Functions\n" + "\n" + "This function adds a timer to the current ba.Context.\n" + "This timer cannot be canceled or modified once created. If you\n" + " require the ability to do so, use the ba.Timer class instead.\n" + "\n" + "time: length of time (in seconds by default) that the timer will wait\n" + "before firing. Note that the actual delay experienced may vary\n " + "depending on the timetype. (see below)\n" + "\n" + "call: A callable Python object. Note that the timer will retain a\n" + "strong reference to the callable for as long as it exists, so you\n" + "may want to look into concepts such as ba.WeakCall if that is not\n" + "desired.\n" + "\n" + "repeat: if True, the timer will fire repeatedly, with each successive\n" + "firing having the same delay as the first.\n" + "\n" + "timetype can be either 'sim', 'base', or 'real'. It defaults to\n" + "'sim'. Types are explained below:\n" + "\n" + "'sim' time maps to local simulation time in ba.Activity or ba.Session\n" + "Contexts. This means that it may progress slower in slow-motion play\n" + "modes, stop when the game is paused, etc. This time type is not\n" + "available in UI contexts.\n" + "\n" + "'base' time is also linked to gameplay in ba.Activity or ba.Session\n" + "Contexts, but it progresses at a constant rate regardless of\n " + "slow-motion states or pausing. It can, however, slow down or stop\n" + "in certain cases such as network outages or game slowdowns due to\n" + "cpu load. Like 'sim' time, this is unavailable in UI contexts.\n" + "\n" + "'real' time always maps to actual clock time with a bit of filtering\n" + "added, regardless of Context. (the filtering prevents it from going\n" + "backwards or jumping forward by large amounts due to the app being\n" + "backgrounded, system time changing, etc.)\n" + "Real time timers are currently only available in the UI context.\n" + "\n" + "the 'timeformat' arg defaults to seconds but can also be milliseconds.\n" + "\n" + "# timer example: print some stuff through time:\n" + "ba.screenmessage('hello from now!')\n" + "ba.timer(1.0, ba.Call(ba.screenmessage, 'hello from the future!'))\n" + "ba.timer(2.0, ba.Call(ba.screenmessage, 'hello from the future 2!'))\n"}, + + {"time", (PyCFunction)PyTime, METH_VARARGS | METH_KEYWORDS, + "time(timetype: ba.TimeType = TimeType.SIM,\n" + " timeformat: ba.TimeFormat = TimeFormat.SECONDS)\n" + " -> \n" + "\n" + "Return the current time.\n" + "\n" + "Category: General Utility Functions\n" + "\n" + "The time returned depends on the current ba.Context and timetype.\n" + "\n" + "timetype can be either SIM, BASE, or REAL. It defaults to\n" + "SIM. Types are explained below:\n" + "\n" + "SIM time maps to local simulation time in ba.Activity or ba.Session\n" + "Contexts. This means that it may progress slower in slow-motion play\n" + "modes, stop when the game is paused, etc. This time type is not\n" + "available in UI contexts.\n" + "\n" + "BASE time is also linked to gameplay in ba.Activity or ba.Session\n" + "Contexts, but it progresses at a constant rate regardless of\n " + "slow-motion states or pausing. It can, however, slow down or stop\n" + "in certain cases such as network outages or game slowdowns due to\n" + "cpu load. Like 'sim' time, this is unavailable in UI contexts.\n" + "\n" + "REAL time always maps to actual clock time with a bit of filtering\n" + "added, regardless of Context. (the filtering prevents it from going\n" + "backwards or jumping forward by large amounts due to the app being\n" + "backgrounded, system time changing, etc.)\n" + "\n" + "the 'timeformat' arg defaults to SECONDS which returns float seconds,\n" + "but it can also be MILLISECONDS to return integer milliseconds.\n" + "\n" + "Note: If you need pure unfiltered clock time, just use the standard\n" + "Python functions such as time.time()."}, + + {"pushcall", (PyCFunction)PyPushCall, METH_VARARGS | METH_KEYWORDS, + "pushcall(call: Callable, from_other_thread: bool = False,\n" + " suppress_other_thread_warning: bool = False ) -> None\n" + "\n" + "Pushes a call onto the event loop to be run during the next cycle.\n" + "\n" + "Category: General Utility Functions\n" + "\n" + "This can be handy for calls that are disallowed from within other\n" + "callbacks, etc.\n" + "\n" + "This call expects to be used in the game thread, and will automatically\n" + "save and restore the ba.Context to behave seamlessly.\n" + "\n" + "If you want to push a call from outside of the game thread,\n" + "however, you can pass 'from_other_thread' as True. In this case\n" + "the call will always run in the UI context on the game thread."}, + + {"getactivity", (PyCFunction)PyGetActivity, METH_VARARGS | METH_KEYWORDS, + "getactivity(doraise: bool = True) -> \n" + "\n" + "Return the current ba.Activity instance.\n" + "\n" + "Category: Gameplay Functions\n" + "\n" + "Note that this is based on context; thus code run in a timer generated\n" + "in Activity 'foo' will properly return 'foo' here, even if another\n" + "Activity has since been created or is transitioning in.\n" + "If there is no current Activity, raises a ba.ActivityNotFoundError.\n" + "If doraise is False, None will be returned instead in that case."}, + + {"newactivity", (PyCFunction)PyNewActivity, METH_VARARGS | METH_KEYWORDS, + "newactivity(activity_type: Type[ba.Activity],\n" + " settings: dict = None) -> ba.Activity\n" + "\n" + "Instantiates a ba.Activity given a type object.\n" + "\n" + "Category: General Utility Functions\n" + "\n" + "Activities require special setup and thus cannot be directly\n" + "instantiated; you must go through this function."}, + + {"get_foreground_host_session", (PyCFunction)PyGetForegroundHostSession, + METH_VARARGS | METH_KEYWORDS, + "get_foreground_host_session() -> Optional[ba.Session]\n" + "\n" + "(internal)\n" + "\n" + "Return the ba.Session currently being displayed, or None if there is\n" + "none."}, + + {"register_activity", (PyCFunction)PyRegisterActivity, + METH_VARARGS | METH_KEYWORDS, + "register_activity(activity: ba.Activity) -> ActivityData\n" + "\n" + "(internal)"}, + + {"register_session", (PyCFunction)PyRegisterSession, + METH_VARARGS | METH_KEYWORDS, + "register_session(session: ba.Session) -> SessionData\n" + "\n" + "(internal)"}, + + {"is_in_replay", (PyCFunction)PyIsInReplay, METH_VARARGS | METH_KEYWORDS, + "is_in_replay() -> bool\n" + "\n" + "(internal)"}, + + {"new_replay_session", (PyCFunction)PyNewReplaySession, + METH_VARARGS | METH_KEYWORDS, + "new_replay_session(file_name: str) -> None\n" + "\n" + "(internal)"}, + + {"new_host_session", (PyCFunction)PyNewHostSession, + METH_VARARGS | METH_KEYWORDS, + "new_host_session(sessiontype: Type[ba.Session],\n" + " benchmark_type: str = None) -> None\n" + "\n" + "(internal)"}, + + {"getsession", (PyCFunction)PyGetSession, METH_VARARGS | METH_KEYWORDS, + "getsession(doraise: bool = True) -> \n" + "\n" + "Category: Gameplay Functions\n" + "\n" + "Returns the current ba.Session instance.\n" + "Note that this is based on context; thus code being run in the UI\n" + "context will return the UI context here even if a game Session also\n" + "exists, etc. If there is no current Session, an Exception is raised, " + "or\n" + "if doraise is False then None is returned instead."}, + + {nullptr, nullptr, 0, nullptr}}; + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/ballistica/python/methods/python_methods_app.h b/src/ballistica/python/methods/python_methods_app.h new file mode 100644 index 00000000..26467755 --- /dev/null +++ b/src/ballistica/python/methods/python_methods_app.h @@ -0,0 +1,17 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_APP_H_ +#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_APP_H_ + +#include "ballistica/python/python_sys.h" + +namespace ballistica { + +/// App related individual python methods for our module. +class PythonMethodsApp { + public: + static PyMethodDef methods_def[]; +}; + +} // namespace ballistica +#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_APP_H_ diff --git a/src/ballistica/python/methods/python_methods_gameplay.cc b/src/ballistica/python/methods/python_methods_gameplay.cc new file mode 100644 index 00000000..54f3c7b2 --- /dev/null +++ b/src/ballistica/python/methods/python_methods_gameplay.cc @@ -0,0 +1,765 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/methods/python_methods_gameplay.h" + +#include +#include +#include + +#include "ballistica/app/app.h" +#include "ballistica/dynamics/bg/bg_dynamics.h" +#include "ballistica/dynamics/collision.h" +#include "ballistica/dynamics/dynamics.h" +#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/game/connection/connection_to_client.h" +#include "ballistica/game/game_stream.h" +#include "ballistica/game/host_activity.h" +#include "ballistica/game/player_spec.h" +#include "ballistica/generic/json.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/input/device/input_device.h" +#include "ballistica/media/component/sound.h" +#include "ballistica/platform/platform.h" +#include "ballistica/python/python.h" +#include "ballistica/python/python_context_call_runnable.h" +#include "ballistica/scene/node/node.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.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" + +auto PyNewNode(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("new_node"); + Node* n = g_python->DoNewNode(args, keywds); + if (!n) { + return nullptr; + } + return n->NewPyRef(); + BA_PYTHON_CATCH; +} + +auto PyPrintNodes(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("print_nodes"); + HostActivity* host_activity = + g_game->GetForegroundContext().GetHostActivity(); + if (!host_activity) { + throw Exception(PyExcType::kContext); + } + Scene* scene = host_activity->scene(); + std::string s; + int count = 1; + for (auto&& i : scene->nodes()) { + char buffer[128]; + snprintf(buffer, sizeof(buffer), "#%d: type: %-14s desc: %s", count, + i->type()->name().c_str(), i->label().c_str()); + s += buffer; + Log(buffer); + count++; + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetNodes(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_nodes"); + HostActivity* host_activity = Context::current().GetHostActivity(); + if (!host_activity) { + throw Exception(PyExcType::kContext); + } + Scene* scene = host_activity->scene(); + PyObject* py_list = PyList_New(0); + for (auto&& i : scene->nodes()) { + PyList_Append(py_list, i->BorrowPyRef()); + } + return py_list; + BA_PYTHON_CATCH; +} + +static auto DoGetCollideValue(Dynamics* dynamics, const Collision* c, + const char* name) -> PyObject* { + BA_PYTHON_TRY; + if (!strcmp(name, "depth")) { + return Py_BuildValue("f", c->depth); + } else if (!strcmp(name, "position")) { + return Py_BuildValue("(fff)", c->x, c->y, c->z); + } else if (!strcmp(name, "sourcenode")) { + if (!dynamics->in_collide_message()) { + PyErr_SetString( + PyExc_AttributeError, + "collide value 'sourcenode' is only valid while processing " + "collide messages"); + return nullptr; + } + Node* n = dynamics->GetActiveCollideSrcNode(); + if (n) { + return n->NewPyRef(); + } else { + Py_RETURN_NONE; + } + } else if (!strcmp(name, "opposingnode")) { + if (!dynamics->in_collide_message()) { + PyErr_SetString( + PyExc_AttributeError, + "collide value 'opposingnode' is only valid while processing " + "collide messages"); + return nullptr; + } + Node* n = dynamics->GetActiveCollideDstNode(); + if (n) { + return n->NewPyRef(); + } else { + Py_RETURN_NONE; + } + } else if (!strcmp(name, "opposingbody")) { + return Py_BuildValue("i", dynamics->GetCollideMessageReverseOrder() + ? c->body_id_2 + : c->body_id_1); + } else { + PyErr_SetString( + PyExc_AttributeError, + (std::string("\"") + name + "\" is not a valid collide value name") + .c_str()); + return nullptr; + } + BA_PYTHON_CATCH; +} + +auto PyGetCollisionInfo(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_collision_info"); + HostActivity* host_activity = Context::current().GetHostActivity(); + if (!host_activity) { + throw Exception(PyExcType::kContext); + } + Dynamics* dynamics = host_activity->scene()->dynamics(); + assert(dynamics); + PyObject* obj = nullptr; + + // Take arg list as individual items or possibly a single tuple + Py_ssize_t argc = PyTuple_GET_SIZE(args); + if (argc > 1) { + obj = args; + } else if (argc == 1) { + obj = PyTuple_GET_ITEM(args, 0); + } + Collision* c = dynamics->active_collision(); + if (!c) { + PyErr_SetString(PyExc_RuntimeError, + "This must be called from a collision callback."); + return nullptr; + } + if (PyUnicode_Check(obj)) { + return DoGetCollideValue(dynamics, c, PyUnicode_AsUTF8(obj)); + } else if (PyTuple_Check(obj)) { + Py_ssize_t size = PyTuple_GET_SIZE(obj); + PyObject* return_tuple = PyTuple_New(size); + for (Py_ssize_t i = 0; i < size; i++) { + PyObject* o = PyTuple_GET_ITEM(obj, i); + if (PyUnicode_Check(o)) { + PyObject* val_obj = DoGetCollideValue(dynamics, c, PyUnicode_AsUTF8(o)); + if (val_obj) { + PyTuple_SetItem(return_tuple, i, val_obj); + } else { + Py_DECREF(return_tuple); + return nullptr; + } + } else { + Py_DECREF(return_tuple); + PyErr_SetString(PyExc_TypeError, "Expected a string as tuple member."); + return nullptr; + } + } + return return_tuple; + } else { + PyErr_SetString(PyExc_TypeError, "Expected a string or tuple."); + return nullptr; + } + BA_PYTHON_CATCH; +} + +auto PyCameraShake(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("camera_shake"); + assert(InGameThread()); + float intensity = 1.0f; + static const char* kwlist[] = {"intensity", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|f", + const_cast(kwlist), &intensity)) { + return nullptr; + } + g_graphics->LocalCameraShake(intensity); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyPlaySound(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("play_sound"); + + assert(InGameThread()); + PyObject* sound_obj; + float volume = 1.0f; + int host_only = 0; + PyObject* pos_obj = Py_None; + static const char* kwlist[] = {"sound", "volume", "position", "host_only", + nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|fOp", + const_cast(kwlist), &sound_obj, + &volume, &pos_obj, &host_only)) { + return nullptr; + } + + Sound* sound = Python::GetPySound(sound_obj); + + // Can play sounds in a host scene context. + if (Scene* scene = Context::current().GetMutableScene()) { + if (sound->scene() != scene) { + throw Exception("Sound was not loaded in this context.", + PyExcType::kContext); + } + if (pos_obj != Py_None) { + std::vector vals = Python::GetPyFloats(pos_obj); + if (vals.size() != 3) { + throw Exception("Expected 3 floats for pos (got " + + std::to_string(vals.size()) + ")", + PyExcType::kValue); + } + scene->PlaySoundAtPosition(sound, volume, vals[0], vals[1], vals[2], + static_cast(host_only)); + } else { + scene->PlaySound(sound, volume, static_cast(host_only)); + } + } else { + throw Exception("Can't play sounds in this context.", PyExcType::kContext); + } + + Py_RETURN_NONE; + + BA_PYTHON_CATCH; +} + +auto PyEmitFx(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("emit_fx"); + static const char* kwlist[] = {"position", "velocity", "count", + "scale", "spread", "chunk_type", + "emit_type", "tendril_type", nullptr}; + PyObject* pos_obj = Py_None; + PyObject* vel_obj = Py_None; + int count = 10; + float scale = 1.0f; + float spread = 1.0f; + const char* chunk_type_str = "rock"; + const char* emit_type_str = "chunks"; + const char* tendril_type_str = "smoke"; + assert(InGameThread()); + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "O|Oiffsss", const_cast(kwlist), &pos_obj, + &vel_obj, &count, &scale, &spread, &chunk_type_str, &emit_type_str, + &tendril_type_str)) { + return nullptr; + } + float x, y, z; + assert(pos_obj); + { + std::vector vals = Python::GetPyFloats(pos_obj); + if (vals.size() != 3) { + throw Exception("Expected 3 floats for position.", PyExcType::kValue); + } + x = vals[0]; + y = vals[1]; + z = vals[2]; + } + float vx = 0.0f; + float vy = 0.0f; + float vz = 0.0f; + if (vel_obj != Py_None) { + std::vector vals = Python::GetPyFloats(vel_obj); + if (vals.size() != 3) { + throw Exception("Expected 3 floats for velocity.", PyExcType::kValue); + } + vx = vals[0]; + vy = vals[1]; + vz = vals[2]; + } + BGDynamicsChunkType chunk_type; + if (!strcmp(chunk_type_str, "rock")) { + chunk_type = BGDynamicsChunkType::kRock; + } else if (!strcmp(chunk_type_str, "ice")) { + chunk_type = BGDynamicsChunkType::kIce; + } else if (!strcmp(chunk_type_str, "slime")) { + chunk_type = BGDynamicsChunkType::kSlime; + } else if (!strcmp(chunk_type_str, "metal")) { + chunk_type = BGDynamicsChunkType::kMetal; + } else if (!strcmp(chunk_type_str, "spark")) { + chunk_type = BGDynamicsChunkType::kSpark; + } else if (!strcmp(chunk_type_str, "splinter")) { + chunk_type = BGDynamicsChunkType::kSplinter; + } else if (!strcmp(chunk_type_str, "sweat")) { + chunk_type = BGDynamicsChunkType::kSweat; + } else { + throw Exception( + "Invalid chunk type: '" + std::string(chunk_type_str) + "'.", + PyExcType::kValue); + } + BGDynamicsTendrilType tendril_type; + if (!strcmp(tendril_type_str, "smoke")) { + tendril_type = BGDynamicsTendrilType::kSmoke; + } else if (!strcmp(tendril_type_str, "thin_smoke")) { + tendril_type = BGDynamicsTendrilType::kThinSmoke; + } else if (!strcmp(tendril_type_str, "ice")) { + tendril_type = BGDynamicsTendrilType::kIce; + } else { + throw Exception( + "Invalid tendril type: '" + std::string(tendril_type_str) + "'.", + PyExcType::kValue); + } + BGDynamicsEmitType emit_type; + if (!strcmp(emit_type_str, "chunks")) { + emit_type = BGDynamicsEmitType::kChunks; + } else if (!strcmp(emit_type_str, "stickers")) { + emit_type = BGDynamicsEmitType::kStickers; + } else if (!strcmp(emit_type_str, "tendrils")) { + emit_type = BGDynamicsEmitType::kTendrils; + } else if (!strcmp(emit_type_str, "distortion")) { + emit_type = BGDynamicsEmitType::kDistortion; + } else if (!strcmp(emit_type_str, "flag_stand")) { + emit_type = BGDynamicsEmitType::kFlagStand; + } else { + throw Exception("Invalid emit type: '" + std::string(emit_type_str) + "'.", + PyExcType::kValue); + } + if (Scene* scene = Context::current().GetMutableScene()) { + BGDynamicsEmission e; + e.emit_type = emit_type; + e.position = Vector3f(x, y, z); + e.velocity = Vector3f(vx, vy, vz); + e.count = count; + e.scale = scale; + e.spread = spread; + e.chunk_type = chunk_type; + e.tendril_type = tendril_type; + if (GameStream* output_stream = scene->GetGameStream()) { + output_stream->EmitBGDynamics(e); + } +#if !BA_HEADLESS_BUILD + g_bg_dynamics->Emit(e); +#endif // !BA_HEADLESS_BUILD + } else { + throw Exception("Can't emit bg dynamics in this context.", + PyExcType::kContext); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PySetMapBounds(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_map_bounds"); + HostActivity* host_activity = Context::current().GetHostActivity(); + if (!host_activity) { + throw Exception(PyExcType::kContext); + } + float xmin, ymin, zmin, xmax, ymax, zmax; + assert(InGameThread()); + if (!PyArg_ParseTuple(args, "(ffffff)", &xmin, &ymin, &zmin, &xmax, &ymax, + &zmax)) { + return nullptr; + } + host_activity->scene()->SetMapBounds(xmin, ymin, zmin, xmax, ymax, zmax); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetForegroundHostActivity(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_foreground_host_activity"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + + // Note: we return None if not in the game thread. + HostActivity* h = InGameThread() + ? g_game->GetForegroundContext().GetHostActivity() + : nullptr; + if (h != nullptr) { + PyObject* obj = h->GetPyActivity(); + Py_INCREF(obj); + return obj; + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetGameRoster(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_game_roster"); + BA_PRECONDITION(InGameThread()); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + PythonRef py_client_list(PyList_New(0), PythonRef::kSteal); + cJSON* party = g_game->game_roster(); + assert(party); + int len = cJSON_GetArraySize(party); + for (int i = 0; i < len; i++) { + cJSON* client = cJSON_GetArrayItem(party, i); + assert(client); + cJSON* spec = cJSON_GetObjectItem(client, "spec"); + cJSON* players = cJSON_GetObjectItem(client, "p"); + PythonRef py_player_list(PyList_New(0), PythonRef::kSteal); + if (players != nullptr) { + int plen = cJSON_GetArraySize(players); + for (int j = 0; j < plen; ++j) { + cJSON* player = cJSON_GetArrayItem(players, j); + if (player != nullptr) { + cJSON* name = cJSON_GetObjectItem(player, "n"); + cJSON* py_name_full = cJSON_GetObjectItem(player, "nf"); + cJSON* id_obj = cJSON_GetObjectItem(player, "i"); + int id_val = id_obj ? id_obj->valueint : -1; + if (name != nullptr && name->valuestring != nullptr + && py_name_full != nullptr && py_name_full->valuestring != nullptr + && id_val != -1) { + PythonRef py_player( + Py_BuildValue( + "{sssssi}", "name", + Utils::GetValidUTF8(name->valuestring, "ggr1").c_str(), + "name_full", + Utils::GetValidUTF8(py_name_full->valuestring, "ggr2") + .c_str(), + "id", id_val), + PythonRef::kSteal); + // This increments ref. + PyList_Append(py_player_list.get(), py_player.get()); + } + } + } + } + + // If there's a client_id with this data, include it; otherwise pass None. + cJSON* client_id = cJSON_GetObjectItem(client, "i"); + int clientid{}; + PythonRef client_id_ref; + if (client_id != nullptr) { + clientid = client_id->valueint; + client_id_ref.Steal(PyLong_FromLong(clientid)); + } else { + client_id_ref.Acquire(Py_None); + } + + // Let's also include a public account-id if we have one. + std::string account_id; + if (clientid == -1) { + account_id = AppInternalGetPublicAccountID(); + } else { + auto client2 = g_game->connections_to_clients().find(clientid); + if (client2 != g_game->connections_to_clients().end()) { + account_id = client2->second->peer_public_account_id(); + } + } + PythonRef account_id_ref; + if (account_id.empty()) { + account_id_ref.Acquire(Py_None); + } else { + account_id_ref.Steal(PyUnicode_FromString(account_id.c_str())); + } + + // Py_BuildValue steals a ref; gotta increment ourself (edit: NO IT DOESNT) + // Py_INCREF(py_player_list.get()); + PythonRef py_client( + Py_BuildValue( + "{sssssOsOsO}", "display_string", + (spec && spec->valuestring) + ? PlayerSpec(spec->valuestring).GetDisplayString().c_str() + : "", + "spec_string", (spec && spec->valuestring) ? spec->valuestring : "", + "players", py_player_list.get(), "client_id", client_id_ref.get(), + "account_id", account_id_ref.get()), + PythonRef::kSteal); + PyList_Append(py_client_list.get(), + py_client.get()); // this increments ref + } + return py_client_list.NewRef(); + BA_PYTHON_CATCH; +} + +auto PyGetScoresToBeat(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_scores_to_beat"); + const char* level; + const char* config; + PyObject* callback_obj = Py_None; + static const char* kwlist[] = {"level", "config", "callback", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "ssO", + const_cast(kwlist), &level, &config, + &callback_obj)) { + return nullptr; + } + + // Allocate a Call object for this and pass its pointer to the main thread; + // we'll ref/de-ref it when it comes back. + auto* call = Object::NewDeferred(callback_obj); + g_app->PushGetScoresToBeatCall(level, config, call); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PySetDebugSpeedExponent(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_debug_speed_exponent"); + int speed; + if (!PyArg_ParseTuple(args, "i", &speed)) { + return nullptr; + } + HostActivity* host_activity = Context::current().GetHostActivity(); + if (!host_activity) { + throw Exception(PyExcType::kContext); + } +#if BA_DEBUG_BUILD + g_game->SetDebugSpeedExponent(speed); +#else + throw Exception("This call only functions in the debug build."); +#endif + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetReplaySpeedExponent(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_replay_speed_exponent"); + assert(g_game); + return PyLong_FromLong(g_game->replay_speed_exponent()); + BA_PYTHON_CATCH; +} + +auto PySetReplaySpeedExponent(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_replay_speed_exponent"); + int speed; + if (!PyArg_ParseTuple(args, "i", &speed)) return nullptr; + assert(g_game); + g_game->SetReplaySpeedExponent(speed); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyResetGameActivityTracking(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("reset_game_activity_tracking"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + if (g_game) { + g_game->ResetActivityTracking(); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyResetRandomPlayerNames(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("reset_random_player_names"); + InputDevice::ResetRandomNames(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetRandomNames(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_random_names"); + PyObject* list = PyList_New(0); + const std::list& random_name_list = Utils::GetRandomNameList(); + for (const auto& i : random_name_list) { + assert(Utils::IsValidUTF8(i)); + PyObject* obj = PyUnicode_FromString(i.c_str()); + PyList_Append(list, obj); + Py_DECREF(obj); + } + return list; + BA_PYTHON_CATCH; +} + +PyMethodDef PythonMethodsGameplay::methods_def[] = { + {"get_random_names", PyGetRandomNames, METH_VARARGS, + "get_random_names() -> list\n" + "\n" + "(internal)\n" + "\n" + "Returns the random names used by the game."}, + + {"reset_random_player_names", (PyCFunction)PyResetRandomPlayerNames, + METH_VARARGS | METH_KEYWORDS, + "reset_random_player_names() -> None\n" + "\n" + "(internal)"}, + + {"reset_game_activity_tracking", (PyCFunction)PyResetGameActivityTracking, + METH_VARARGS | METH_KEYWORDS, + "reset_game_activity_tracking() -> None\n" + "\n" + "(internal)"}, + + {"set_replay_speed_exponent", PySetReplaySpeedExponent, METH_VARARGS, + "set_replay_speed_exponent(speed: int) -> None\n" + "\n" + "(internal)\n" + "\n" + "Set replay speed. Actual displayed speed is pow(2,speed)."}, + + {"get_replay_speed_exponent", PyGetReplaySpeedExponent, METH_VARARGS, + "get_replay_speed_exponent() -> int\n" + "\n" + "(internal)\n" + "\n" + "Returns current replay speed value. Actual displayed speed is " + "pow(2,speed)."}, + + {"set_debug_speed_exponent", PySetDebugSpeedExponent, METH_VARARGS, + "set_debug_speed_exponent(speed: int) -> None\n" + "\n" + "(internal)\n" + "\n" + "Sets the debug speed scale for the game. Actual speed is pow(2,speed)."}, + + {"get_scores_to_beat", (PyCFunction)PyGetScoresToBeat, + METH_VARARGS | METH_KEYWORDS, + "get_scores_to_beat(level: str, config: str, callback: Callable) -> None\n" + "\n" + "(internal)"}, + + {"get_game_roster", (PyCFunction)PyGetGameRoster, + METH_VARARGS | METH_KEYWORDS, + "get_game_roster() -> List[Dict[str, Any]]\n" + "\n" + "(internal)"}, + + {"get_foreground_host_activity", (PyCFunction)PyGetForegroundHostActivity, + METH_VARARGS | METH_KEYWORDS, + "get_foreground_host_activity() -> Optional[ba.Activity]\n" + "\n" + "(internal)\n" + "\n" + "Returns the ba.Activity currently in the foreground, or None if there\n" + "is none.\n"}, + + {"set_map_bounds", (PyCFunction)PySetMapBounds, + METH_VARARGS | METH_KEYWORDS, + "set_map_bounds(bounds: Tuple[float, float, float, float, float, float])\n" + " -> None\n" + "\n" + "(internal)\n" + "\n" + "Set map bounds. Generally nodes that go outside of this box are " + "killed."}, + + {"emitfx", (PyCFunction)PyEmitFx, METH_VARARGS | METH_KEYWORDS, + "emitfx(position: Sequence[float],\n" + " velocity: Optional[Sequence[float]] = None,\n" + " count: int = 10, scale: float = 1.0, spread: float = 1.0,\n" + " chunk_type: str = 'rock', emit_type: str ='chunks',\n" + " tendril_type: str = 'smoke') -> None\n" + "\n" + "Emit particles, smoke, etc. into the fx sim layer.\n" + "\n" + "Category: Gameplay Functions\n" + "\n" + "The fx sim layer is a secondary dynamics simulation that runs in\n" + "the background and just looks pretty; it does not affect gameplay.\n" + "Note that the actual amount emitted may vary depending on graphics\n" + "settings, exiting element counts, or other factors."}, + + {"playsound", (PyCFunction)PyPlaySound, METH_VARARGS | METH_KEYWORDS, + "playsound(sound: Sound, volume: float = 1.0,\n" + " position: Sequence[float] = None, host_only: bool = False) -> None\n" + "\n" + "Play a ba.Sound a single time.\n" + "\n" + "Category: Gameplay Functions\n" + "\n" + "If position is not provided, the sound will be at a constant volume\n" + "everywhere. Position should be a float tuple of size 3."}, + + {"camerashake", (PyCFunction)PyCameraShake, METH_VARARGS | METH_KEYWORDS, + "camerashake(intensity: float = 1.0) -> None\n" + "\n" + "Shake the camera.\n" + "\n" + "Category: Gameplay Functions\n" + "\n" + "Note that some cameras and/or platforms (such as VR) may not display\n" + "camera-shake, so do not rely on this always being visible to the\n" + "player as a gameplay cue."}, + + {"get_collision_info", PyGetCollisionInfo, METH_VARARGS, + "get_collision_info(*args: Any) -> Any\n" + "\n" + "Return collision related values\n" + "\n" + "Category: Gameplay Functions\n" + "\n" + "Returns a single collision value or tuple of values such as location,\n" + "depth, nodes involved, etc. Only call this in the handler of a\n" + "collision-triggered callback or message"}, + + {"getnodes", PyGetNodes, METH_VARARGS, + "getnodes() -> list\n" + "\n" + "Return all nodes in the current ba.Context." + "\n" + "Category: Gameplay Functions"}, + + {"printnodes", PyPrintNodes, METH_VARARGS, + "printnodes() -> None\n" + "\n" + "Print various info about existing nodes; useful for debugging.\n" + "\n" + "Category: Gameplay Functions"}, + + {"newnode", (PyCFunction)PyNewNode, METH_VARARGS | METH_KEYWORDS, + "newnode(type: str, owner: ba.Node = None,\n" + "attrs: dict = None, name: str = None, delegate: Any = None)\n" + " -> Node\n" + "\n" + "Add a node of the given type to the game.\n" + "\n" + "Category: Gameplay Functions\n" + "\n" + "If a dict is provided for 'attributes', the node's initial attributes\n" + "will be set based on them.\n" + "\n" + "'name', if provided, will be stored with the node purely for debugging\n" + "purposes. If no name is provided, an automatic one will be generated\n" + "such as 'terrain@foo.py:30'.\n" + "\n" + "If 'delegate' is provided, Python messages sent to the node will go to\n" + "that object's handlemessage() method. Note that the delegate is stored\n" + "as a weak-ref, so the node itself will not keep the object alive.\n" + "\n" + "if 'owner' is provided, the node will be automatically killed when that\n" + "object dies. 'owner' can be another node or a ba.Actor"}, + + {nullptr, nullptr, 0, nullptr}}; + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/ballistica/python/methods/python_methods_gameplay.h b/src/ballistica/python/methods/python_methods_gameplay.h new file mode 100644 index 00000000..95b0e8c1 --- /dev/null +++ b/src/ballistica/python/methods/python_methods_gameplay.h @@ -0,0 +1,17 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_GAMEPLAY_H_ +#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_GAMEPLAY_H_ + +#include "ballistica/python/python_sys.h" + +namespace ballistica { + +/// Gameplay related individual python methods for our module. +class PythonMethodsGameplay { + public: + static PyMethodDef methods_def[]; +}; + +} // namespace ballistica +#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_GAMEPLAY_H_ diff --git a/src/ballistica/python/methods/python_methods_graphics.cc b/src/ballistica/python/methods/python_methods_graphics.cc new file mode 100644 index 00000000..92bbf923 --- /dev/null +++ b/src/ballistica/python/methods/python_methods_graphics.cc @@ -0,0 +1,315 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/methods/python_methods_graphics.h" + +#include + +#include "ballistica/game/game.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/text/text_graphics.h" +#include "ballistica/platform/platform.h" +#include "ballistica/python/python.h" +#include "ballistica/python/python_context_call_runnable.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" + +auto PyCharStr(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("charstr"); + PyObject* name_obj; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &name_obj)) { + return nullptr; + } + assert(g_game); + auto id(Python::GetPyEnum_SpecialChar(name_obj)); + assert(Utils::IsValidUTF8(g_game->CharStr(id))); + return PyUnicode_FromString(g_game->CharStr(id).c_str()); + BA_PYTHON_CATCH; +} + +auto PySafeColor(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("safecolor"); + PyObject* color_obj; + float red, green, blue; + float target_intensity = 0.6f; + static const char* kwlist[] = {"color", "target_intensity", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|f", + const_cast(kwlist), &color_obj, + &target_intensity)) { + return nullptr; + } + if (!PySequence_Check(color_obj)) { + throw Exception("Expected a sequence.", PyExcType::kType); + } + int len = static_cast(PySequence_Length(color_obj)); + if (len != 3 && len != 4) { + throw Exception("Expected a 3 or 4 length sequence; got " + + Python::ObjToString(color_obj) + ".", + PyExcType::kValue); + } + PythonRef red_obj(PySequence_GetItem(color_obj, 0), PythonRef::kSteal); + PythonRef green_obj(PySequence_GetItem(color_obj, 1), PythonRef::kSteal); + PythonRef blue_obj(PySequence_GetItem(color_obj, 2), PythonRef::kSteal); + red = Python::GetPyFloat(red_obj.get()); + green = Python::GetPyFloat(green_obj.get()); + blue = Python::GetPyFloat(blue_obj.get()); + Graphics::GetSafeColor(&red, &green, &blue, target_intensity); + if (len == 3) { + return Py_BuildValue("(fff)", red, green, blue); + } else { + PythonRef alpha_obj(PySequence_GetItem(color_obj, 3), PythonRef::kSteal); + float alpha = Python::GetPyFloat(alpha_obj.get()); + return Py_BuildValue("(ffff)", red, green, blue, alpha); + } + BA_PYTHON_CATCH; +} + +auto PyGetMaxGraphicsQuality(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_max_graphics_quality"); + if (g_graphics && g_graphics->has_supports_high_quality_graphics_value() + && g_graphics->supports_high_quality_graphics()) { + return Py_BuildValue("s", "High"); + } else { + return Py_BuildValue("s", "Medium"); + } + BA_PYTHON_CATCH; +} + +auto PyEvaluateLstr(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("evaluate_lstr"); + const char* value; + static const char* kwlist[] = {"value", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &value)) { + return nullptr; + } + return PyUnicode_FromString( + g_game->CompileResourceString(value, "evaluate_lstr").c_str()); + BA_PYTHON_CATCH; +} + +auto PyGetStringHeight(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_string_height"); + std::string s; + int suppress_warning = 0; + PyObject* s_obj; + static const char* kwlist[] = {"string", "suppress_warning", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|i", + const_cast(kwlist), &s_obj, + &suppress_warning)) { + return nullptr; + } + if (!suppress_warning) { + BA_LOG_PYTHON_TRACE( + "get_string_height() use is heavily discouraged as it reduces " + "language-independence; pass suppress_warning=True if you must use " + "it."); + } + s = Python::GetPyString(s_obj); +#if BA_DEBUG_BUILD + if (g_game->CompileResourceString(s, "get_string_height test") != s) { + BA_LOG_PYTHON_TRACE( + "resource-string passed to get_string_height; this should be avoided"); + } +#endif + assert(g_graphics); + return Py_BuildValue("f", g_text_graphics->GetStringHeight(s)); + BA_PYTHON_CATCH; +} + +auto PyGetStringWidth(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_string_width"); + std::string s; + PyObject* s_obj; + int suppress_warning = 0; + static const char* kwlist[] = {"string", "suppress_warning", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|i", + const_cast(kwlist), &s_obj, + &suppress_warning)) { + return nullptr; + } + if (!suppress_warning) { + BA_LOG_PYTHON_TRACE( + "get_string_width() use is heavily discouraged as it reduces " + "language-independence; pass suppress_warning=True if you must use " + "it."); + } + s = Python::GetPyString(s_obj); +#if BA_DEBUG_BUILD + if (g_game->CompileResourceString(s, "get_string_width debug test") != s) { + BA_LOG_PYTHON_TRACE( + "resource-string passed to get_string_width; this should be avoided"); + } +#endif + assert(g_graphics); + return Py_BuildValue("f", g_text_graphics->GetStringWidth(s)); + BA_PYTHON_CATCH; +} + +auto PyHaveChars(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("have_chars"); + std::string text; + PyObject* text_obj; + static const char* kwlist[] = {"text", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &text_obj)) { + return nullptr; + } + text = Python::GetPyString(text_obj); + if (TextGraphics::HaveChars(text)) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +auto PyAddCleanFrameCallback(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("add_clean_frame_callback"); + PyObject* call_obj; + static const char* kwlist[] = {"call", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &call_obj)) { + return nullptr; + } + g_python->AddCleanFrameCommand(Object::New(call_obj)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyHasGammaControl(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("has_gamma_control"); + // phasing this out; our old non-sdl2 mac has gamma controls but nothing newer + // does... +#if BA_OSTYPE_MACOS && !BA_SDL2_BUILD + Py_RETURN_TRUE; +#else + Py_RETURN_FALSE; +#endif + BA_PYTHON_CATCH; +} + +auto PyGetDisplayResolution(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_display_resolution"); + int x = 0; + int y = 0; + bool have_res = g_platform->GetDisplayResolution(&x, &y); + if (have_res) { + return Py_BuildValue("(ii)", x, y); + } else { + Py_RETURN_NONE; + } + BA_PYTHON_CATCH; +} + +PyMethodDef PythonMethodsGraphics::methods_def[] = { + {"get_display_resolution", PyGetDisplayResolution, METH_VARARGS, + "get_display_resolution() -> Optional[Tuple[int, int]]\n" + "\n" + "(internal)\n" + "\n" + "Return the currently selected display resolution for fullscreen\n" + "display. Returns None if resolutions cannot be directly set."}, + + {"has_gamma_control", PyHasGammaControl, METH_VARARGS, + "has_gamma_control() -> bool\n" + "\n" + "(internal)\n" + "\n" + "Returns whether the system can adjust overall screen gamma)"}, + + {"add_clean_frame_callback", (PyCFunction)PyAddCleanFrameCallback, + METH_VARARGS | METH_KEYWORDS, + "add_clean_frame_callback(call: Callable) -> None\n" + "\n" + "(internal)\n" + "\n" + "Provide an object to be called once the next non-progress-bar-frame has\n" + "been rendered. Useful for queueing things to load in the background\n" + "without elongating any current progress-bar-load."}, + + {"have_chars", (PyCFunction)PyHaveChars, METH_VARARGS | METH_KEYWORDS, + "have_chars(text: str) -> bool\n" + "\n" + "(internal)"}, + + {"get_string_width", (PyCFunction)PyGetStringWidth, + METH_VARARGS | METH_KEYWORDS, + "get_string_width(string: str, suppress_warning: bool = False) -> float\n" + "\n" + "(internal)\n" + "\n" + "Given a string, returns its width using the standard small app\n" + "font."}, + + {"get_string_height", (PyCFunction)PyGetStringHeight, + METH_VARARGS | METH_KEYWORDS, + "get_string_height(string: str, suppress_warning: bool = False) -> float\n" + "\n" + "(internal)\n" + "\n" + "Given a string, returns its height using the standard small app\n" + "font."}, + + {"evaluate_lstr", (PyCFunction)PyEvaluateLstr, METH_VARARGS | METH_KEYWORDS, + "evaluate_lstr(value: str) -> str\n" + "\n" + "(internal)"}, + + {"get_max_graphics_quality", PyGetMaxGraphicsQuality, METH_VARARGS, + "get_max_graphics_quality() -> str\n" + "\n" + "(internal)\n" + "\n" + "Return the max graphics-quality supported on the current hardware."}, + + {"safecolor", (PyCFunction)PySafeColor, METH_VARARGS | METH_KEYWORDS, + "safecolor(color: Sequence[float], target_intensity: float = 0.6)\n" + " -> Tuple[float, ...]\n" + "\n" + "Given a color tuple, return a color safe to display as text.\n" + "\n" + "Category: General Utility Functions\n" + "\n" + "Accepts tuples of length 3 or 4. This will slightly brighten very\n" + "dark colors, etc."}, + + {"charstr", (PyCFunction)PyCharStr, METH_VARARGS | METH_KEYWORDS, + "charstr(char_id: ba.SpecialChar) -> str\n" + "\n" + "Get a unicode string representing a special character.\n" + "\n" + "Category: General Utility Functions\n" + "\n" + "Note that these utilize the private-use block of unicode characters\n" + "(U+E000-U+F8FF) and are specific to the game; exporting or rendering\n" + "them elsewhere will be meaningless.\n" + "\n" + "see ba.SpecialChar for the list of available characters."}, + + {nullptr, nullptr, 0, nullptr}}; + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/ballistica/python/methods/python_methods_graphics.h b/src/ballistica/python/methods/python_methods_graphics.h new file mode 100644 index 00000000..022dcfc0 --- /dev/null +++ b/src/ballistica/python/methods/python_methods_graphics.h @@ -0,0 +1,17 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_GRAPHICS_H_ +#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_GRAPHICS_H_ + +#include "ballistica/python/python_sys.h" + +namespace ballistica { + +/// Graphics related individual python methods for our module. +class PythonMethodsGraphics { + public: + static PyMethodDef methods_def[]; +}; + +} // namespace ballistica +#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_GRAPHICS_H_ diff --git a/src/ballistica/python/methods/python_methods_input.cc b/src/ballistica/python/methods/python_methods_input.cc new file mode 100644 index 00000000..e98cc541 --- /dev/null +++ b/src/ballistica/python/methods/python_methods_input.cc @@ -0,0 +1,394 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/methods/python_methods_input.h" + +#include +#include + +#include "ballistica/app/app_globals.h" +#include "ballistica/game/game.h" +#include "ballistica/input/device/input_device.h" +#include "ballistica/input/device/touch_input.h" +#include "ballistica/input/input.h" +#include "ballistica/platform/platform.h" +#include "ballistica/python/python.h" +#include "ballistica/ui/ui.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" + +auto PyGetConfigurableGamePads(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_configurable_game_pads"); + std::vector gamepads = g_input->GetConfigurableGamePads(); + PyObject* list = PyList_New(0); + for (auto&& i : gamepads) { + PyObject* obj = i->NewPyRef(); + PyList_Append(list, obj); + Py_DECREF(obj); + } + return list; + BA_PYTHON_CATCH; +} + +auto PyHaveTouchScreenInput(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("have_touch_screen_input"); + if (g_app_globals->touch_input) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +auto PyStartListeningForWiiRemotes(PyObject* self, PyObject* args) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("start_listening_for_wii_remotes"); + g_platform->StartListeningForWiiRemotes(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyStopListeningForWiiRemotes(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("stop_listening_for_wii_remotes"); + g_platform->StopListeningForWiiRemotes(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PySetDeviceAccount(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_device_account"); + std::string name; + PyObject* name_obj; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &name_obj)) { + return nullptr; + } + name = Python::GetPyString(name_obj); + AccountType account_type; + + // on headless builds we keep these distinct from regular + // device accounts (so we get a 'ServerXXX' name, etc) +#if BA_HEADLESS_BUILD + account_type = AccountType::kServer; +#else + account_type = AccountType::kDevice; +#endif + g_game->PushSetAccountCall(account_type, AccountState::kSignedIn, name, + g_platform->GetDeviceAccountID()); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetDeviceLoginID(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_device_login_id"); + assert(Utils::IsValidUTF8(g_platform->GetDeviceAccountID())); + return PyUnicode_FromString(g_platform->GetDeviceAccountID().c_str()); + BA_PYTHON_CATCH; +} + +auto PySetTouchscreenEditing(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_touchscreen_editing"); + int editing; + if (!PyArg_ParseTuple(args, "p", &editing)) { + return nullptr; + } + if (g_app_globals->touch_input) { + g_app_globals->touch_input->set_editing(static_cast(editing)); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyCaptureGamePadInput(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("capture_gamepad_input"); + assert(InGameThread()); + assert(g_python); + PyObject* obj; + if (!PyArg_ParseTuple(args, "O", &obj)) { + return nullptr; + } + g_python->CaptureGamePadInput(obj); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyReleaseGamePadInput(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("release_gamepad_input"); + assert(InGameThread()); + assert(g_python); + g_python->ReleaseGamePadInput(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyCaptureKeyboardInput(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("capture_keyboard_input"); + assert(InGameThread()); + if (!g_python) { + return nullptr; + } + PyObject* obj; + if (!PyArg_ParseTuple(args, "O", &obj)) { + return nullptr; + } + g_python->CaptureKeyboardInput(obj); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyReleaseKeyboardInput(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("release_keyboard_input"); + assert(InGameThread()); + if (!g_python) { + return nullptr; + } + g_python->ReleaseKeyboardInput(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyLockAllInput(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("lock_all_input"); + assert(InGameThread()); + assert(g_input); + g_input->LockAllInput(false, Python::GetPythonFileLocation()); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyUnlockAllInput(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("unlock_all_input"); + assert(InGameThread()); + assert(g_input); + g_input->UnlockAllInput(false, Python::GetPythonFileLocation()); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetUIInputDevice(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_ui_input_device"); + assert(InGameThread()); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + InputDevice* d = g_ui->GetUIInputDevice(); + if (d) { + return d->NewPyRef(); + } else { + Py_RETURN_NONE; + } + BA_PYTHON_CATCH; +} + +auto PySetUIInputDevice(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_ui_input_device"); + assert(InGameThread()); + static const char* kwlist[] = {"input", nullptr}; + PyObject* input_device_obj = Py_None; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "O", const_cast(kwlist), &input_device_obj)) { + return nullptr; + } + g_ui->SetUIInputDevice((input_device_obj == Py_None) + ? nullptr + : Python::GetPyInputDevice(input_device_obj)); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetInputDevice(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_input_device"); + assert(InGameThread()); + const char* name; + const char* unique_id; + int doraise = true; + static const char* kwlist[] = {"name", "unique_id", "doraise", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "ss|i", + const_cast(kwlist), &name, + &unique_id, &doraise)) { + return nullptr; + } + InputDevice* d = g_input->GetInputDevice(name, unique_id); + if (d) { + return d->NewPyRef(); + } else { + if (doraise) { + throw Exception(std::string("Input device not found: '") + name + " " + + unique_id + "'.", + PyExcType::kInputDeviceNotFound); + } else { + Py_RETURN_NONE; + } + } + BA_PYTHON_CATCH; +} + +auto PyGetLocalActiveInputDevicesCount(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_local_active_input_devices_count"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + BA_PRECONDITION(g_input); + return PyLong_FromLong(g_input->GetLocalActiveInputDeviceCount()); + BA_PYTHON_CATCH; +} + +PyMethodDef PythonMethodsInput::methods_def[] = { + {"get_local_active_input_devices_count", + (PyCFunction)PyGetLocalActiveInputDevicesCount, + METH_VARARGS | METH_KEYWORDS, + "get_local_active_input_devices_count() -> int\n" + "\n" + "(internal)"}, + + {"getinputdevice", (PyCFunction)PyGetInputDevice, + METH_VARARGS | METH_KEYWORDS, + "getinputdevice(name: str, unique_id: str, doraise: bool = True)\n" + " -> \n" + "\n" + "(internal)\n" + "\n" + "Given a type name and a unique identifier, returns an InputDevice.\n" + "Throws an Exception if the input-device is not found, or returns None\n" + "if 'doraise' is False.\n"}, + + {"set_ui_input_device", (PyCFunction)PySetUIInputDevice, + METH_VARARGS | METH_KEYWORDS, + "set_ui_input_device(input_device: Optional[ba.InputDevice]) -> None\n" + "\n" + "(internal)\n" + "\n" + "Sets the input-device that currently owns the user interface."}, + + {"get_ui_input_device", (PyCFunction)PyGetUIInputDevice, + METH_VARARGS | METH_KEYWORDS, + "get_ui_input_device() -> ba.InputDevice\n" + "\n" + "(internal)\n" + "\n" + "Returns the input-device that currently owns the user interface, or\n" + "None if there is none."}, + + {"unlock_all_input", PyUnlockAllInput, METH_VARARGS, + "unlock_all_input() -> None\n" + "\n" + "(internal)\n" + "\n" + "Resumes normal keyboard, mouse, and gamepad event processing."}, + + {"lock_all_input", PyLockAllInput, METH_VARARGS, + "lock_all_input() -> None\n" + "\n" + "(internal)\n" + "\n" + "Prevents all keyboard, mouse, and gamepad events from being processed."}, + + {"release_keyboard_input", PyReleaseKeyboardInput, METH_VARARGS, + "release_keyboard_input() -> None\n" + "\n" + "(internal)\n" + "\n" + "Resumes normal keyboard event processing."}, + + {"capture_keyboard_input", PyCaptureKeyboardInput, METH_VARARGS, + "capture_keyboard_input(call: Callable[[dict], None]) -> None\n" + "\n" + "(internal)\n" + "\n" + "Add a callable to be called for subsequent keyboard-game-pad events.\n" + "The method is passed a dict containing info about the event."}, + + {"release_gamepad_input", PyReleaseGamePadInput, METH_VARARGS, + "release_gamepad_input() -> None\n" + "\n" + "(internal)\n" + "\n" + "Resumes normal gamepad event processing."}, + + {"capture_gamepad_input", PyCaptureGamePadInput, METH_VARARGS, + "capture_gamepad_input(call: Callable[[dict], None]) -> None\n" + "\n" + "(internal)\n" + "\n" + "Add a callable to be called for subsequent gamepad events.\n" + "The method is passed a dict containing info about the event."}, + + {"set_touchscreen_editing", PySetTouchscreenEditing, METH_VARARGS, + "set_touchscreen_editing(editing: bool) -> None\n" + "\n" + "(internal)"}, + + {"get_device_login_id", (PyCFunction)PyGetDeviceLoginID, + METH_VARARGS | METH_KEYWORDS, "internal"}, + + {"set_device_account", (PyCFunction)PySetDeviceAccount, + METH_VARARGS | METH_KEYWORDS, "internal"}, + + {"stop_listening_for_wii_remotes", PyStopListeningForWiiRemotes, + METH_VARARGS, + "stop_listening_for_wii_remotes() -> None\n" + "\n" + "(internal)\n" + "\n" + "Stop listening for connections from wii remotes."}, + + {"start_listening_for_wii_remotes", PyStartListeningForWiiRemotes, + METH_VARARGS, + "start_listening_for_wii_remotes() -> None\n" + "\n" + "(internal)\n" + "\n" + "Start listening for connections from wii remotes."}, + + {"have_touchscreen_input", PyHaveTouchScreenInput, METH_VARARGS, + "have_touchscreen_input() -> bool\n" + "\n" + "(internal)\n" + "\n" + "Returns whether or not a touch-screen input is present"}, + + {"get_configurable_game_pads", PyGetConfigurableGamePads, METH_VARARGS, + "get_configurable_game_pads() -> list\n" + "\n" + "(internal)\n" + "\n" + "Returns a list of the currently connected gamepads that can be\n" + "configured."}, + + {nullptr, nullptr, 0, nullptr}}; + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/ballistica/python/methods/python_methods_input.h b/src/ballistica/python/methods/python_methods_input.h new file mode 100644 index 00000000..7b2821cf --- /dev/null +++ b/src/ballistica/python/methods/python_methods_input.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_INPUT_H_ +#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_INPUT_H_ + +#include "ballistica/python/python_sys.h" + +namespace ballistica { + +// Input related individual python methods for our module. +class PythonMethodsInput { + public: + static PyMethodDef methods_def[]; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_INPUT_H_ diff --git a/src/ballistica/python/methods/python_methods_media.cc b/src/ballistica/python/methods/python_methods_media.cc new file mode 100644 index 00000000..bd775568 --- /dev/null +++ b/src/ballistica/python/methods/python_methods_media.cc @@ -0,0 +1,563 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/methods/python_methods_media.h" + +#include +#if 0 // Cpplint errs w/o this, CLion errs with it. Hard to please everybody. +#include +#endif + +#include "ballistica/game/host_activity.h" +#include "ballistica/game/session/host_session.h" +#include "ballistica/graphics/graphics_server.h" +#include "ballistica/media/component/collide_model.h" +#include "ballistica/media/component/data.h" +#include "ballistica/media/component/model.h" +#include "ballistica/media/component/sound.h" +#include "ballistica/media/component/texture.h" +#include "ballistica/python/python.h" +#include "ballistica/ui/ui.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" + +auto PyGetTexture(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("gettexture"); + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + return Context::current_target().GetTexture(name)->NewPyRef(); + BA_PYTHON_CATCH; +} + +auto PyGetPackageTexture(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("getpackagetexture"); + const char* name; + PyObject* package_obj; + static const char* kwlist[] = {"package", "name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "Os", + const_cast(kwlist), &package_obj, + &name)) { + return nullptr; + } + auto fullname = g_python->ValidatedPackageAssetName(package_obj, name); + return Context::current_target().GetTexture(fullname)->NewPyRef(); + BA_PYTHON_CATCH; +} + +auto PyGetSound(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("getsound"); + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + return Context::current_target().GetSound(name)->NewPyRef(); + BA_PYTHON_CATCH; +} + +auto PyGetPackageSound(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("getpackagesound"); + const char* name; + PyObject* package_obj; + static const char* kwlist[] = {"package", "name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "Os", + const_cast(kwlist), &package_obj, + &name)) { + return nullptr; + } + auto fullname = g_python->ValidatedPackageAssetName(package_obj, name); + return Context::current_target().GetSound(fullname)->NewPyRef(); + BA_PYTHON_CATCH; +} + +auto PyGetData(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("getdata"); + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + return Context::current_target().GetData(name)->NewPyRef(); + BA_PYTHON_CATCH; +} + +auto PyGetPackageData(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("getpackagedata"); + const char* name; + PyObject* package_obj; + static const char* kwlist[] = {"package", "name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "Os", + const_cast(kwlist), &package_obj, + &name)) { + return nullptr; + } + auto fullname = g_python->ValidatedPackageAssetName(package_obj, name); + return Context::current_target().GetData(fullname)->NewPyRef(); + BA_PYTHON_CATCH; +} + +auto PyGetModel(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("getmodel"); + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + return Context::current_target().GetModel(name)->NewPyRef(); + BA_PYTHON_CATCH; +} + +auto PyGetPackageModel(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("getpackagemodel"); + const char* name; + PyObject* package_obj; + static const char* kwlist[] = {"package", "name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "Os", + const_cast(kwlist), &package_obj, + &name)) { + return nullptr; + } + auto fullname = g_python->ValidatedPackageAssetName(package_obj, name); + return Context::current_target().GetTexture(fullname)->NewPyRef(); + BA_PYTHON_CATCH; +} + +auto PyGetCollideModel(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("getcollidemodel"); + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + return Context::current_target().GetCollideModel(name)->NewPyRef(); + BA_PYTHON_CATCH; +} + +auto PyGetPackageCollideModel(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("getpackagecollidemodel"); + const char* name; + PyObject* package_obj; + static const char* kwlist[] = {"package", "name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "Os", + const_cast(kwlist), &package_obj, + &name)) { + return nullptr; + } + auto fullname = g_python->ValidatedPackageAssetName(package_obj, name); + return Context::current_target().GetCollideModel(fullname)->NewPyRef(); + BA_PYTHON_CATCH; +} + +auto PyMusicPlayerStop(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("musicplayerstop"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + g_platform->MusicPlayerStop(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyMusicPlayerPlay(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("musicplayerplay"); + PyObject* files_obj; + static const char* kwlist[] = {"files", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &files_obj)) { + return nullptr; + } + g_platform->MusicPlayerPlay(files_obj); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyMusicPlayerSetVolume(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("musicplayersetvolume"); + float volume; + static const char* kwlist[] = {"volume", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "f", + const_cast(kwlist), &volume)) { + return nullptr; + } + g_platform->MusicPlayerSetVolume(volume); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyMusicPlayerShutdown(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("musicplayershutdown"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + g_platform->MusicPlayerShutdown(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyReloadMedia(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("reloadmedia"); + assert(g_graphics_server); + g_graphics_server->PushReloadMediaCall(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetQRCodeTexture(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("getqrcodetexture"); + const char* url; + static const char* kwlist[] = {"url", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &url)) { + return nullptr; + } + // FIXME - should add this to context; for now just hard-coded for UI though + if (Context::current().GetUIContext() != nullptr) { + // these textures aren't actually stored in the UI context; + // we just make sure we're here so we're not corrupting a game/session. + return Object::New(url)->NewPyRef(); + } else { + throw Exception("QR-Code textures can only be created in the UI context.", + PyExcType::kContext); + } + BA_PYTHON_CATCH; +} + +auto PyMacMusicAppInit(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("macmusicappinit"); + g_platform->MacMusicAppInit(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyMacMusicAppGetVolume(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("macmusicappgetvolume"); + return PyLong_FromLong(g_platform->MacMusicAppGetVolume()); + BA_PYTHON_CATCH; +} + +auto PyMacMusicAppSetVolume(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("macmusicappsetvolume"); + int volume; + static const char* kwlist[] = {"volume", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "i", + const_cast(kwlist), &volume)) { + return nullptr; + } + g_platform->MacMusicAppSetVolume(volume); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyMacMusicAppGetLibrarySource(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("macmusicappgetlibrarysource"); + g_platform->MacMusicAppGetLibrarySource(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyMacMusicAppStop(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("macmusicappstop"); + g_platform->MacMusicAppStop(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyMacMusicAppPlayPlaylist(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("macmusicappplayplaylist"); + std::string playlist; + PyObject* playlist_obj; + static const char* kwlist[] = {"playlist", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &playlist_obj)) { + return nullptr; + } + playlist = Python::GetPyString(playlist_obj); + if (g_platform->MacMusicAppPlayPlaylist(playlist)) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +auto PyMacMusicAppGetPlaylists(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("macmusicappgetplaylists"); + PyObject* py_list = PyList_New(0); + std::list playlists = g_platform->MacMusicAppGetPlaylists(); + for (auto&& i : playlists) { + PyObject* str_obj = PyUnicode_FromString(i.c_str()); + PyList_Append(py_list, str_obj); + Py_DECREF(str_obj); + } + return py_list; + BA_PYTHON_CATCH; +} + +auto PyIsOSPlayingMusic(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("isosplayingmusic"); + if (g_platform->IsOSPlayingMusic()) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +PyMethodDef PythonMethodsMedia::methods_def[] = { + {"is_os_playing_music", (PyCFunction)PyIsOSPlayingMusic, + METH_VARARGS | METH_KEYWORDS, + "is_os_playing_music() -> bool\n" + "\n" + "(internal)\n" + "\n" + "Tells whether the OS is currently playing music of some sort.\n" + "\n" + "(Used to determine whether the game should avoid playing its own)"}, + + {"mac_music_app_init", (PyCFunction)PyMacMusicAppInit, + METH_VARARGS | METH_KEYWORDS, + "mac_music_app_init() -> None\n" + "\n" + "(internal)"}, + + {"mac_music_app_get_volume", (PyCFunction)PyMacMusicAppGetVolume, + METH_VARARGS | METH_KEYWORDS, + "mac_music_app_get_volume() -> int\n" + "\n" + "(internal)"}, + + {"mac_music_app_set_volume", (PyCFunction)PyMacMusicAppSetVolume, + METH_VARARGS | METH_KEYWORDS, + "mac_music_app_set_volume(volume: int) -> None\n" + "\n" + "(internal)"}, + + {"mac_music_app_get_library_source", + (PyCFunction)PyMacMusicAppGetLibrarySource, METH_VARARGS | METH_KEYWORDS, + "mac_music_app_get_library_source() -> None\n" + "\n" + "(internal)"}, + + {"mac_music_app_stop", (PyCFunction)PyMacMusicAppStop, + METH_VARARGS | METH_KEYWORDS, + "mac_music_app_stop() -> None\n" + "\n" + "(internal)"}, + + {"mac_music_app_play_playlist", (PyCFunction)PyMacMusicAppPlayPlaylist, + METH_VARARGS | METH_KEYWORDS, + "mac_music_app_play_playlist(playlist: str) -> bool\n" + "\n" + "(internal)"}, + + {"mac_music_app_get_playlists", (PyCFunction)PyMacMusicAppGetPlaylists, + METH_VARARGS | METH_KEYWORDS, + "mac_music_app_get_playlists() -> List[str]\n" + "\n" + "(internal)"}, + + {"get_qrcode_texture", (PyCFunction)PyGetQRCodeTexture, + METH_VARARGS | METH_KEYWORDS, + "get_qrcode_texture(url: str) -> ba.Texture\n" + "\n" + "(internal)"}, + + {"reload_media", PyReloadMedia, METH_VARARGS, + "reload_media() -> None\n" + "\n" + "(internal)\n" + "\n" + "Reload all currently loaded game media; useful for\n" + "development/debugging."}, + + {"music_player_shutdown", (PyCFunction)PyMusicPlayerShutdown, + METH_VARARGS | METH_KEYWORDS, + "music_player_shutdown() -> None\n" + "\n" + "(internal)\n" + "\n" + "Finalizes internal music file playback (for internal use)"}, + + {"music_player_set_volume", (PyCFunction)PyMusicPlayerSetVolume, + METH_VARARGS | METH_KEYWORDS, + "music_player_set_volume(volume: float) -> None\n" + "\n" + "(internal)\n" + "\n" + "Sets internal music player volume (for internal use)"}, + + {"music_player_play", (PyCFunction)PyMusicPlayerPlay, + METH_VARARGS | METH_KEYWORDS, + "music_player_play(files: Any) -> None\n" + "\n" + "(internal)\n" + "\n" + "Starts internal music file playback (for internal use)"}, + + {"music_player_stop", (PyCFunction)PyMusicPlayerStop, + METH_VARARGS | METH_KEYWORDS, + "music_player_stop() -> None\n" + "\n" + "(internal)\n" + "\n" + "Stops internal music file playback (for internal use)"}, + + {"getcollidemodel", (PyCFunction)PyGetCollideModel, + METH_VARARGS | METH_KEYWORDS, + "getcollidemodel(name: str) -> ba.CollideModel\n" + "\n" + "Return a collide-model, loading it if necessary.\n" + "\n" + "Category: Asset Functions\n" + "\n" + "Collide-models are used in physics calculations for such things as\n" + "terrain.\n" + "\n" + "Note that this function returns immediately even if the media has yet\n" + "to be loaded. To avoid hitches, instantiate your media objects in\n" + "advance of when you will be using them, allowing time for them to load\n" + "in the background if necessary."}, + + {"get_package_collide_model", (PyCFunction)PyGetPackageCollideModel, + METH_VARARGS | METH_KEYWORDS, + "get_package_collide_model(package: ba.AssetPackage, name: str)\n" + "-> ba.CollideModel\n" + "\n" + "(internal)\n"}, + + {"getmodel", (PyCFunction)PyGetModel, METH_VARARGS | METH_KEYWORDS, + "getmodel(name: str) -> ba.Model\n" + "\n" + "Return a model, loading it if necessary.\n" + "\n" + "Category: Asset Functions\n" + "\n" + "Note that this function returns immediately even if the media has yet\n" + "to be loaded. To avoid hitches, instantiate your media objects in\n" + "advance of when you will be using them, allowing time for them to load\n" + "in the background if necessary."}, + + {"get_package_model", (PyCFunction)PyGetPackageModel, + METH_VARARGS | METH_KEYWORDS, + "get_package_model(package: ba.AssetPackage, name: str) -> ba.Model\n" + "\n" + "(internal)\n"}, + + {"getsound", (PyCFunction)PyGetSound, METH_VARARGS | METH_KEYWORDS, + "getsound(name: str) -> ba.Sound\n" + "\n" + "Return a sound, loading it if necessary.\n" + "\n" + "Category: Asset Functions\n" + "\n" + "Note that this function returns immediately even if the media has yet\n" + "to be loaded. To avoid hitches, instantiate your media objects in\n" + "advance of when you will be using them, allowing time for them to load\n" + "in the background if necessary."}, + + {"get_package_sound", (PyCFunction)PyGetPackageSound, + METH_VARARGS | METH_KEYWORDS, + "get_package_sound(package: ba.AssetPackage, name: str) -> ba.Sound\n" + "\n" + "(internal).\n"}, + + {"getdata", (PyCFunction)PyGetData, METH_VARARGS | METH_KEYWORDS, + "getdata(name: str) -> ba.Data\n" + "\n" + "Return a data, loading it if necessary.\n" + "\n" + "Category: Asset Functions\n" + "\n" + "Note that this function returns immediately even if the media has yet\n" + "to be loaded. To avoid hitches, instantiate your media objects in\n" + "advance of when you will be using them, allowing time for them to load\n" + "in the background if necessary."}, + + {"get_package_data", (PyCFunction)PyGetPackageData, + METH_VARARGS | METH_KEYWORDS, + "get_package_data(package: ba.AssetPackage, name: str) -> ba.Data\n" + "\n" + "(internal).\n"}, + + {"gettexture", (PyCFunction)PyGetTexture, METH_VARARGS | METH_KEYWORDS, + "gettexture(name: str) -> ba.Texture\n" + "\n" + "Return a texture, loading it if necessary.\n" + "\n" + "Category: Asset Functions\n" + "\n" + "Note that this function returns immediately even if the media has yet\n" + "to be loaded. To avoid hitches, instantiate your media objects in\n" + "advance of when you will be using them, allowing time for them to load\n" + "in the background if necessary."}, + + {"get_package_texture", (PyCFunction)PyGetPackageTexture, + METH_VARARGS | METH_KEYWORDS, + "get_package_texture(package: ba.AssetPackage, name: str) -> ba.Texture\n" + "\n" + "(internal)"}, + + {nullptr, nullptr, 0, nullptr}}; + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/ballistica/python/methods/python_methods_media.h b/src/ballistica/python/methods/python_methods_media.h new file mode 100644 index 00000000..69bc5f48 --- /dev/null +++ b/src/ballistica/python/methods/python_methods_media.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_MEDIA_H_ +#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_MEDIA_H_ + +#include "ballistica/python/python_sys.h" + +namespace ballistica { + +/// Media related individual python methods for our module. +class PythonMethodsMedia { + public: + static PyMethodDef methods_def[]; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_MEDIA_H_ diff --git a/src/ballistica/python/methods/python_methods_networking.cc b/src/ballistica/python/methods/python_methods_networking.cc new file mode 100644 index 00000000..2d5cb1eb --- /dev/null +++ b/src/ballistica/python/methods/python_methods_networking.cc @@ -0,0 +1,610 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/methods/python_methods_networking.h" + +#include +#include +#include + +#include "ballistica/app/app_globals.h" +#include "ballistica/game/connection/connection_to_host.h" +#include "ballistica/game/game.h" +#include "ballistica/math/vector3f.h" +#include "ballistica/networking/master_server_config.h" +#include "ballistica/networking/network_reader.h" +#include "ballistica/networking/networking.h" +#include "ballistica/networking/sockaddr.h" +#include "ballistica/networking/telnet_server.h" +#include "ballistica/platform/platform.h" +#include "ballistica/python/python.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" + +auto PyGetPublicPartyEnabled(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("getpublicpartyenabled"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) + return nullptr; + assert(g_python); + if (g_game->public_party_enabled()) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +auto PySetPublicPartyEnabled(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("setpublicpartyenabled"); + int enable; + static const char* kwlist[] = {"enabled", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", + const_cast(kwlist), &enable)) { + return nullptr; + } + assert(g_python); + g_game->SetPublicPartyEnabled(static_cast(enable)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PySetPublicPartyName(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("setpublicpartyname"); + PyObject* name_obj; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &name_obj)) { + return nullptr; + } + std::string name = Python::GetPyString(name_obj); + assert(g_python); + g_game->SetPublicPartyName(name); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PySetPublicPartyStatsURL(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("setpublicpartystatsurl"); + PyObject* url_obj; + static const char* kwlist[] = {"url", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &url_obj)) { + return nullptr; + } + // The call expects an empty string for the no-url option. + std::string url = (url_obj == Py_None) ? "" : Python::GetPyString(url_obj); + assert(g_python); + g_game->SetPublicPartyStatsURL(url); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetPublicPartyMaxSize(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("getpublicpartymaxsize"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + assert(g_python); + return PyLong_FromLong(g_game->public_party_max_size()); + BA_PYTHON_CATCH; +} + +auto PySetPublicPartyMaxSize(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("setpublicpartymaxsize"); + int max_size; + static const char* kwlist[] = {"max_size", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "i", + const_cast(kwlist), &max_size)) { + return nullptr; + } + assert(g_python); + g_game->SetPublicPartyMaxSize(max_size); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PySetAuthenticateClients(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_authenticate_clients"); + int enable; + static const char* kwlist[] = {"enable", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", + const_cast(kwlist), &enable)) { + return nullptr; + } + assert(g_game); + g_game->set_require_client_authentication(static_cast(enable)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PySetAdmins(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_admins"); + PyObject* admins_obj; + static const char* kwlist[] = {"admins", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &admins_obj)) { + return nullptr; + } + assert(g_game); + + auto admins = Python::GetPyStrings(admins_obj); + std::set adminset; + for (auto&& admin : admins) { + adminset.insert(admin); + } + g_game->set_admin_public_ids(adminset); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PySetEnableDefaultKickVoting(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_enable_default_kick_voting"); + int enable; + static const char* kwlist[] = {"enable", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", + const_cast(kwlist), &enable)) { + return nullptr; + } + assert(g_game); + g_game->set_kick_voting_enabled(static_cast(enable)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyConnectToParty(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("connect_to_party"); + std::string address; + PyObject* address_obj; + int port = kDefaultPort; + + // Whether we should print standard 'connecting...' and 'party full..' + // messages when false, only odd errors such as version incompatibility will + // be printed and most connection attempts will be silent todo: could + // generalize this to pass all results to a callback instead + int print_progress = 1; + static const char* kwlist[] = {"address", "port", "print_progress", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|ip", + const_cast(kwlist), &address_obj, + &port, &print_progress)) { + return nullptr; + } + address = Python::GetPyString(address_obj); + + // Disallow in headless build (people were using this for spam-bots). + + if (HeadlessMode()) { + throw Exception("Not available in headless mode."); + } + + SockAddr s; + try { + s = SockAddr(address, port); + + // HACK: CLion currently flags our catch clause as unreachable even + // though SockAddr constructor can throw exceptions. Work around that here. + if (explicit_bool(false)) { + throw Exception(); + } + } catch (const std::exception&) { + ScreenMessage(g_game->GetResourceString("invalidAddressErrorText"), + {1, 0, 0}); + Py_RETURN_NONE; + } + g_game->PushHostConnectedUDPCall(s, static_cast(print_progress)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyAcceptPartyInvitation(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("accept_party_invitation"); + const char* invite_id; + static const char* kwlist[] = {"invite_id", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &invite_id)) { + return nullptr; + } + g_platform->AndroidGPGSPartyInviteAccept(invite_id); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetGooglePlayPartyClientCount(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_google_play_party_client_count"); + BA_PRECONDITION(InGameThread()); +#if BA_GOOGLE_BUILD + return PyLong_FromLong(g_game->GetGooglePlayClientCount()); +#else + return PyLong_FromLong(0); +#endif + BA_PYTHON_CATCH; +} + +auto PyClientInfoQueryResponse(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("client_info_query_response"); + const char* token; + PyObject* response_obj; + static const char* kwlist[] = {"token", "response", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "sO", + const_cast(kwlist), &token, + &response_obj)) { + return nullptr; + } + g_game->SetClientInfoFromMasterServer(token, response_obj); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetConnectionToHostInfo(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_connection_to_host_info"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + ConnectionToHost* hc = g_game->connection_to_host(); + if (hc) { + return Py_BuildValue("{sssi}", "name", hc->party_name().c_str(), + "build_number", hc->build_number()); + } else { + return Py_BuildValue("{}"); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyDisconnectFromHost(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("disconnect_from_host"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + g_game->PushDisconnectFromHostCall(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyDisconnectClient(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("disconnect_client"); + int client_id; + int ban_time = 300; // Old default before we exposed this. + static const char* kwlist[] = {"client_id", "ban_time", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|i", + const_cast(kwlist), &client_id, + &ban_time)) { + return nullptr; + } + bool kickable = g_game->DisconnectClient(client_id, ban_time); + if (kickable) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +auto PyGetGamePort(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_game_port"); + int port = 0; + if (g_network_reader != nullptr) { + // hmmm; we're just fetching the ipv4 port here; + // 6 could be different.... + port = g_network_reader->port4(); + } + return Py_BuildValue("i", port); + BA_PYTHON_CATCH; +} + +auto PyGetMasterServerAddress(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_master_server_address"); + int source = -1; // use default.. + if (!PyArg_ParseTuple(args, "|i", &source)) { + return nullptr; + } + // source -1 implies to use current one + if (source == -1) { + source = g_app_globals->master_server_source; + } + const char* addr; + if (source == 0) { + addr = BA_MASTER_SERVER_DEFAULT_ADDR; + } else if (source == 1) { + addr = BA_MASTER_SERVER_FALLBACK_ADDR; + } else { + BA_LOG_ONCE("Error: Got unexpected source: " + std::to_string(source) + + "."); + addr = BA_MASTER_SERVER_FALLBACK_ADDR; + } + return PyUnicode_FromString(addr); + BA_PYTHON_CATCH; +} + +auto PySetMasterServerSource(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_master_server_source"); + int source; + if (!PyArg_ParseTuple(args, "i", &source)) return nullptr; + if (source != 0 && source != 1) { + BA_LOG_ONCE("Error: Invalid server source: " + std::to_string(source) + + "."); + source = 1; + } + g_app_globals->master_server_source = source; + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PySetTelnetAccessEnabled(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_telnet_access_enabled"); + assert(InGameThread()); + int enable; + static const char* kwlist[] = {"enable", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", + const_cast(kwlist), &enable)) { + return nullptr; + } + if (g_app_globals->telnet_server) { + g_app_globals->telnet_server->SetAccessEnabled(static_cast(enable)); + } else { + throw Exception("Telnet server not enabled."); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyHostScanCycle(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("host_scan_cycle"); + g_networking->HostScanCycle(); + std::vector results = + g_networking->GetScanResults(); + PyObject* py_list = PyList_New(0); + for (auto&& i : results) { + PyList_Append(py_list, Py_BuildValue("{ssss}", "display_string", + i.display_string.c_str(), "address", + i.address.c_str())); + } + return py_list; + BA_PYTHON_CATCH; +} + +auto PyEndHostScanning(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("end_host_scanning"); + g_networking->EndHostScanning(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyHaveConnectedClients(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("have_connected_clients"); + if (g_game->GetConnectedClientCount() > 0) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +auto PyInvitePlayers(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("invite_players"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + g_platform->AndroidGPGSPartyInvitePlayers(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +PyMethodDef PythonMethodsNetworking::methods_def[] = { + {"invite_players", (PyCFunction)PyInvitePlayers, + METH_VARARGS | METH_KEYWORDS, + "invite_players() -> None\n" + "\n" + "(internal)" + "\n" + "Category: General Utility Functions"}, + + {"have_connected_clients", (PyCFunction)PyHaveConnectedClients, + METH_VARARGS | METH_KEYWORDS, + "have_connected_clients() -> bool\n" + "\n" + "(internal)\n" + "\n" + "Category: General Utility Functions"}, + + {"end_host_scanning", (PyCFunction)PyEndHostScanning, + METH_VARARGS | METH_KEYWORDS, + "end_host_scanning() -> None\n" + "\n" + "(internal)\n" + "\n" + "Category: General Utility Functions"}, + + {"host_scan_cycle", (PyCFunction)PyHostScanCycle, + METH_VARARGS | METH_KEYWORDS, + "host_scan_cycle() -> list\n" + "\n" + "(internal)"}, + + {"set_telnet_access_enabled", (PyCFunction)PySetTelnetAccessEnabled, + METH_VARARGS | METH_KEYWORDS, + "set_telnet_access_enabled(enable: bool)\n" + " -> None\n" + "\n" + "(internal)"}, + + {"set_master_server_source", PySetMasterServerSource, METH_VARARGS, + "set_master_server_source(source: int) -> None\n" + "\n" + "(internal)"}, + + {"get_master_server_address", PyGetMasterServerAddress, METH_VARARGS, + "get_master_server_address(source: int = -1) -> str\n" + "\n" + "(internal)\n" + "\n" + "Return the address of the master server."}, + + {"get_game_port", PyGetGamePort, METH_VARARGS, + "get_game_port() -> int\n" + "\n" + "(internal)\n" + "\n" + "Return the port ballistica is hosting on."}, + + {"disconnect_from_host", (PyCFunction)PyDisconnectFromHost, + METH_VARARGS | METH_KEYWORDS, + "disconnect_from_host() -> None\n" + "\n" + "(internal)\n" + "\n" + "Category: General Utility Functions"}, + + {"disconnect_client", (PyCFunction)PyDisconnectClient, + METH_VARARGS | METH_KEYWORDS, + "disconnect_client(client_id: int, ban_time: int = 300) -> bool\n" + "\n" + "(internal)"}, + + {"get_connection_to_host_info", (PyCFunction)PyGetConnectionToHostInfo, + METH_VARARGS | METH_KEYWORDS, + "get_connection_to_host_info() -> dict\n" + "\n" + "(internal)"}, + + {"client_info_query_response", (PyCFunction)PyClientInfoQueryResponse, + METH_VARARGS | METH_KEYWORDS, + "client_info_query_response(token: str, response: Any) -> None\n" + "\n" + "(internal)"}, + + {"get_google_play_party_client_count", + (PyCFunction)PyGetGooglePlayPartyClientCount, METH_VARARGS | METH_KEYWORDS, + "get_google_play_party_client_count() -> int\n" + "\n" + "(internal)"}, + + {"accept_party_invitation", (PyCFunction)PyAcceptPartyInvitation, + METH_VARARGS | METH_KEYWORDS, + "accept_party_invitation(invite_id: str) -> None\n" + "\n" + "(internal)"}, + + {"connect_to_party", (PyCFunction)PyConnectToParty, + METH_VARARGS | METH_KEYWORDS, + "connect_to_party(address: str, port: int = None,\n" + " print_progress: bool = True) -> None\n" + "\n" + "(internal)"}, + + {"set_authenticate_clients", (PyCFunction)PySetAuthenticateClients, + METH_VARARGS | METH_KEYWORDS, + "set_authenticate_clients(enable: bool) -> None\n" + "\n" + "(internal)"}, + + {"set_admins", (PyCFunction)PySetAdmins, METH_VARARGS | METH_KEYWORDS, + "set_admins(admins: List[str]) -> None\n" + "\n" + "(internal)"}, + + {"set_enable_default_kick_voting", + (PyCFunction)PySetEnableDefaultKickVoting, METH_VARARGS | METH_KEYWORDS, + "set_enable_default_kick_voting(enable: bool) -> None\n" + "\n" + "(internal)"}, + + {"set_public_party_max_size", (PyCFunction)PySetPublicPartyMaxSize, + METH_VARARGS | METH_KEYWORDS, + "set_public_party_max_size(max_size: int) -> None\n" + "\n" + "(internal)"}, + + {"get_public_party_max_size", (PyCFunction)PyGetPublicPartyMaxSize, + METH_VARARGS | METH_KEYWORDS, + "get_public_party_max_size() -> int\n" + "\n" + "(internal)"}, + + {"set_public_party_stats_url", (PyCFunction)PySetPublicPartyStatsURL, + METH_VARARGS | METH_KEYWORDS, + "set_public_party_stats_url(url: Optional[str]) -> None\n" + "\n" + "(internal)"}, + + {"set_public_party_name", (PyCFunction)PySetPublicPartyName, + METH_VARARGS | METH_KEYWORDS, + "set_public_party_name(name: str) -> None\n" + "\n" + "(internal)"}, + + {"set_public_party_enabled", (PyCFunction)PySetPublicPartyEnabled, + METH_VARARGS | METH_KEYWORDS, + "set_public_party_enabled(enabled: bool) -> None\n" + "\n" + "(internal)"}, + + {"get_public_party_enabled", (PyCFunction)PyGetPublicPartyEnabled, + METH_VARARGS | METH_KEYWORDS, + "get_public_party_enabled() -> bool\n" + "\n" + "(internal)"}, + + {nullptr, nullptr, 0, nullptr}}; + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/ballistica/python/methods/python_methods_networking.h b/src/ballistica/python/methods/python_methods_networking.h new file mode 100644 index 00000000..bb6a2c42 --- /dev/null +++ b/src/ballistica/python/methods/python_methods_networking.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_NETWORKING_H_ +#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_NETWORKING_H_ + +#include "ballistica/python/python_sys.h" + +namespace ballistica { + +/// Networking related individual python methods for our module. +class PythonMethodsNetworking { + public: + static PyMethodDef methods_def[]; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_NETWORKING_H_ diff --git a/src/ballistica/python/methods/python_methods_system.cc b/src/ballistica/python/methods/python_methods_system.cc new file mode 100644 index 00000000..89691355 --- /dev/null +++ b/src/ballistica/python/methods/python_methods_system.cc @@ -0,0 +1,1031 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/methods/python_methods_system.h" + +#include +#include +#include +#include + +#include "ballistica/app/app.h" +#include "ballistica/app/app_config.h" +#include "ballistica/app/app_globals.h" +#include "ballistica/game/game_stream.h" +#include "ballistica/game/host_activity.h" +#include "ballistica/game/session/host_session.h" +#include "ballistica/game/session/replay_client_session.h" +#include "ballistica/graphics/camera.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/input/input.h" +#include "ballistica/media/component/texture.h" +#include "ballistica/media/media.h" +#include "ballistica/platform/platform.h" +#include "ballistica/python/python.h" +#include "ballistica/python/python_context_call_runnable.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +// Ignore signed bitwise warnings; python macros do it quite a bit. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +#pragma ide diagnostic ignored "RedundantCast" + +auto PyIsRunningOnOuya(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("is_running_on_ouya"); + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +auto PySetUpSigInt(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_up_sig_int"); + if (g_app) { + g_app->PushInterruptSignalSetupCall(); + } else { + Log("SigInt handler called before g_app exists."); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyIsRunningOnFireTV(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("is_running_on_fire_tv"); + if (g_platform->IsRunningOnFireTV()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +auto PyHavePermission(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("have_permission"); + BA_PRECONDITION(InGameThread()); + Permission permission; + PyObject* permission_obj; + static const char* kwlist[] = {"permission", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "O", const_cast(kwlist), &permission_obj)) { + return nullptr; + } + + permission = Python::GetPyEnum_Permission(permission_obj); + + if (g_platform->HavePermission(permission)) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +auto PyRequestPermission(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("request_permission"); + BA_PRECONDITION(InGameThread()); + Permission permission; + PyObject* permission_obj; + static const char* kwlist[] = {"permission", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "O", const_cast(kwlist), &permission_obj)) { + return nullptr; + } + + permission = Python::GetPyEnum_Permission(permission_obj); + g_platform->RequestPermission(permission); + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyInGameThread(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("in_game_thread"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + if (InGameThread()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +auto PySetThreadName(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_thread_name"); + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + g_platform->SetCurrentThreadName(name); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetThreadName(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_thread_name"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + return PyUnicode_FromString(GetCurrentThreadName().c_str()); + BA_PYTHON_CATCH; +} + +// returns an extra hash value that can be incorporated into security checks; +// this contains things like whether console commands have been run, etc. +auto PyExtraHashValue(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("extra_hash_value"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + const char* h = (g_app_globals->user_ran_commands ? "cjief3l" : "wofocj8"); + return PyUnicode_FromString(h); + BA_PYTHON_CATCH; +} + +auto PySetHaveMods(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_have_mods"); + int have_mods; + if (!PyArg_ParseTuple(args, "p", &have_mods)) return nullptr; + g_app_globals->have_mods = static_cast(have_mods); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetIdleTime(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_idle_time"); + return PyLong_FromLong(static_cast_check_fit( // NOLINT + g_input ? g_input->GetIdleTime() : 0)); + BA_PYTHON_CATCH; +} + +auto PyHasUserRunCommands(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("has_user_run_commands"); + if (g_app_globals->user_ran_commands) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +auto PyHasUserMods(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("has_user_mods"); + if (g_app_globals->have_mods) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +auto PyValueTest(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("value_test"); + const char* arg; + double change = 0.0f; + double absolute = 0.0f; + bool have_change = false; + bool have_absolute = false; + PyObject* change_obj = Py_None; + PyObject* absolute_obj = Py_None; + static const char* kwlist[] = {"arg", "change", "absolute", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s|OO", + const_cast(kwlist), &arg, + &change_obj, &absolute_obj)) { + return nullptr; + } + if (change_obj != Py_None) { + if (absolute_obj != Py_None) { + throw Exception("Can't provide both a change and absolute"); + } + have_change = true; + change = Python::GetPyDouble(change_obj); + } + if (absolute_obj != Py_None) { + have_absolute = true; + absolute = Python::GetPyDouble(absolute_obj); + } + double return_val = 0.0f; + if (!strcmp(arg, "bufferTime")) { + if (have_change) { + g_app_globals->buffer_time += static_cast(change); + } + if (have_absolute) { + g_app_globals->buffer_time = static_cast(absolute); + } + g_app_globals->buffer_time = std::max(0, g_app_globals->buffer_time); + return_val = g_app_globals->buffer_time; + } else if (!strcmp(arg, "delaySampling")) { + if (have_change) { + g_app_globals->delay_samples += static_cast(change); + } + if (have_absolute) { + g_app_globals->buffer_time = static_cast(absolute); + } + g_app_globals->delay_samples = std::max(1, g_app_globals->delay_samples); + return_val = g_app_globals->delay_samples; + } else if (!strcmp(arg, "dynamicsSyncTime")) { + if (have_change) { + g_app_globals->dynamics_sync_time += static_cast(change); + } + if (have_absolute) { + g_app_globals->dynamics_sync_time = static_cast(absolute); + } + g_app_globals->dynamics_sync_time = + std::max(0, g_app_globals->dynamics_sync_time); + return_val = g_app_globals->dynamics_sync_time; + } else if (!strcmp(arg, "showNetInfo")) { + if (have_change && change > 0.5f) { + g_graphics->set_show_net_info(true); + } + if (have_change && change < -0.5f) { + g_graphics->set_show_net_info(false); + } + if (have_absolute) { + g_graphics->set_show_net_info(static_cast(absolute)); + } + return_val = g_graphics->show_net_info(); + } else if (!strcmp(arg, "allowCameraMovement")) { + Camera* camera = g_graphics->camera(); + if (camera) { + if (have_change && change > 0.5f) { + camera->set_lock_panning(false); + } + if (have_change && change < -0.5f) { + camera->set_lock_panning(true); + } + if (have_absolute) { + camera->set_lock_panning(!static_cast(absolute)); + } + return_val = !camera->lock_panning(); + } + } else if (!strcmp(arg, "cameraPanSpeedScale")) { + Camera* camera = g_graphics->camera(); + if (camera) { + double val = camera->pan_speed_scale(); + if (have_change) { + camera->set_pan_speed_scale(static_cast(val + change)); + } + if (have_absolute) { + camera->set_pan_speed_scale(static_cast(absolute)); + } + if (camera->pan_speed_scale() < 0) { + camera->set_pan_speed_scale(0); + } + return_val = camera->pan_speed_scale(); + } + } else { + auto handled = + g_graphics->ValueTest(arg, have_absolute ? &absolute : nullptr, + have_change ? &change : nullptr, &return_val); + if (!handled) { + ScreenMessage("invalid arg: " + std::string(arg)); + } + } + + return PyFloat_FromDouble(return_val); + + BA_PYTHON_CATCH; +} + +auto PyDebugPrintPyErr(PyObject* self, PyObject* args) -> PyObject* { + Platform::SetLastPyCall("debug_print_py_err"); + if (PyErr_Occurred()) { + // we pass zero here to avoid grabbing references to this exception + // which can cause objects to stick around and trip up our deletion checks + // (nodes, actors existing after their games have ended) + PyErr_PrintEx(0); + PyErr_Clear(); + } + Py_RETURN_NONE; +} + +auto PyPrintContext(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("print_context"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + Python::LogContextAuto(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyPrintLoadInfo(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("print_load_info"); + g_media->PrintLoadInfo(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetReplaysDir(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_replays_dir"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + return PyUnicode_FromString(g_platform->GetReplaysDir().c_str()); + BA_PYTHON_CATCH; +} + +auto PyGetAppConfigDefaultValue(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_app_config_default_value"); + const char* key = ""; + static const char* kwlist[] = {"key", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &key)) { + return nullptr; + } + const AppConfig::Entry* entry = g_app_config->GetEntry(key); + if (entry == nullptr) { + throw Exception("Invalid config value '" + std::string(key) + "'", + PyExcType::kValue); + } + switch (entry->GetType()) { + case AppConfig::Entry::Type::kString: + return PyUnicode_FromString(entry->DefaultStringValue().c_str()); + case AppConfig::Entry::Type::kInt: + return PyLong_FromLong(entry->DefaultIntValue()); + case AppConfig::Entry::Type::kFloat: + return PyFloat_FromDouble(entry->DefaultFloatValue()); + case AppConfig::Entry::Type::kBool: + if (entry->DefaultBoolValue()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + default: + throw Exception(PyExcType::kValue); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyAppConfigGetBuiltinKeys(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("app_config_get_builtin_keys"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + PythonRef list(PyList_New(0), PythonRef::kSteal); + for (auto&& i : g_app_config->entries_by_name()) { + PyList_Append(list.get(), PyUnicode_FromString(i.first.c_str())); + } + return list.HandOver(); + BA_PYTHON_CATCH; +} + +auto PyResolveAppConfigValue(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("resolve_app_config_value"); + + const char* key; + static const char* kwlist[] = {"key", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &key)) { + return nullptr; + } + auto entry = g_app_config->GetEntry(key); + if (entry == nullptr) { + throw Exception("Invalid config value '" + std::string(key) + "'.", + PyExcType::kValue); + } + switch (entry->GetType()) { + case AppConfig::Entry::Type::kString: + return PyUnicode_FromString(entry->StringValue().c_str()); + case AppConfig::Entry::Type::kInt: + return PyLong_FromLong(entry->IntValue()); + case AppConfig::Entry::Type::kFloat: + return PyFloat_FromDouble(entry->FloatValue()); + case AppConfig::Entry::Type::kBool: + if (entry->BoolValue()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + + default: + throw Exception(PyExcType::kValue); + } + BA_PYTHON_CATCH; +} + +auto PyGetLowLevelConfigValue(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_low_level_config_value"); + const char* key; + int default_value; + static const char* kwlist[] = {"key", "default_value", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "si", const_cast(kwlist), &key, &default_value)) + return nullptr; + return PyLong_FromLong( + g_platform->GetLowLevelConfigValue(key, default_value)); + BA_PYTHON_CATCH; +} + +auto PySetLowLevelConfigValue(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_low_level_config_value"); + const char* key; + int value; + static const char* kwlist[] = {"key", "value", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "si", + const_cast(kwlist), &key, &value)) + return nullptr; + g_platform->SetLowLevelConfigValue(key, value); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PySetPlatformMiscReadVals(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_platform_misc_read_vals"); + PyObject* vals_obj; + static const char* kwlist[] = {"mode", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", + const_cast(kwlist), &vals_obj)) { + return nullptr; + } + std::string vals = Python::GetPyString(vals_obj); + g_platform->SetPlatformMiscReadVals(vals); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetLogFilePath(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_log_file_path"); + std::string config_dir = g_platform->GetConfigDirectory(); + std::string logpath = config_dir + BA_DIRSLASH + "log.json"; + return PyUnicode_FromString(logpath.c_str()); + BA_PYTHON_CATCH; +} + +auto PyIsLogFull(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("is_log_full"); + if (g_app_globals->log_full) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +auto PyGetLog(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("get_log"); + std::string log_fin; + { + std::lock_guard lock(g_app_globals->log_mutex); + log_fin = g_app_globals->log; + } + // we want to use something with error handling here since the last + // bit of this string could be truncated utf8 chars.. + return PyUnicode_FromString( + Utils::GetValidUTF8(log_fin.c_str(), "_glg1").c_str()); + BA_PYTHON_CATCH; +} + +auto PyMarkLogSent(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("mark_log_sent"); + // this way we won't try to send it at shutdown time and whatnot + g_app_globals->put_log = true; + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyIncrementAnalyticsCount(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("increment_analytics_count"); + const char* name; + int increment = 1; + static const char* kwlist[] = {"name", "increment", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "s|p", const_cast(kwlist), &name, &increment)) { + return nullptr; + } + g_platform->IncrementAnalyticsCount(name, increment); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyIncrementAnalyticsCountRaw(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("increment_analytics_count_raw"); + const char* name; + int increment = 1; + static const char* kwlist[] = {"name", "increment", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "s|i", const_cast(kwlist), &name, &increment)) { + return nullptr; + } + g_platform->IncrementAnalyticsCountRaw(name, increment); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyIncrementAnalyticsCountRaw2(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("increment_analytics_count_raw2"); + const char* name; + int uses_increment = 1; + int increment = 1; + static const char* kwlist[] = {"name", "uses_increment", "increment", + nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s|ii", + const_cast(kwlist), &name, + &uses_increment, &increment)) { + return nullptr; + } + g_platform->IncrementAnalyticsCountRaw2(name, uses_increment, increment); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PySubmitAnalyticsCounts(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("submit_analytics_counts"); + g_platform->SubmitAnalyticsCounts(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PySetAnalyticsScreen(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_analytics_screen"); + const char* screen; + static const char* kwlist[] = {"screen", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &screen)) { + return nullptr; + } + g_platform->SetAnalyticsScreen(screen); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PySetInternalLanguageKeys(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_internal_language_keys"); + PyObject* list_obj; + PyObject* random_names_list_obj; + if (!PyArg_ParseTuple(args, "OO", &list_obj, &random_names_list_obj)) { + return nullptr; + } + BA_PRECONDITION(PyList_Check(list_obj)); + BA_PRECONDITION(PyList_Check(random_names_list_obj)); + std::map language; + int size = static_cast(PyList_GET_SIZE(list_obj)); + for (int i = 0; i < size; i++) { + PyObject* entry = PyList_GET_ITEM(list_obj, i); + if (!PyTuple_Check(entry) || PyTuple_GET_SIZE(entry) != 2 + || !PyUnicode_Check(PyTuple_GET_ITEM(entry, 0)) + || !PyUnicode_Check(PyTuple_GET_ITEM(entry, 1))) { + throw Exception("Invalid root language data."); + } + language[PyUnicode_AsUTF8(PyTuple_GET_ITEM(entry, 0))] = + PyUnicode_AsUTF8(PyTuple_GET_ITEM(entry, 1)); + } + size = static_cast(PyList_GET_SIZE(random_names_list_obj)); + std::list random_names; + for (int i = 0; i < size; i++) { + PyObject* entry = PyList_GET_ITEM(random_names_list_obj, i); + if (!PyUnicode_Check(entry)) { + throw Exception("Got non-string in random name list.", PyExcType::kType); + } + random_names.emplace_back(PyUnicode_AsUTF8(entry)); + } + Utils::SetRandomNameList(random_names); + assert(g_game); + g_game->SetLanguageKeys(language); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyIsOuyaBuild(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("is_ouya_builds"); + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +auto PyAndroidMediaScanFile(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("android_media_scan_file"); + const char* file_name; + static const char* kwlist[] = {"file_name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &file_name)) { + return nullptr; + } + g_platform->AndroidRefreshFile(file_name); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyAndroidGetExternalStoragePath(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("android_get_external_storage_path"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } +#if BA_OSTYPE_ANDROID + std::string path = g_platform->GetExternalStoragePath(); + if (path.empty()) { + Py_RETURN_NONE; + } else { + assert(Utils::IsValidUTF8(path)); + return PyUnicode_FromString(path.c_str()); + } +#else // BA_OSTYPE_ANDROID + throw Exception("Only valid on android."); +#endif // BA_OSTYPE_ANDROID + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyAndroidShowWifiSettings(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("android_show_wifi_settings"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + g_platform->AndroidShowWifiSettings(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyPrintObjects(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("print_objects"); + Object::PrintObjects(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyDoOnce(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("do_once"); + if (g_python->DoOnce()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + BA_PYTHON_CATCH; +} + +auto PyApp(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("app"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + return g_python->obj(Python::ObjID::kApp).NewRef(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +PyMethodDef PythonMethodsSystem::methods_def[] = { + {"printobjects", (PyCFunction)PyPrintObjects, METH_VARARGS | METH_KEYWORDS, + "printobjects() -> None\n" + "\n" + "Print debugging info about game objects.\n" + "\n" + "Category: General Utility Functions\n" + "\n" + "This call only functions in debug builds of the game.\n" + "It prints various info about the current object count, etc."}, + + {"do_once", (PyCFunction)PyDoOnce, METH_VARARGS | METH_KEYWORDS, + "do_once() -> bool\n" + "\n" + "Return whether this is the first time running a line of code.\n" + "\n" + "Category: General Utility Functions\n" + "\n" + "This is used by 'print_once()' type calls to keep from overflowing\n" + "logs. The call functions by registering the filename and line where\n" + "The call is made from. Returns True if this location has not been\n" + "registered already, and False if it has.\n" + "\n" + "# Example: this print will only fire for the first loop iteration:\n" + "for i in range(10):\n" + " if ba.do_once():\n" + " print('Hello once from loop!')"}, + + {"_app", (PyCFunction)PyApp, METH_VARARGS | METH_KEYWORDS, + "_app() -> ba.App\n" + "\n" + "(internal)"}, + + {"android_media_scan_file", (PyCFunction)PyAndroidMediaScanFile, + METH_VARARGS | METH_KEYWORDS, + "android_media_scan_file(file_name: str) -> None\n" + "\n" + "(internal)\n" + "\n" + "Refreshes Android MTP Index for a file; use this to get file\n" + "modifications to be reflected in Android File Transfer."}, + + {"android_get_external_storage_path", + (PyCFunction)PyAndroidGetExternalStoragePath, METH_VARARGS | METH_KEYWORDS, + "android_get_external_storage_path() -> str\n" + "\n" + "(internal)\n" + "\n" + "Returns the android external storage path, or None if there is none on\n" + "this device"}, + + {"android_show_wifi_settings", (PyCFunction)PyAndroidShowWifiSettings, + METH_VARARGS | METH_KEYWORDS, + "android_show_wifi_settings() -> None\n" + "\n" + "(internal)"}, + + {"is_ouya_build", PyIsOuyaBuild, METH_VARARGS, + "is_ouya_build() -> bool\n" + "\n" + "(internal)\n" + "\n" + "Returns whether we're running the ouya-specific version"}, + + {"set_internal_language_keys", PySetInternalLanguageKeys, METH_VARARGS, + "set_internal_language_keys(listobj: List[Tuple[str, str]],\n" + " random_names_list: List[Tuple[str, str]]) -> None\n" + "\n" + "(internal)"}, + + {"set_analytics_screen", (PyCFunction)PySetAnalyticsScreen, + METH_VARARGS | METH_KEYWORDS, + "set_analytics_screen(screen: str) -> None\n" + "\n" + "Used for analytics to see where in the app players spend their time.\n" + "\n" + "Category: General Utility Functions\n" + "\n" + "Generally called when opening a new window or entering some UI.\n" + "'screen' should be a string description of an app location\n" + "('Main Menu', etc.)"}, + + {"submit_analytics_counts", (PyCFunction)PySubmitAnalyticsCounts, + METH_VARARGS | METH_KEYWORDS, + "submit_analytics_counts() -> None\n" + "\n" + "(internal)"}, + + {"increment_analytics_count_raw_2", + (PyCFunction)PyIncrementAnalyticsCountRaw2, METH_VARARGS | METH_KEYWORDS, + "increment_analytics_count_raw_2(name: str,\n" + " uses_increment: bool = True, increment: int = 1) -> None\n" + "\n" + "(internal)"}, + + {"increment_analytics_counts_raw", + (PyCFunction)PyIncrementAnalyticsCountRaw, METH_VARARGS | METH_KEYWORDS, + "increment_analytics_counts_raw(name: str, increment: int = 1) -> None\n" + "\n" + "(internal)"}, + + {"increment_analytics_count", (PyCFunction)PyIncrementAnalyticsCount, + METH_VARARGS | METH_KEYWORDS, + "increment_analytics_count(name: str, increment: int = 1) -> None\n" + "\n" + "(internal)"}, + + {"mark_log_sent", (PyCFunction)PyMarkLogSent, METH_VARARGS | METH_KEYWORDS, + "mark_log_sent() -> None\n" + "\n" + "(internal)"}, + + {"getlog", (PyCFunction)PyGetLog, METH_VARARGS | METH_KEYWORDS, + "getlog() -> str\n" + "\n" + "(internal)"}, + + {"is_log_full", PyIsLogFull, METH_VARARGS, + "is_log_full() -> bool\n" + "\n" + "(internal)"}, + + {"get_log_file_path", PyGetLogFilePath, METH_VARARGS, + "get_log_file_path() -> str\n" + "\n" + "(internal)\n" + "\n" + "Return the path to the app log file."}, + + {"set_platform_misc_read_vals", (PyCFunction)PySetPlatformMiscReadVals, + METH_VARARGS | METH_KEYWORDS, + "set_platform_misc_read_vals(mode: str) -> None\n" + "\n" + "(internal)"}, + + {"set_low_level_config_value", (PyCFunction)PySetLowLevelConfigValue, + METH_VARARGS | METH_KEYWORDS, + "set_low_level_config_value(key: str, value: int) -> None\n" + "\n" + "(internal)"}, + + {"get_low_level_config_value", (PyCFunction)PyGetLowLevelConfigValue, + METH_VARARGS | METH_KEYWORDS, + "get_low_level_config_value(key: str, default_value: int) -> int\n" + "\n" + "(internal)"}, + + {"resolve_appconfig_value", (PyCFunction)PyResolveAppConfigValue, + METH_VARARGS | METH_KEYWORDS, + "resolve_appconfig_value(key: str) -> Any\n" + "\n" + "(internal)"}, + + {"get_appconfig_default_value", (PyCFunction)PyGetAppConfigDefaultValue, + METH_VARARGS | METH_KEYWORDS, + "get_appconfig_default_value(key: str) -> Any\n" + "\n" + "(internal)"}, + + {"get_appconfig_builtin_keys", (PyCFunction)PyAppConfigGetBuiltinKeys, + METH_VARARGS | METH_KEYWORDS, + "get_appconfig_builtin_keys() -> List[str]\n" + "\n" + "(internal)"}, + + {"get_replays_dir", (PyCFunction)PyGetReplaysDir, + METH_VARARGS | METH_KEYWORDS, + "get_replays_dir() -> str\n" + "\n" + "(internal)"}, + + {"print_load_info", (PyCFunction)PyPrintLoadInfo, + METH_VARARGS | METH_KEYWORDS, + "print_load_info() -> None\n" + "\n" + "(internal)\n" + "\n" + "Category: General Utility Functions"}, + + {"print_context", (PyCFunction)PyPrintContext, METH_VARARGS | METH_KEYWORDS, + "print_context() -> None\n" + "\n" + "(internal)\n" + "\n" + "Prints info about the current context state; for debugging.\n"}, + + {"debug_print_py_err", PyDebugPrintPyErr, METH_VARARGS, + "debug_print_py_err() -> None\n" + "\n" + "(internal)\n" + "\n" + "Debugging func for tracking leaked Python errors in the C++ layer.."}, + + {"value_test", (PyCFunction)PyValueTest, METH_VARARGS | METH_KEYWORDS, + "value_test(arg: str, change: float = None, absolute: float = None)\n" + " -> float\n" + "\n" + "(internal)"}, + + {"has_user_mods", PyHasUserMods, METH_VARARGS, + "has_user_mods() -> bool\n" + "\n" + "(internal)\n" + "\n" + "Returns whether the system varies from default configuration\n" + "(by user mods, etc)"}, + + {"has_user_run_commands", PyHasUserRunCommands, METH_VARARGS, + "has_user_run_commands() -> bool\n" + "\n" + "(internal)"}, + + {"get_idle_time", PyGetIdleTime, METH_VARARGS, + "get_idle_time() -> int\n" + "\n" + "(internal)\n" + "\n" + "Returns the amount of time since any game input has been processed"}, + + {"set_have_mods", PySetHaveMods, METH_VARARGS, + "set_have_mods(have_mods: bool) -> None\n" + "\n" + "(internal)"}, + + {"ehv", (PyCFunction)PyExtraHashValue, METH_VARARGS | METH_KEYWORDS, + "ehv() -> None\n" + "\n" + "(internal)"}, + + {"get_thread_name", (PyCFunction)PyGetThreadName, + METH_VARARGS | METH_KEYWORDS, + "get_thread_name() -> str\n" + "\n" + "(internal)\n" + "\n" + "Returns the name of the current thread.\n" + "This may vary depending on platform and should not be used in logic;\n" + "only for debugging."}, + + {"set_thread_name", (PyCFunction)PySetThreadName, + METH_VARARGS | METH_KEYWORDS, + "set_thread_name(name: str) -> None\n" + "\n" + "(internal)\n" + "\n" + "Sets the name of the current thread (on platforms where this is\n" + "available). Thread names are only for debugging and should not be\n" + "used in logic, as naming behavior can vary across platforms.\n"}, + + {"in_game_thread", (PyCFunction)PyInGameThread, + METH_VARARGS | METH_KEYWORDS, + "in_game_thread() -> bool\n" + "\n" + "(internal)\n" + "\n" + "Returns whether or not the current thread is the game thread."}, + + {"request_permission", (PyCFunction)PyRequestPermission, + METH_VARARGS | METH_KEYWORDS, + "request_permission(permission: ba.Permission) -> None\n" + "\n" + "(internal)"}, + + {"have_permission", (PyCFunction)PyHavePermission, + METH_VARARGS | METH_KEYWORDS, + "have_permission(permission: ba.Permission) -> bool\n" + "\n" + "(internal)"}, + + {"is_running_on_fire_tv", PyIsRunningOnFireTV, METH_VARARGS, + "is_running_on_fire_tv() -> bool\n" + "\n" + "(internal)"}, + + {"is_running_on_ouya", PyIsRunningOnOuya, METH_VARARGS, + "is_running_on_ouya() -> bool\n" + "\n" + "(internal)"}, + + {"setup_sigint", (PyCFunction)PySetUpSigInt, METH_NOARGS, + "setup_sigint() -> None\n" + "\n" + "(internal)"}, + + {nullptr, nullptr, 0, nullptr}}; + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/ballistica/python/methods/python_methods_system.h b/src/ballistica/python/methods/python_methods_system.h new file mode 100644 index 00000000..d22017db --- /dev/null +++ b/src/ballistica/python/methods/python_methods_system.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_SYSTEM_H_ +#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_SYSTEM_H_ + +#include "ballistica/python/python_sys.h" + +namespace ballistica { + +/// System related individual python methods for our module. +class PythonMethodsSystem { + public: + static PyMethodDef methods_def[]; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_SYSTEM_H_ diff --git a/src/ballistica/python/methods/python_methods_ui.cc b/src/ballistica/python/methods/python_methods_ui.cc new file mode 100644 index 00000000..38fddc48 --- /dev/null +++ b/src/ballistica/python/methods/python_methods_ui.cc @@ -0,0 +1,2710 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/methods/python_methods_ui.h" + +#include +#include + +#include "ballistica/app/app.h" +#include "ballistica/app/app_globals.h" +#include "ballistica/game/account.h" +#include "ballistica/game/game.h" +#include "ballistica/input/input.h" +#include "ballistica/python/python.h" +#include "ballistica/ui/root_ui.h" +#include "ballistica/ui/ui.h" +#include "ballistica/ui/widget/button_widget.h" +#include "ballistica/ui/widget/check_box_widget.h" +#include "ballistica/ui/widget/column_widget.h" +#include "ballistica/ui/widget/container_widget.h" +#include "ballistica/ui/widget/h_scroll_widget.h" +#include "ballistica/ui/widget/image_widget.h" +#include "ballistica/ui/widget/root_widget.h" +#include "ballistica/ui/widget/row_widget.h" +#include "ballistica/ui/widget/scroll_widget.h" +#include "ballistica/ui/widget/text_widget.h" + +#if !BA_HEADLESS_BUILD +extern "C" void SDL_ericf_focus(); +#endif + +namespace ballistica { + +// Ignore signed bitwise stuff; python macros do it quite a bit. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" + +auto PyButtonWidget(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("buttonwidget"); + PyObject* size_obj = Py_None; + PyObject* pos_obj = Py_None; + PyObject* label_obj = Py_None; + PyObject* parent_obj = Py_None; + PyObject* edit_obj = Py_None; + ContainerWidget* parent_widget = nullptr; + PyObject* on_activate_call_obj = Py_None; + PyObject* color_obj = Py_None; + PyObject* down_widget_obj = Py_None; + Widget* down_widget = nullptr; + PyObject* up_widget_obj = Py_None; + Widget* up_widget = nullptr; + PyObject* left_widget_obj = Py_None; + Widget* left_widget = nullptr; + PyObject* right_widget_obj = Py_None; + Widget* right_widget = nullptr; + PyObject* texture_obj = Py_None; + PyObject* tint_texture_obj = Py_None; + PyObject* text_scale_obj = Py_None; + PyObject* textcolor_obj = Py_None; + PyObject* enable_sound_obj = Py_None; + PyObject* model_transparent_obj = Py_None; + PyObject* model_opaque_obj = Py_None; + PyObject* repeat_obj = Py_None; + PyObject* scale_obj = Py_None; + PyObject* transition_delay_obj = Py_None; + PyObject* on_select_call_obj = Py_None; + PyObject* button_type_obj = Py_None; + PyObject* extra_touch_border_scale_obj = Py_None; + PyObject* selectable_obj = Py_None; + PyObject* show_buffer_top_obj = Py_None; + PyObject* icon_obj = Py_None; + PyObject* iconscale_obj = Py_None; + PyObject* icon_tint_obj = Py_None; + PyObject* icon_color_obj = Py_None; + PyObject* autoselect_obj = Py_None; + PyObject* mask_texture_obj = Py_None; + PyObject* tint_color_obj = Py_None; + PyObject* tint2_color_obj = Py_None; + PyObject* text_flatness_obj = Py_None; + PyObject* text_res_scale_obj = Py_None; + PyObject* enabled_obj = Py_None; + static const char* kwlist[] = {"edit", + "parent", + "size", + "position", + "on_activate_call", + "label", + "color", + "down_widget", + "up_widget", + "left_widget", + "right_widget", + "texture", + "text_scale", + "textcolor", + "enable_sound", + "model_transparent", + "model_opaque", + "repeat", + "scale", + "transition_delay", + "on_select_call", + "button_type", + "extra_touch_border_scale", + "selectable", + "show_buffer_top", + "icon", + "iconscale", + "icon_tint", + "icon_color", + "autoselect", + "mask_texture", + "tint_texture", + "tint_color", + "tint2_color", + "text_flatness", + "text_res_scale", + "enabled", + nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "|OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", + const_cast(kwlist), &edit_obj, &parent_obj, &size_obj, + &pos_obj, &on_activate_call_obj, &label_obj, &color_obj, + &down_widget_obj, &up_widget_obj, &left_widget_obj, &right_widget_obj, + &texture_obj, &text_scale_obj, &textcolor_obj, &enable_sound_obj, + &model_transparent_obj, &model_opaque_obj, &repeat_obj, &scale_obj, + &transition_delay_obj, &on_select_call_obj, &button_type_obj, + &extra_touch_border_scale_obj, &selectable_obj, &show_buffer_top_obj, + &icon_obj, &iconscale_obj, &icon_tint_obj, &icon_color_obj, + &autoselect_obj, &mask_texture_obj, &tint_texture_obj, + &tint_color_obj, &tint2_color_obj, &text_flatness_obj, + &text_res_scale_obj, &enabled_obj)) + return nullptr; + + if (!g_game->IsInUIContext()) { + throw Exception( + "This must be called within the UI context (see ba.Context docs)", + PyExcType::kContext); + } + ScopedSetContext cp(g_game->GetUIContextTarget()); + + // grab the edited widget or create a new one --------------------- + Object::Ref b; + if (edit_obj != Py_None) { + b = dynamic_cast(Python::GetPyWidget(edit_obj)); + if (!b.exists()) { + throw Exception("Invalid or nonexistent widget.", + PyExcType::kWidgetNotFound); + } + } else { + parent_widget = + parent_obj == Py_None + ? g_ui->screen_root_widget() + : dynamic_cast(Python::GetPyWidget(parent_obj)); + if (parent_widget == nullptr) { + throw Exception("Parent widget nonexistent or not a container.", + PyExcType::kWidgetNotFound); + } + b = Object::New(); + } + + // set applicable values ---------------------------- + if (label_obj != Py_None) { + b->SetText(Python::GetPyString(label_obj)); + } + if (on_activate_call_obj != Py_None) { + b->set_on_activate_call(on_activate_call_obj); + } + + if (down_widget_obj != Py_None) { + down_widget = Python::GetPyWidget(down_widget_obj); + if (!down_widget) { + throw Exception("Invalid down widget.", PyExcType::kWidgetNotFound); + } + b->set_down_widget(down_widget); + } + if (up_widget_obj != Py_None) { + up_widget = Python::GetPyWidget(up_widget_obj); + if (!up_widget) { + throw Exception("Invalid up widget.", PyExcType::kWidgetNotFound); + } + b->set_up_widget(up_widget); + } + if (autoselect_obj != Py_None) { + b->set_auto_select(Python::GetPyBool(autoselect_obj)); + } + if (left_widget_obj != Py_None) { + left_widget = Python::GetPyWidget(left_widget_obj); + if (!left_widget) { + throw Exception("Invalid left widget.", PyExcType::kWidgetNotFound); + } + b->set_left_widget(left_widget); + } + if (right_widget_obj != Py_None) { + right_widget = Python::GetPyWidget(right_widget_obj); + if (!right_widget) { + throw Exception("Invalid right widget.", PyExcType::kWidgetNotFound); + } + b->set_right_widget(right_widget); + } + if (model_transparent_obj != Py_None) { + b->SetModelTransparent(Python::GetPyModel(model_transparent_obj)); + } + if (show_buffer_top_obj != Py_None) { + b->set_show_buffer_top(Python::GetPyFloat(show_buffer_top_obj)); + } + if (model_opaque_obj != Py_None) { + b->SetModelOpaque(Python::GetPyModel(model_opaque_obj)); + } + if (on_select_call_obj != Py_None) { + b->SetOnSelectCall(on_select_call_obj); + } + if (selectable_obj != Py_None) { + b->set_selectable(Python::GetPyBool(selectable_obj)); + } + if (size_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(size_obj); + b->set_width(p.x); + b->set_height(p.y); + } + if (pos_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(pos_obj); + b->set_translate(p.x, p.y); + } + if (scale_obj != Py_None) { + b->set_scale(Python::GetPyFloat(scale_obj)); + } + if (iconscale_obj != Py_None) { + b->set_icon_scale(Python::GetPyFloat(iconscale_obj)); + } + if (icon_tint_obj != Py_None) { + b->set_icon_tint(Python::GetPyFloat(icon_tint_obj)); + } + if (icon_color_obj != Py_None) { + std::vector c = Python::GetPyFloats(icon_color_obj); + if (c.size() != 3 && c.size() != 4) { + throw Exception("Expected 3 or 4 floats for icon_color.", + PyExcType::kValue); + } + b->set_icon_color(c[0], c[1], c[2], (c.size() > 3) ? c[3] : 1.0f); + } + if (extra_touch_border_scale_obj != Py_None) { + b->set_extra_touch_border_scale( + Python::GetPyFloat(extra_touch_border_scale_obj)); + } + if (texture_obj != Py_None) { + b->SetTexture(Python::GetPyTexture(texture_obj)); + } + if (mask_texture_obj != Py_None) { + b->SetMaskTexture(Python::GetPyTexture(mask_texture_obj)); + } + if (tint_texture_obj != Py_None) { + b->SetTintTexture(Python::GetPyTexture(tint_texture_obj)); + } + if (icon_obj != Py_None) { + b->SetIcon(Python::GetPyTexture(icon_obj)); + } + if (button_type_obj != Py_None) { + std::string button_type = Python::GetPyString(button_type_obj); + if (button_type == "back") { + b->set_style(ButtonWidget::Style::kBack); + } else if (button_type == "backSmall") { + b->set_style(ButtonWidget::Style::kBackSmall); + } else if (button_type == "regular") { + b->set_style(ButtonWidget::Style::kRegular); + } else if (button_type == "square") { + b->set_style(ButtonWidget::Style::kSquare); + } else if (button_type == "tab") { + b->set_style(ButtonWidget::Style::kTab); + } else { + throw Exception("Invalid button type: " + button_type + ".", + PyExcType::kValue); + } + } + if (repeat_obj != Py_None) { + b->set_repeat(Python::GetPyBool(repeat_obj)); + } + if (color_obj != Py_None) { + std::vector c = Python::GetPyFloats(color_obj); + if (c.size() != 3) { + throw Exception("Expected 3 floats for color.", PyExcType::kValue); + } + b->SetColor(c[0], c[1], c[2]); + } + if (textcolor_obj != Py_None) { + std::vector c = Python::GetPyFloats(textcolor_obj); + if (c.size() != 3 && c.size() != 4) { + throw Exception("Expected 3 or 4 floats for textcolor.", + PyExcType::kValue); + } + b->set_text_color(c[0], c[1], c[2], (c.size() > 3) ? c[3] : 1.0f); + } + if (tint_color_obj != Py_None) { + std::vector c = Python::GetPyFloats(tint_color_obj); + if (c.size() != 3) { + throw Exception("Expected 3 floats for tint_color.", PyExcType::kValue); + } + b->set_tint_color(c[0], c[1], c[2]); + } + if (tint2_color_obj != Py_None) { + std::vector c = Python::GetPyFloats(tint2_color_obj); + if (c.size() != 3) { + throw Exception("Expected 3 floats for tint2_color.", PyExcType::kValue); + } + b->set_tint2_color(c[0], c[1], c[2]); + } + if (text_flatness_obj != Py_None) { + b->set_text_flatness(Python::GetPyFloat(text_flatness_obj)); + } + if (text_scale_obj != Py_None) { + b->set_text_scale(Python::GetPyFloat(text_scale_obj)); + } + if (enable_sound_obj != Py_None) { + b->set_enable_sound(Python::GetPyBool(enable_sound_obj)); + } + if (transition_delay_obj != Py_None) { + // We accept this as seconds; widget takes milliseconds. +#if BA_TEST_BUILD + g_python->TimeFormatCheck(TimeFormat::kSeconds, transition_delay_obj); +#endif + b->set_transition_delay(1000.0f * Python::GetPyFloat(transition_delay_obj)); + } + if (text_res_scale_obj != Py_None) { + b->SetTextResScale(Python::GetPyFloat(text_res_scale_obj)); + } + if (enabled_obj != Py_None) { + b->set_enabled(Python::GetPyBool(selectable_obj)); + } + + // If making a new widget add it at the end. + if (edit_obj == Py_None) { + g_ui->AddWidget(b.get(), parent_widget); + } + + return b->NewPyRef(); + + BA_PYTHON_CATCH; +} + +auto PyCheckBoxWidget(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("checkboxwidget"); + PyObject* size_obj = Py_None; + PyObject* pos_obj = Py_None; + PyObject* text_obj = Py_None; + PyObject* value_obj = Py_None; + PyObject* on_value_change_call_obj = Py_None; + PyObject* on_select_call_obj = Py_None; + PyObject* scale_obj = Py_None; + PyObject* is_radio_button_obj = Py_None; + PyObject* maxwidth_obj = Py_None; + PyObject* parent_obj = Py_None; + PyObject* edit_obj = Py_None; + ContainerWidget* parent_widget = nullptr; + PyObject* text_scale_obj = Py_None; + PyObject* textcolor_obj = Py_None; + PyObject* autoselect_obj = Py_None; + PyObject* color_obj = Py_None; + + static const char* kwlist[] = {"edit", + "parent", + "size", + "position", + "text", + "value", + "on_value_change_call", + "on_select_call", + "text_scale", + "textcolor", + "scale", + "is_radio_button", + "maxwidth", + "autoselect", + "color", + nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "|OOOOOOOOOOOOOOO", const_cast(kwlist), + &edit_obj, &parent_obj, &size_obj, &pos_obj, &text_obj, &value_obj, + &on_value_change_call_obj, &on_select_call_obj, &text_scale_obj, + &textcolor_obj, &scale_obj, &is_radio_button_obj, &maxwidth_obj, + &autoselect_obj, &color_obj)) { + return nullptr; + } + + if (!g_game->IsInUIContext()) { + throw Exception( + "This must be called within the UI context (see ba.Context docs).", + PyExcType::kContext); + } + ScopedSetContext cp(g_game->GetUIContextTarget()); + + // grab the edited widget or create a new one --------------------- + Object::Ref widget; + if (edit_obj != Py_None) { + widget = dynamic_cast(Python::GetPyWidget(edit_obj)); + if (!widget.exists()) { + throw Exception("Invalid or nonexistent widget.", + PyExcType::kWidgetNotFound); + } + } else { + parent_widget = + parent_obj == Py_None + ? g_ui->screen_root_widget() + : dynamic_cast(Python::GetPyWidget(parent_obj)); + if (parent_widget == nullptr) { + throw Exception("Parent widget nonexistent or not a container.", + PyExcType::kWidgetNotFound); + } + widget = Object::New(); + } + + // set applicable values ---------------------------- + if (size_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(size_obj); + widget->SetWidth(p.x); + widget->SetHeight(p.y); + } + if (pos_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(pos_obj); + widget->set_translate(p.x, p.y); + } + if (autoselect_obj != Py_None) { + widget->set_auto_select(Python::GetPyBool(autoselect_obj)); + } + if (text_obj != Py_None) { + widget->SetText(Python::GetPyString(text_obj)); + } + if (value_obj != Py_None) { + widget->SetValue(Python::GetPyBool(value_obj)); + } + if (color_obj != Py_None) { + std::vector c = Python::GetPyFloats(color_obj); + if (c.size() != 3) + throw Exception("Expected 3 floats for color.", PyExcType::kValue); + widget->set_color(c[0], c[1], c[2]); + } + if (maxwidth_obj != Py_None) { + widget->SetMaxWidth(Python::GetPyFloat(maxwidth_obj)); + } + if (is_radio_button_obj != Py_None) { + widget->SetIsRadioButton(Python::GetPyBool(is_radio_button_obj)); + } + if (scale_obj != Py_None) { + widget->set_scale(Python::GetPyFloat(scale_obj)); + } + if (on_value_change_call_obj != Py_None) { + widget->SetOnValueChangeCall(on_value_change_call_obj); + } + if (on_select_call_obj != Py_None) { + widget->SetOnSelectCall(on_select_call_obj); + } + if (text_scale_obj != Py_None) { + widget->SetTextScale(Python::GetPyFloat(text_scale_obj)); + } + if (textcolor_obj != Py_None) { + std::vector c = Python::GetPyFloats(textcolor_obj); + if (c.size() != 3 && c.size() != 4) { + throw Exception("Expected 3 or 4 float values for textcolor.", + PyExcType::kValue); + } + if (c.size() == 3) { + widget->set_text_color(c[0], c[1], c[2], 1.0f); + } else { + widget->set_text_color(c[0], c[1], c[2], c[3]); + } + } + + // if making a new widget add it at the end + if (edit_obj == Py_None) { + g_ui->AddWidget(widget.get(), parent_widget); + } + + return widget->NewPyRef(); + + BA_PYTHON_CATCH; +} + +auto PyImageWidget(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("imagewidget"); + PyObject* size_obj = Py_None; + PyObject* pos_obj = Py_None; + PyObject* texture_obj = Py_None; + PyObject* tint_texture_obj = Py_None; + ContainerWidget* parent_widget = nullptr; + PyObject* parent_obj = Py_None; + PyObject* edit_obj = Py_None; + PyObject* color_obj = Py_None; + PyObject* tint_color_obj = Py_None; + PyObject* tint2_color_obj = Py_None; + PyObject* opacity_obj = Py_None; + PyObject* model_transparent_obj = Py_None; + PyObject* model_opaque_obj = Py_None; + PyObject* has_alpha_channel_obj = Py_None; + PyObject* transition_delay_obj = Py_None; + PyObject* draw_controller_obj = Py_None; + PyObject* tilt_scale_obj = Py_None; + PyObject* mask_texture_obj = Py_None; + PyObject* radial_amount_obj = Py_None; + + static const char* kwlist[] = {"edit", + "parent", + "size", + "position", + "color", + "texture", + "opacity", + "model_transparent", + "model_opaque", + "has_alpha_channel", + "tint_texture", + "tint_color", + "transition_delay", + "draw_controller", + "tint2_color", + "tilt_scale", + "mask_texture", + "radial_amount", + nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "|OOOOOOOOOOOOOOOOOO", const_cast(kwlist), + &edit_obj, &parent_obj, &size_obj, &pos_obj, &color_obj, &texture_obj, + &opacity_obj, &model_transparent_obj, &model_opaque_obj, + &has_alpha_channel_obj, &tint_texture_obj, &tint_color_obj, + &transition_delay_obj, &draw_controller_obj, &tint2_color_obj, + &tilt_scale_obj, &mask_texture_obj, &radial_amount_obj)) + return nullptr; + + if (!g_game->IsInUIContext()) { + throw Exception( + "This must be called within the UI context (see ba.Context docs).", + PyExcType::kContext); + } + ScopedSetContext cp(g_game->GetUIContextTarget()); + + // grab the edited widget or create a new one --------------------- + Object::Ref b; + if (edit_obj != Py_None) { + b = dynamic_cast(Python::GetPyWidget(edit_obj)); + if (!b.exists()) + throw Exception("Invalid or nonexistent widget.", + PyExcType::kWidgetNotFound); + } else { + parent_widget = + parent_obj == Py_None + ? g_ui->screen_root_widget() + : dynamic_cast(Python::GetPyWidget(parent_obj)); + if (parent_widget == nullptr) { + throw Exception("Parent widget nonexistent or not a container.", + PyExcType::kWidgetNotFound); + } + b = Object::New(); + } + if (size_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(size_obj); + b->set_width(p.x); + b->set_height(p.y); + } + if (texture_obj != Py_None) { + b->SetTexture(Python::GetPyTexture(texture_obj)); + } + if (tint_texture_obj != Py_None) { + b->SetTintTexture(Python::GetPyTexture(tint_texture_obj)); + } + if (mask_texture_obj != Py_None) { + b->SetMaskTexture(Python::GetPyTexture(mask_texture_obj)); + } + if (model_opaque_obj != Py_None) { + b->SetModelOpaque(Python::GetPyModel(model_opaque_obj)); + } + if (model_transparent_obj != Py_None) { + b->SetModelTransparent(Python::GetPyModel(model_transparent_obj)); + } + if (draw_controller_obj != Py_None) { + auto* dcw = Python::GetPyWidget(draw_controller_obj); + if (!dcw) { + throw Exception("Invalid or nonexistent draw-controller widget.", + PyExcType::kWidgetNotFound); + } + b->set_draw_control_parent(dcw); + } + if (has_alpha_channel_obj != Py_None) { + b->set_has_alpha_channel(Python::GetPyBool(has_alpha_channel_obj)); + } + if (opacity_obj != Py_None) { + b->set_opacity(Python::GetPyFloat(opacity_obj)); + } + if (radial_amount_obj != Py_None) { + b->set_radial_amount(Python::GetPyFloat(radial_amount_obj)); + } + if (pos_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(pos_obj); + b->set_translate(p.x, p.y); + } + if (transition_delay_obj != Py_None) { + // We accept this as seconds; widget takes milliseconds. +#if BA_TEST_BUILD + g_python->TimeFormatCheck(TimeFormat::kSeconds, transition_delay_obj); +#endif + b->set_transition_delay(1000.0f * Python::GetPyFloat(transition_delay_obj)); + } + if (color_obj != Py_None) { + std::vector c = Python::GetPyFloats(color_obj); + if (c.size() != 3) { + throw Exception("Expected 3 floats for color.", PyExcType::kValue); + } + b->set_color(c[0], c[1], c[2]); + } + if (tint_color_obj != Py_None) { + std::vector c = Python::GetPyFloats(tint_color_obj); + if (c.size() != 3) { + throw Exception("Expected 3 floats for tint_color.", PyExcType::kValue); + } + b->set_tint_color(c[0], c[1], c[2]); + } + if (tint2_color_obj != Py_None) { + std::vector c = Python::GetPyFloats(tint2_color_obj); + if (c.size() != 3) { + throw Exception("Expected 3 floats for tint2_color.", PyExcType::kValue); + } + b->set_tint2_color(c[0], c[1], c[2]); + } + if (tilt_scale_obj != Py_None) { + b->set_tilt_scale(Python::GetPyFloat(tilt_scale_obj)); + } + + // if making a new widget add it at the end + if (edit_obj == Py_None) { + g_ui->AddWidget(b.get(), parent_widget); + } + + return b->NewPyRef(); + BA_PYTHON_CATCH; +} + +auto PyColumnWidget(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + + Platform::SetLastPyCall("columnwidget"); + + PyObject* size_obj{Py_None}; + PyObject* pos_obj{Py_None}; + PyObject* background_obj{Py_None}; + PyObject* selected_child_obj{Py_None}; + PyObject* visible_child_obj{Py_None}; + PyObject* single_depth_obj{Py_None}; + PyObject* print_list_exit_instructions_obj{Py_None}; + PyObject* parent_obj{Py_None}; + PyObject* edit_obj{Py_None}; + ContainerWidget* parent_widget{}; + PyObject* left_border_obj{Py_None}; + PyObject* top_border_obj{Py_None}; + PyObject* bottom_border_obj{Py_None}; + PyObject* selection_loops_to_parent_obj{Py_None}; + PyObject* border_obj{Py_None}; + PyObject* margin_obj{Py_None}; + PyObject* claims_left_right_obj{Py_None}; + PyObject* claims_tab_obj{Py_None}; + static const char* kwlist[] = {"edit", + "parent", + "size", + "position", + "background", + "selected_child", + "visible_child", + "single_depth", + "print_list_exit_instructions", + "left_border", + "top_border", + "bottom_border", + "selection_loops_to_parent", + "border", + "margin", + "claims_left_right", + "claims_tab", + nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "|OOOOOOOOOOOOOOOOO", const_cast(kwlist), + &edit_obj, &parent_obj, &size_obj, &pos_obj, &background_obj, + &selected_child_obj, &visible_child_obj, &single_depth_obj, + &print_list_exit_instructions_obj, &left_border_obj, &top_border_obj, + &bottom_border_obj, &selection_loops_to_parent_obj, &border_obj, + &margin_obj, &claims_left_right_obj, &claims_tab_obj)) + return nullptr; + + if (!g_game->IsInUIContext()) { + throw Exception( + "This must be called within the UI context (see ba.Context " + "docs).", + PyExcType::kContext); + } + // if (!g_game->IsInUIContext()) { BA_LOG_PYTHON_TRACE("ERROR: This should be + // called within the UI context (see ba.Context docs)");} + ScopedSetContext cp(g_game->GetUIContextTarget()); + + // grab the edited widget or create a new one --------------------- + Object::Ref widget; + if (edit_obj != Py_None) { + widget = dynamic_cast(Python::GetPyWidget(edit_obj)); + if (!widget.exists()) { + throw Exception("Invalid or nonexistent widget.", + PyExcType::kWidgetNotFound); + } + } else { + parent_widget = + parent_obj == Py_None + ? g_ui->screen_root_widget() + : dynamic_cast(Python::GetPyWidget(parent_obj)); + if (!parent_widget) { + throw Exception("Invalid or nonexistent parent widget.", + PyExcType::kWidgetNotFound); + } + widget = Object::New(); + } + + // Set applicable values ---------------------------- + if (size_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(size_obj); + widget->SetWidth(p.x); + widget->SetHeight(p.y); + } + if (single_depth_obj != Py_None) { + widget->set_single_depth(Python::GetPyBool(single_depth_obj)); + } + if (pos_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(pos_obj); + widget->set_translate(p.x, p.y); + } + if (left_border_obj != Py_None) { + widget->set_left_border(Python::GetPyFloat(left_border_obj)); + } + if (top_border_obj != Py_None) { + widget->set_top_border(Python::GetPyFloat(top_border_obj)); + } + if (border_obj != Py_None) { + widget->set_border(Python::GetPyFloat(border_obj)); + } + if (margin_obj != Py_None) { + widget->set_margin(Python::GetPyFloat(margin_obj)); + } + if (bottom_border_obj != Py_None) { + widget->set_bottom_border(Python::GetPyFloat(bottom_border_obj)); + } + if (print_list_exit_instructions_obj != Py_None) { + widget->set_should_print_list_exit_instructions( + Python::GetPyBool(print_list_exit_instructions_obj)); + } + if (background_obj != Py_None) { + widget->set_background(Python::GetPyBool(background_obj)); + } + if (selected_child_obj != Py_None) { + widget->SelectWidget(Python::GetPyWidget(selected_child_obj)); + } + if (visible_child_obj != Py_None) { + widget->ShowWidget(Python::GetPyWidget(visible_child_obj)); + } + if (selection_loops_to_parent_obj != Py_None) { + widget->set_selection_loops_to_parent( + Python::GetPyBool(selection_loops_to_parent_obj)); + } + if (claims_left_right_obj != Py_None) { + widget->set_claims_left_right(Python::GetPyBool(claims_left_right_obj)); + } + if (claims_tab_obj != Py_None) { + widget->set_claims_tab(Python::GetPyBool(claims_tab_obj)); + } + + // if making a new widget add it at the end + if (edit_obj == Py_None) { + g_ui->AddWidget(widget.get(), parent_widget); + } + + return widget->NewPyRef(); + + BA_PYTHON_CATCH; +} + +auto PyContainerWidget(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("containerwidget"); + PyObject* size_obj = Py_None; + PyObject* pos_obj = Py_None; + PyObject* background_obj = Py_None; + PyObject* selected_child_obj = Py_None; + PyObject* transition_obj = Py_None; + PyObject* cancel_button_obj = Py_None; + PyObject* start_button_obj = Py_None; + PyObject* root_selectable_obj = Py_None; + PyObject* on_activate_call_obj = Py_None; + PyObject* claims_left_right_obj = Py_None; + PyObject* claims_up_down_obj = Py_None; + PyObject* claims_tab_obj = Py_None; + PyObject* selection_loops_obj = Py_None; + PyObject* selection_loops_to_parent_obj = Py_None; + PyObject* scale_obj = Py_None; + PyObject* on_outside_click_call_obj = Py_None; + PyObject* print_list_exit_instructions_obj = Py_None; + PyObject* single_depth_obj = Py_None; + PyObject* visible_child_obj = Py_None; + PyObject* stack_offset_obj = Py_None; + PyObject* scale_origin_stack_offset_obj = Py_None; + PyObject* color_obj = Py_None; + PyObject* on_cancel_call_obj = Py_None; + PyObject* click_activate_obj = Py_None; + PyObject* always_highlight_obj = Py_None; + PyObject* parent_obj = Py_None; + ContainerWidget* parent_widget; + PyObject* edit_obj = Py_None; + PyObject* selectable_obj = Py_None; + PyObject* toolbar_visibility_obj = Py_None; + PyObject* on_select_call_obj = Py_None; + PyObject* claim_outside_clicks_obj = Py_None; + + static const char* kwlist[] = {"edit", + "parent", + "size", + "position", + "background", + "selected_child", + "transition", + "cancel_button", + "start_button", + "root_selectable", + "on_activate_call", + "claims_left_right", + "claims_tab", + "selection_loops", + "selection_loops_to_parent", + "scale", + "on_outside_click_call", + "single_depth", + "visible_child", + "stack_offset", + "color", + "on_cancel_call", + "print_list_exit_instructions", + "click_activate", + "always_highlight", + "selectable", + "scale_origin_stack_offset", + "toolbar_visibility", + "on_select_call", + "claim_outside_clicks", + "claims_up_down", + nullptr}; + + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "|OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", + const_cast(kwlist), &edit_obj, &parent_obj, &size_obj, + &pos_obj, &background_obj, &selected_child_obj, &transition_obj, + &cancel_button_obj, &start_button_obj, &root_selectable_obj, + &on_activate_call_obj, &claims_left_right_obj, &claims_tab_obj, + &selection_loops_obj, &selection_loops_to_parent_obj, &scale_obj, + &on_outside_click_call_obj, &single_depth_obj, &visible_child_obj, + &stack_offset_obj, &color_obj, &on_cancel_call_obj, + &print_list_exit_instructions_obj, &click_activate_obj, + &always_highlight_obj, &selectable_obj, + &scale_origin_stack_offset_obj, &toolbar_visibility_obj, + &on_select_call_obj, &claim_outside_clicks_obj, + &claims_up_down_obj)) { + return nullptr; + } + + if (!g_game->IsInUIContext()) + throw Exception( + "This must be called within the UI context (see ba.Context docs).", + PyExcType::kContext); + ScopedSetContext cp(g_game->GetUIContextTarget()); + + // grab the edited widget or create a new one --------------------- + Object::Ref widget; + if (edit_obj != Py_None) { + widget = dynamic_cast(Python::GetPyWidget(edit_obj)); + if (!widget.exists()) { + throw Exception("Invalid or nonexistent widget.", + PyExcType::kWidgetNotFound); + } + } else { + if (parent_obj == Py_None) { + BA_PRECONDITION(g_ui && g_ui->screen_root_widget() != nullptr); + } + parent_widget = + parent_obj == Py_None + ? g_ui->screen_root_widget() + : dynamic_cast(Python::GetPyWidget(parent_obj)); + if (!parent_widget) { + throw Exception("Invalid or nonexistent parent widget.", + PyExcType::kWidgetNotFound); + } + widget = Object::New(); + g_ui->AddWidget(widget.get(), parent_widget); + } + + // Set applicable values. + if (size_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(size_obj); + widget->SetWidth(p.x); + widget->SetHeight(p.y); + } + if (pos_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(pos_obj); + widget->set_translate(p.x, p.y); + } + if (on_cancel_call_obj != Py_None) { + widget->SetOnCancelCall(on_cancel_call_obj); + } + if (scale_obj != Py_None) { + widget->set_scale(Python::GetPyFloat(scale_obj)); + } + if (on_select_call_obj != Py_None) { + widget->SetOnSelectCall(on_select_call_obj); + } + if (selectable_obj != Py_None) { + widget->set_selectable(Python::GetPyBool(selectable_obj)); + } + if (single_depth_obj != Py_None) { + widget->set_single_depth(Python::GetPyBool(single_depth_obj)); + } + if (stack_offset_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(stack_offset_obj); + widget->set_stack_offset(p.x, p.y); + } + if (scale_origin_stack_offset_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(scale_origin_stack_offset_obj); + widget->SetScaleOriginStackOffset(p.x, p.y); + } + if (visible_child_obj != Py_None) { + widget->ShowWidget(Python::GetPyWidget(visible_child_obj)); + } + if (color_obj != Py_None) { + std::vector c = Python::GetPyFloats(color_obj); + if (c.size() != 3 && c.size() != 4) { + throw Exception("Expected 3 or floats for color.", PyExcType::kValue); + } + if (c.size() == 3) { + widget->set_color(c[0], c[1], c[2], 1.0f); + } else { + widget->set_color(c[0], c[1], c[2], c[3]); + } + } + + if (on_activate_call_obj != Py_None) { + widget->SetOnActivateCall(on_activate_call_obj); + } + + if (on_outside_click_call_obj != Py_None) { + widget->SetOnOutsideClickCall(on_outside_click_call_obj); + } + + if (background_obj != Py_None) { + widget->set_background(Python::GetPyBool(background_obj)); + } + if (root_selectable_obj != Py_None) { + widget->SetRootSelectable(Python::GetPyBool(root_selectable_obj)); + } + if (selected_child_obj != Py_None) { + // special case: passing 0 implies deselect + if (PyLong_Check(selected_child_obj) + && (PyLong_AsLong(selected_child_obj) == 0)) { + widget->SelectWidget(nullptr); + } else { + widget->SelectWidget(Python::GetPyWidget(selected_child_obj)); + } + } + + if (transition_obj != Py_None) { + std::string t = Python::GetPyString(transition_obj); + if (t == "in_left") + widget->SetTransition(ContainerWidget::TRANSITION_IN_LEFT); + else if (t == "in_right") + widget->SetTransition(ContainerWidget::TRANSITION_IN_RIGHT); + else if (t == "out_left") + widget->SetTransition(ContainerWidget::TRANSITION_OUT_LEFT); + else if (t == "out_right") + widget->SetTransition(ContainerWidget::TRANSITION_OUT_RIGHT); + else if (t == "in_scale") + widget->SetTransition(ContainerWidget::TRANSITION_IN_SCALE); + else if (t == "out_scale") + widget->SetTransition(ContainerWidget::TRANSITION_OUT_SCALE); + } + + if (cancel_button_obj != Py_None) { + auto* button_widget = + dynamic_cast(Python::GetPyWidget(cancel_button_obj)); + if (!button_widget) { + throw Exception("Invalid cancel_button.", PyExcType::kWidgetNotFound); + } + widget->SetCancelButton(button_widget); + } + if (start_button_obj != Py_None) { + auto* button_widget = + dynamic_cast(Python::GetPyWidget(start_button_obj)); + if (!button_widget) { + throw Exception("Invalid start_button.", PyExcType::kWidgetNotFound); + } + widget->SetStartButton(button_widget); + } + if (claims_left_right_obj != Py_None) { + widget->set_claims_left_right(Python::GetPyBool(claims_left_right_obj)); + } + if (claims_up_down_obj != Py_None) { + widget->set_claims_up_down(Python::GetPyBool(claims_up_down_obj)); + } + if (claims_tab_obj != Py_None) { + widget->set_claims_tab(Python::GetPyBool(claims_tab_obj)); + } + if (selection_loops_obj != Py_None) { + widget->set_selection_loops(Python::GetPyBool(selection_loops_obj)); + } + if (selection_loops_to_parent_obj != Py_None) { + widget->set_selection_loops_to_parent( + Python::GetPyBool(selection_loops_to_parent_obj)); + } + if (print_list_exit_instructions_obj != Py_None) { + widget->set_should_print_list_exit_instructions( + Python::GetPyBool(print_list_exit_instructions_obj)); + } + if (click_activate_obj != Py_None) { + widget->set_click_activate(Python::GetPyBool(click_activate_obj)); + } + if (always_highlight_obj != Py_None) { + widget->set_always_highlight(Python::GetPyBool(always_highlight_obj)); + } + if (toolbar_visibility_obj != Py_None) { + Widget::ToolbarVisibility val; + std::string sval = Python::GetPyString(toolbar_visibility_obj); + if (sval == "menu_minimal") { + val = Widget::ToolbarVisibility::kMenuMinimal; + } else if (sval == "menu_minimal_no_back") { + val = Widget::ToolbarVisibility::kMenuMinimalNoBack; + } else if (sval == "menu_full") { + val = Widget::ToolbarVisibility::kMenuFull; + } else if (sval == "menu_currency") { + val = Widget::ToolbarVisibility::kMenuCurrency; + } else if (sval == "menu_full_root") { + val = Widget::ToolbarVisibility::kMenuFullRoot; + } else if (sval == "in_game") { + val = Widget::ToolbarVisibility::kInGame; + } else if (sval == "inherit") { + val = Widget::ToolbarVisibility::kInherit; + } else { + throw Exception("Invalid toolbar_visibility: '" + sval + "'.", + PyExcType::kValue); + } + widget->SetToolbarVisibility(val); + } + if (claim_outside_clicks_obj != Py_None) { + widget->set_claims_outside_clicks( + Python::GetPyBool(claim_outside_clicks_obj)); + } + return widget->NewPyRef(); + BA_PYTHON_CATCH; +} + +auto PyRowWidget(PyObject* /* self */, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + + Platform::SetLastPyCall("rowwidget"); + + PyObject* size_obj{Py_None}; + PyObject* pos_obj{Py_None}; + PyObject* background_obj{Py_None}; + PyObject* selected_child_obj{Py_None}; + PyObject* visible_child_obj{Py_None}; + PyObject* parent_obj{Py_None}; + PyObject* edit_obj{Py_None}; + ContainerWidget* parent_widget{}; + PyObject* claims_left_right_obj{Py_None}; + PyObject* claims_tab_obj{Py_None}; + PyObject* selection_loops_to_parent_obj{Py_None}; + + static const char* kwlist[] = {"edit", "parent", + "size", "position", + "background", "selected_child", + "visible_child", "claims_left_right", + "claims_tab", "selection_loops_to_parent", + nullptr}; + + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "|OOOOOOOOOO", const_cast(kwlist), &edit_obj, + &parent_obj, &size_obj, &pos_obj, &background_obj, + &selected_child_obj, &visible_child_obj, &claims_left_right_obj, + &claims_tab_obj, &selection_loops_to_parent_obj)) + return nullptr; + + if (!g_game->IsInUIContext()) + throw Exception( + "This must be called within the UI context (see ba.Context docs).", + PyExcType::kContext); + + // Called within the UI context (see ba.Context docs)");} + ScopedSetContext cp(g_game->GetUIContextTarget()); + + // Grab the edited widget or create a new one. + Object::Ref widget; + if (edit_obj != Py_None) { + widget = dynamic_cast(Python::GetPyWidget(edit_obj)); + if (!widget.exists()) { + throw Exception("Invalid or nonexistent widget.", + PyExcType::kWidgetNotFound); + } + } else { + parent_widget = + parent_obj == Py_None + ? g_ui->screen_root_widget() + : dynamic_cast(Python::GetPyWidget(parent_obj)); + if (!parent_widget) { + throw Exception("invalid or nonexistent parent widget.", + PyExcType::kWidgetNotFound); + } + widget = Object::New(); + } + + // Set applicable values. + if (size_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(size_obj); + widget->SetWidth(p.x); + widget->SetHeight(p.y); + } + if (pos_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(pos_obj); + widget->set_translate(p.x, p.y); + } + + if (background_obj != Py_None) { + widget->set_background(Python::GetPyBool(background_obj)); + } + if (selected_child_obj != Py_None) { + widget->SelectWidget(Python::GetPyWidget(selected_child_obj)); + } + if (visible_child_obj != Py_None) { + widget->ShowWidget(Python::GetPyWidget(visible_child_obj)); + } + if (claims_left_right_obj != Py_None) { + widget->set_claims_left_right(Python::GetPyBool(claims_left_right_obj)); + } + if (claims_tab_obj != Py_None) { + widget->set_claims_tab(Python::GetPyBool(claims_tab_obj)); + } + if (selection_loops_to_parent_obj != Py_None) { + widget->set_selection_loops_to_parent( + Python::GetPyBool(selection_loops_to_parent_obj)); + } + + // If making a new widget, add it to the parent. + if (edit_obj == Py_None) { + g_ui->AddWidget(widget.get(), parent_widget); + } + + return widget->NewPyRef(); + + BA_PYTHON_CATCH; +} + +auto PyScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("scrollwidget"); + PyObject* size_obj{Py_None}; + PyObject* pos_obj{Py_None}; + PyObject* background_obj{Py_None}; + PyObject* selected_child_obj{Py_None}; + PyObject* capture_arrows_obj{Py_None}; + PyObject* on_select_call_obj{Py_None}; + PyObject* parent_obj{Py_None}; + PyObject* edit_obj{Py_None}; + PyObject* center_small_content_obj{Py_None}; + ContainerWidget* parent_widget{}; + PyObject* color_obj{Py_None}; + PyObject* highlight_obj{Py_None}; + PyObject* border_opacity_obj{Py_None}; + PyObject* simple_culling_v_obj{Py_None}; + PyObject* selection_loops_to_parent_obj{Py_None}; + PyObject* claims_left_right_obj{Py_None}; + PyObject* claims_tab_obj{Py_None}; + + static const char* kwlist[] = {"edit", + "parent", + "size", + "position", + "background", + "selected_child", + "capture_arrows", + "on_select_call", + "center_small_content", + "color", + "highlight", + "border_opacity", + "simple_culling_v", + "selection_loops_to_parent", + "claims_left_right", + "claims_tab", + nullptr}; + + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "|OOOOOOOOOOOOOOOO", const_cast(kwlist), + &edit_obj, &parent_obj, &size_obj, &pos_obj, &background_obj, + &selected_child_obj, &capture_arrows_obj, &on_select_call_obj, + ¢er_small_content_obj, &color_obj, &highlight_obj, + &border_opacity_obj, &simple_culling_v_obj, + &selection_loops_to_parent_obj, &claims_left_right_obj, + &claims_tab_obj)) + return nullptr; + + if (!g_game->IsInUIContext()) { + throw Exception( + "This must be called within the UI context (see ba.Context docs).", + PyExcType::kContext); + } + ScopedSetContext cp(g_game->GetUIContextTarget()); + + // Grab the edited widget or create a new one. --------------------- + Object::Ref widget; + if (edit_obj != Py_None) { + widget = dynamic_cast(Python::GetPyWidget(edit_obj)); + if (!widget.exists()) { + throw Exception("Invalid or nonexistent edit widget.", + PyExcType::kWidgetNotFound); + } + } else { + parent_widget = + parent_obj == Py_None + ? g_ui->screen_root_widget() + : dynamic_cast(Python::GetPyWidget(parent_obj)); + if (!parent_widget) { + throw Exception("Invalid or nonexistent parent widget.", + PyExcType::kWidgetNotFound); + } + widget = Object::New(); + } + + // Set applicable values. ---------------------------- + if (size_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(size_obj); + widget->SetWidth(p.x); + widget->SetHeight(p.y); + } + if (pos_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(pos_obj); + widget->set_translate(p.x, p.y); + } + if (highlight_obj != Py_None) { + widget->set_highlight(Python::GetPyBool(highlight_obj)); + } + if (border_opacity_obj != Py_None) { + widget->set_border_opacity(Python::GetPyFloat(border_opacity_obj)); + } + if (on_select_call_obj != Py_None) { + widget->SetOnSelectCall(on_select_call_obj); + } + if (center_small_content_obj != Py_None) { + widget->set_center_small_content( + Python::GetPyBool(center_small_content_obj)); + } + if (color_obj != Py_None) { + std::vector c = Python::GetPyFloats(color_obj); + if (c.size() != 3) { + throw Exception("Expected 3 floats for color.", PyExcType::kValue); + } + widget->set_color(c[0], c[1], c[2]); + } + if (capture_arrows_obj != Py_None) { + widget->set_capture_arrows(Python::GetPyBool(capture_arrows_obj)); + } + if (background_obj != Py_None) { + widget->set_background(Python::GetPyBool(background_obj)); + } + if (simple_culling_v_obj != Py_None) { + widget->set_simple_culling_v(Python::GetPyFloat(simple_culling_v_obj)); + } + if (selected_child_obj != Py_None) { + widget->SelectWidget(Python::GetPyWidget(selected_child_obj)); + } + if (selection_loops_to_parent_obj != Py_None) { + widget->set_selection_loops_to_parent( + Python::GetPyBool(selection_loops_to_parent_obj)); + } + if (claims_left_right_obj != Py_None) { + widget->set_claims_left_right(Python::GetPyBool(claims_left_right_obj)); + } + if (claims_tab_obj != Py_None) { + widget->set_claims_tab(Python::GetPyBool(claims_tab_obj)); + } + + // If making a new widget add it at the end. + if (edit_obj == Py_None) { + g_ui->AddWidget(widget.get(), parent_widget); + } + return widget->NewPyRef(); + + BA_PYTHON_CATCH; +} + +auto PyHScrollWidget(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + + Platform::SetLastPyCall("hscrollwidget"); + + PyObject* size_obj = Py_None; + PyObject* pos_obj = Py_None; + PyObject* background_obj = Py_None; + PyObject* selected_child_obj = Py_None; + PyObject* capture_arrows_obj = Py_None; + PyObject* on_select_call_obj = Py_None; + PyObject* parent_obj = Py_None; + PyObject* edit_obj = Py_None; + PyObject* center_small_content_obj = Py_None; + ContainerWidget* parent_widget = nullptr; + PyObject* color_obj = Py_None; + PyObject* highlight_obj = Py_None; + PyObject* border_opacity_obj = Py_None; + PyObject* simple_culling_h_obj = Py_None; + PyObject* claims_left_right_obj = Py_None; + PyObject* claims_tab_obj = Py_None; + + static const char* kwlist[] = {"edit", + "parent", + "size", + "position", + "background", + "selected_child", + "capture_arrows", + "on_select_call", + "center_small_content", + "color", + "highlight", + "border_opacity", + "simple_culling_h", + "claims_left_right", + "claims_tab", + nullptr}; + + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "|OOOOOOOOOOOOOOO", const_cast(kwlist), + &edit_obj, &parent_obj, &size_obj, &pos_obj, &background_obj, + &selected_child_obj, &capture_arrows_obj, &on_select_call_obj, + ¢er_small_content_obj, &color_obj, &highlight_obj, + &border_opacity_obj, &simple_culling_h_obj, &claims_left_right_obj, + &claims_tab_obj)) + return nullptr; + + if (!g_game->IsInUIContext()) { + throw Exception( + "This must be called within the UI context (see ba.Context docs).", + PyExcType::kContext); + } + ScopedSetContext cp(g_game->GetUIContextTarget()); + + // grab the edited widget or create a new one --------------------- + Object::Ref widget; + if (edit_obj != Py_None) { + widget = dynamic_cast(Python::GetPyWidget(edit_obj)); + if (!widget.exists()) { + throw Exception("Invalid or nonexistent edit widget.", + PyExcType::kWidgetNotFound); + } + } else { + parent_widget = + parent_obj == Py_None + ? g_ui->screen_root_widget() + : dynamic_cast(Python::GetPyWidget(parent_obj)); + if (!parent_widget) { + throw Exception("Invalid or nonexistent parent widget.", + PyExcType::kWidgetNotFound); + } + widget = Object::New(); + } + + // set applicable values ---------------------------- + if (size_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(size_obj); + widget->SetWidth(p.x); + widget->SetHeight(p.y); + } + if (pos_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(pos_obj); + widget->set_translate(p.x, p.y); + } + if (highlight_obj != Py_None) { + widget->set_highlight(Python::GetPyBool(highlight_obj)); + } + if (border_opacity_obj != Py_None) { + widget->setBorderOpacity(Python::GetPyFloat(border_opacity_obj)); + } + if (on_select_call_obj != Py_None) { + widget->SetOnSelectCall(on_select_call_obj); + } + if (center_small_content_obj != Py_None) { + widget->SetCenterSmallContent(Python::GetPyBool(center_small_content_obj)); + } + if (color_obj != Py_None) { + std::vector c = Python::GetPyFloats(color_obj); + if (c.size() != 3) { + throw Exception("Expected 3 floats for color.", PyExcType::kValue); + } + widget->setColor(c[0], c[1], c[2]); + } + if (capture_arrows_obj != Py_None) { + widget->set_capture_arrows(Python::GetPyBool(capture_arrows_obj)); + } + if (background_obj != Py_None) { + widget->set_background(Python::GetPyBool(background_obj)); + } + if (simple_culling_h_obj != Py_None) { + widget->set_simple_culling_h(Python::GetPyFloat(simple_culling_h_obj)); + } + if (selected_child_obj != Py_None) { + widget->SelectWidget(Python::GetPyWidget(selected_child_obj)); + } + if (claims_left_right_obj != Py_None) { + widget->set_claims_left_right(Python::GetPyBool(claims_left_right_obj)); + } + if (claims_tab_obj != Py_None) { + widget->set_claims_tab(Python::GetPyBool(claims_tab_obj)); + } + + // if making a new widget add it at the end + if (edit_obj == Py_None) { + g_ui->AddWidget(widget.get(), parent_widget); + } + return widget->NewPyRef(); + + BA_PYTHON_CATCH; +} + +auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("textwidget"); + PyObject* size_obj = Py_None; + PyObject* pos_obj = Py_None; + PyObject* text_obj = Py_None; + PyObject* v_align_obj = Py_None; + PyObject* h_align_obj = Py_None; + PyObject* editable_obj = Py_None; + PyObject* padding_obj = Py_None; + PyObject* on_return_press_call_obj = Py_None; + PyObject* on_activate_call_obj = Py_None; + PyObject* selectable_obj = Py_None; + PyObject* max_chars_obj = Py_None; + PyObject* color_obj = Py_None; + PyObject* click_activate_obj = Py_None; + PyObject* on_select_call_obj = Py_None; + PyObject* maxwidth_obj = Py_None; + PyObject* max_height_obj = Py_None; + PyObject* scale_obj = Py_None; + PyObject* corner_scale_obj = Py_None; + PyObject* always_highlight_obj = Py_None; + PyObject* draw_controller_obj = Py_None; + PyObject* description_obj = Py_None; + PyObject* transition_delay_obj = Py_None; + PyObject* flatness_obj = Py_None; + PyObject* shadow_obj = Py_None; + PyObject* big_obj = Py_None; + PyObject* parent_obj = Py_None; + ContainerWidget* parent_widget = nullptr; + PyObject* edit_obj = Py_None; + PyObject* query_obj = Py_None; + PyObject* autoselect_obj = Py_None; + PyObject* rotate_obj = Py_None; + PyObject* enabled_obj = Py_None; + PyObject* force_internal_editing_obj = Py_None; + PyObject* always_show_carat_obj = Py_None; + PyObject* extra_touch_border_scale_obj = Py_None; + PyObject* res_scale_obj = Py_None; + + static const char* kwlist[] = {"edit", + "parent", + "size", + "position", + "text", + "v_align", + "h_align", + "editable", + "padding", + "on_return_press_call", + "on_activate_call", + "selectable", + "query", + "max_chars", + "color", + "click_activate", + "on_select_call", + "always_highlight", + "draw_controller", + "scale", + "corner_scale", + "description", + "transition_delay", + "maxwidth", + "max_height", + "flatness", + "shadow", + "autoselect", + "rotate", + "enabled", + "force_internal_editing", + "always_show_carat", + "big", + "extra_touch_border_scale", + "res_scale", + nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "|OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", + const_cast(kwlist), &edit_obj, &parent_obj, &size_obj, + &pos_obj, &text_obj, &v_align_obj, &h_align_obj, &editable_obj, + &padding_obj, &on_return_press_call_obj, &on_activate_call_obj, + &selectable_obj, &query_obj, &max_chars_obj, &color_obj, + &click_activate_obj, &on_select_call_obj, &always_highlight_obj, + &draw_controller_obj, &scale_obj, &corner_scale_obj, &description_obj, + &transition_delay_obj, &maxwidth_obj, &max_height_obj, &flatness_obj, + &shadow_obj, &autoselect_obj, &rotate_obj, &enabled_obj, + &force_internal_editing_obj, &always_show_carat_obj, &big_obj, + &extra_touch_border_scale_obj, &res_scale_obj)) + return nullptr; + + if (!g_game->IsInUIContext()) + throw Exception( + "This must be called within the UI context (see ba.Context docs).", + PyExcType::kContext); + // if (!g_game->IsInUIContext()) { BA_LOG_PYTHON_TRACE("ERROR: This should be + // called within the UI context (see ba.Context docs)");} + ScopedSetContext cp(g_game->GetUIContextTarget()); + + // grab the edited widget or create a new one --------------------- + Object::Ref widget; + if (query_obj != Py_None) { + widget = dynamic_cast(Python::GetPyWidget(query_obj)); + if (!widget.exists()) { + throw Exception("Invalid or nonexistent widget.", + PyExcType::kWidgetNotFound); + } + return PyUnicode_FromString(widget->text_raw().c_str()); + } + if (edit_obj != Py_None) { + widget = dynamic_cast(Python::GetPyWidget(edit_obj)); + if (!widget.exists()) { + throw Exception("Invalid or nonexistent widget.", + PyExcType::kWidgetNotFound); + } + } else { + parent_widget = + parent_obj == Py_None + ? g_ui->screen_root_widget() + : dynamic_cast(Python::GetPyWidget(parent_obj)); + if (!parent_widget) { + throw Exception("Invalid or nonexistent parent widget.", + PyExcType::kWidgetNotFound); + } + widget = Object::New(); + } + + // Set applicable values ---------------------------- + if (max_chars_obj != Py_None) { + widget->set_max_chars( + static_cast_check_fit(Python::GetPyInt64(max_chars_obj))); + } + if (size_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(size_obj); + widget->SetWidth(p.x); + widget->SetHeight(p.y); + } + if (description_obj != Py_None) { + // FIXME - compiling Lstr values to flat strings before passing them in; + // we should probably extend TextWidget to handle this internally, but + // punting on that for now.. + widget->set_description(g_game->CompileResourceString( + Python::GetPyString(description_obj), "textwidget set desc")); + } + if (autoselect_obj != Py_None) { + widget->set_auto_select(Python::GetPyBool(autoselect_obj)); + } + if (transition_delay_obj != Py_None) { + // we accept this as seconds; widget takes milliseconds +#if BA_TEST_BUILD + g_python->TimeFormatCheck(TimeFormat::kSeconds, transition_delay_obj); +#endif + widget->set_transition_delay(1000.0f + * Python::GetPyFloat(transition_delay_obj)); + } + if (enabled_obj != Py_None) { + widget->SetEnabled(Python::GetPyBool(enabled_obj)); + } + if (always_show_carat_obj != Py_None) { + widget->set_always_show_carat(Python::GetPyBool(always_show_carat_obj)); + } + if (big_obj != Py_None) { + widget->SetBig(Python::GetPyBool(big_obj)); + } + if (force_internal_editing_obj != Py_None) { + widget->set_force_internal_editing( + Python::GetPyBool(force_internal_editing_obj)); + } + if (pos_obj != Py_None) { + Point2D p = Python::GetPyPoint2D(pos_obj); + widget->set_translate(p.x, p.y); + } + if (flatness_obj != Py_None) { + widget->set_flatness(Python::GetPyFloat(flatness_obj)); + } + if (rotate_obj != Py_None) { + widget->set_rotate(Python::GetPyFloat(rotate_obj)); + } + if (shadow_obj != Py_None) { + widget->set_shadow(Python::GetPyFloat(shadow_obj)); + } + if (maxwidth_obj != Py_None) { + widget->set_max_width(Python::GetPyFloat(maxwidth_obj)); + } + if (max_height_obj != Py_None) { + widget->set_max_height(Python::GetPyFloat(max_height_obj)); + } + // note: need to make sure to set this before settings text + // (influences whether we look for json strings or not) + if (editable_obj != Py_None) { + widget->SetEditable(Python::GetPyBool(editable_obj)); + } + + if (text_obj != Py_None) { + widget->SetText(Python::GetPyString(text_obj)); + } + if (h_align_obj != Py_None) { + std::string halign = Python::GetPyString(h_align_obj); + if (halign == "left") { + widget->set_halign(TextWidget::HAlign::kLeft); + } else if (halign == "center") { + widget->set_halign(TextWidget::HAlign::kCenter); + } else if (halign == "right") { + widget->set_halign(TextWidget::HAlign::kRight); + } else { + throw Exception("Invalid halign.", PyExcType::kValue); + } + } + if (v_align_obj != Py_None) { + std::string valign = Python::GetPyString(v_align_obj); + if (valign == "top") { + widget->set_valign(TextWidget::VAlign::kTop); + } else if (valign == "center") { + widget->set_valign(TextWidget::VAlign::kCenter); + } else if (valign == "bottom") { + widget->set_valign(TextWidget::VAlign::kBottom); + } else { + throw Exception("Invalid valign.", PyExcType::kValue); + } + } + if (always_highlight_obj != Py_None) { + widget->set_always_highlight(Python::GetPyBool(always_highlight_obj)); + } + if (padding_obj != Py_None) { + widget->set_padding(Python::GetPyFloat(padding_obj)); + } + if (scale_obj != Py_None) { + widget->set_center_scale(Python::GetPyFloat(scale_obj)); + } + // *normal* widget scale.. we currently plug 'scale' into 'centerScale'. ew. + if (corner_scale_obj != Py_None) { + widget->set_scale(Python::GetPyFloat(corner_scale_obj)); + } + if (draw_controller_obj != Py_None) { + auto* dcw = Python::GetPyWidget(draw_controller_obj); + if (!dcw) { + throw Exception("Invalid or nonexistent draw-controller widget.", + PyExcType::kWidgetNotFound); + } + widget->set_draw_control_parent(dcw); + } + if (on_return_press_call_obj != Py_None) { + widget->set_on_return_press_call(on_return_press_call_obj); + } + if (on_select_call_obj != Py_None) { + widget->SetOnSelectCall(on_select_call_obj); + } + if (on_activate_call_obj != Py_None) { + widget->set_on_activate_call(on_activate_call_obj); + } + if (selectable_obj != Py_None) + widget->set_selectable(Python::GetPyBool(selectable_obj)); + + if (color_obj != Py_None) { + std::vector c = Python::GetPyFloats(color_obj); + if (c.size() == 3) { + widget->set_color(c[0], c[1], c[2], 1.0f); + } else if (c.size() == 4) { + widget->set_color(c[0], c[1], c[2], c[3]); + } else { + throw Exception("Expected 3 or 4 floats for color.", PyExcType::kValue); + } + } + if (click_activate_obj != Py_None) { + widget->set_click_activate(Python::GetPyBool(click_activate_obj)); + } + if (extra_touch_border_scale_obj != Py_None) { + widget->set_extra_touch_border_scale( + Python::GetPyFloat(extra_touch_border_scale_obj)); + } + if (res_scale_obj != Py_None) { + widget->set_res_scale(Python::GetPyFloat(res_scale_obj)); + } + + // if making a new widget add it at the end + if (edit_obj == Py_None) { + g_ui->AddWidget(widget.get(), parent_widget); + } + return widget->NewPyRef(); + + BA_PYTHON_CATCH; +} + +auto PyWidgetCall(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + + Platform::SetLastPyCall("widget"); + + PyObject* edit_obj = Py_None; + PyObject* down_widget_obj = Py_None; + PyObject* up_widget_obj = Py_None; + PyObject* left_widget_obj = Py_None; + PyObject* right_widget_obj = Py_None; + PyObject* show_buffer_top_obj = Py_None; + PyObject* show_buffer_bottom_obj = Py_None; + PyObject* show_buffer_left_obj = Py_None; + PyObject* show_buffer_right_obj = Py_None; + PyObject* autoselect_obj = Py_None; + + static const char* kwlist[] = {"edit", + "up_widget", + "down_widget", + "left_widget", + "right_widget", + "show_buffer_top", + "show_buffer_bottom", + "show_buffer_left", + "show_buffer_right", + "autoselect", + nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "O|OOOOOOOOO", const_cast(kwlist), &edit_obj, + &up_widget_obj, &down_widget_obj, &left_widget_obj, &right_widget_obj, + &show_buffer_top_obj, &show_buffer_bottom_obj, &show_buffer_left_obj, + &show_buffer_right_obj, &autoselect_obj)) + return nullptr; + + if (!g_game->IsInUIContext()) { + throw Exception( + "This must be called within the UI context (see ba.Context docs).", + PyExcType::kContext); + } + ScopedSetContext cp(g_game->GetUIContextTarget()); + + Widget* widget = nullptr; + if (edit_obj != Py_None) { + widget = Python::GetPyWidget(edit_obj); + } + if (!widget) + throw Exception("Invalid or nonexistent widget passed.", + PyExcType::kWidgetNotFound); + + if (down_widget_obj != Py_None) { + Widget* down_widget = Python::GetPyWidget(down_widget_obj); + if (!down_widget) { + throw Exception("Invalid down widget.", PyExcType::kWidgetNotFound); + } + widget->set_down_widget(down_widget); + } + if (up_widget_obj != Py_None) { + Widget* up_widget = Python::GetPyWidget(up_widget_obj); + if (!up_widget) { + throw Exception("Invalid up widget.", PyExcType::kWidgetNotFound); + } + widget->set_up_widget(up_widget); + } + if (left_widget_obj != Py_None) { + Widget* left_widget = Python::GetPyWidget(left_widget_obj); + if (!left_widget) { + throw Exception("Invalid left widget.", PyExcType::kWidgetNotFound); + } + widget->set_left_widget(left_widget); + } + if (right_widget_obj != Py_None) { + Widget* right_widget = Python::GetPyWidget(right_widget_obj); + if (!right_widget) { + throw Exception("Invalid right widget.", PyExcType::kWidgetNotFound); + } + widget->set_right_widget(right_widget); + } + if (show_buffer_top_obj != Py_None) { + widget->set_show_buffer_top(Python::GetPyFloat(show_buffer_top_obj)); + } + if (show_buffer_bottom_obj != Py_None) { + widget->set_show_buffer_bottom(Python::GetPyFloat(show_buffer_bottom_obj)); + } + if (show_buffer_left_obj != Py_None) { + widget->set_show_buffer_left(Python::GetPyFloat(show_buffer_left_obj)); + } + if (show_buffer_right_obj != Py_None) { + widget->set_show_buffer_right(Python::GetPyFloat(show_buffer_right_obj)); + } + if (autoselect_obj != Py_None) { + widget->set_auto_select(Python::GetPyBool(autoselect_obj)); + } + + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyUIBounds(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("uibounds"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + assert(g_graphics); + // note: to be safe, we return our min guaranteed screen bounds; not our + // current (which can be bigger) + float x = 0.5f * kBaseVirtualResX; + float virtual_res_y = kBaseVirtualResY; + float y = 0.5f * virtual_res_y; + return Py_BuildValue("(ffff)", -x, x, -y, y); + BA_PYTHON_CATCH; +} + +auto PyFocusWindow(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + + Platform::SetLastPyCall("focuswindow"); + + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + assert(InGameThread()); +#if BA_OSTYPE_MACOS && BA_XCODE_BUILD && !BA_HEADLESS_BUILD + SDL_ericf_focus(); +#else +#endif + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyShowOnlineScoreUI(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("show_online_score_ui"); + const char* show = "general"; + PyObject* game_obj = Py_None; + PyObject* game_version_obj = Py_None; + static const char* kwlist[] = {"show", "game", "game_version", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|sOO", + const_cast(kwlist), &show, &game_obj, + &game_version_obj)) { + return nullptr; + } + std::string game; + if (game_obj != Py_None) { + game = Python::GetPyString(game_obj); + } + std::string game_version; + if (game_version_obj != Py_None) { + game_version = Python::GetPyString(game_version_obj); + } + g_app->PushShowOnlineScoreUICall(show, game, game_version); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyFadeScreen(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + + Platform::SetLastPyCall("fade_screen"); + + // This can only be called in the UI context. + int fade = 0; + float time = 0.25; + PyObject* endcall = nullptr; + static const char* kwlist[] = {"to", "time", "endcall", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "|pfO", + const_cast(kwlist), &fade, &time, + &endcall)) { + return nullptr; + } + g_graphics->FadeScreen(static_cast(fade), + static_cast(1000.0f * time), endcall); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyShowAd(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("show_ad"); + BA_PRECONDITION(InGameThread()); + const char* purpose; + PyObject* on_completion_call_obj = Py_None; + int pass_actually_showed = false; + static const char* kwlist[] = {"purpose", "on_completion_call", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "s|O", const_cast(kwlist), &purpose, + &on_completion_call_obj, &pass_actually_showed)) { + return nullptr; + } + AppInternalSetAdCompletionCall(on_completion_call_obj, + static_cast(pass_actually_showed)); + + // In cases where we support ads, store our callback and kick one off. + // We'll then fire our callback once its done. + // If we *don't* support ads, just store our callback and then kick off + // an ad-view-complete message ourself so the event flow is similar.. + if (g_platform->GetHasAds()) { + g_platform->ShowAd(purpose); + } else { + AppInternalPushAdViewComplete(purpose, false); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +// (same as PyShowAd but passes actually_showed arg in callback) +auto PyShowAd2(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + + Platform::SetLastPyCall("show_ad_2"); + BA_PRECONDITION(InGameThread()); + const char* purpose; + PyObject* on_completion_call_obj = Py_None; + int pass_actually_showed = true; + static const char* kwlist[] = {"purpose", "on_completion_call", nullptr}; + if (!PyArg_ParseTupleAndKeywords( + args, keywds, "s|O", const_cast(kwlist), &purpose, + &on_completion_call_obj, &pass_actually_showed)) { + return nullptr; + } + AppInternalSetAdCompletionCall(on_completion_call_obj, + static_cast(pass_actually_showed)); + + // In cases where we support ads, store our callback and kick one off. + // We'll then fire our callback once its done. + // If we *don't* support ads, just store our callback and then kick off + // an ad-view-complete message ourself so the event flow is similar.. + if (g_platform->GetHasAds()) { + g_platform->ShowAd(purpose); + } else { + AppInternalPushAdViewComplete(purpose, false); + } + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyShowAppInvite(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("show_app_invite"); + std::string title; + std::string message; + std::string code; + PyObject* title_obj; + PyObject* message_obj; + PyObject* code_obj; + static const char* kwlist[] = {"title", "message", "code", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "OOO", + const_cast(kwlist), &title_obj, + &message_obj, &code_obj)) { + return nullptr; + } + title = Python::GetPyString(title_obj); + message = Python::GetPyString(message_obj); + code = Python::GetPyString(code_obj); + g_platform->AndroidShowAppInvite(title, message, code); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyShowProgressBar(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + + Platform::SetLastPyCall("show_progress_bar"); + + g_graphics->EnableProgressBar(false); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyShowInvitesUI(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("show_invites_ui"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + g_platform->AndroidGPGSPartyShowInvites(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PySetPartyIconAlwaysVisible(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + + Platform::SetLastPyCall("set_party_icon_always_visible"); + + int value; + static const char* kwlist[] = {"value", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", + const_cast(kwlist), &value)) { + return nullptr; + } + assert(g_input); + g_ui->root_ui()->set_always_draw_party_icon(static_cast(value)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyChatMessage(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("chat_message"); + std::string message; + PyObject* message_obj; + PyObject* clients_obj = Py_None; + PyObject* sender_override_obj = Py_None; + std::string sender_override; + const std::string* sender_override_p{}; + std::vector* clients_p{}; + + static const char* kwlist[] = {"message", "clients", "sender_override", + nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|OO", + const_cast(kwlist), &message_obj, + &clients_obj, &sender_override_obj)) { + return nullptr; + } + message = Python::GetPyString(message_obj); + if (sender_override_obj != Py_None) { + sender_override = Python::GetPyString(sender_override_obj); + sender_override_p = &sender_override; + } + + if (clients_obj != Py_None) { + std::vector clients = Python::GetPyInts(clients_obj); + clients_p = &clients; + } + g_game->SendChatMessage(message, clients_p, sender_override_p); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetChatMessages(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + + Platform::SetLastPyCall("get_chat_messages"); + + BA_PRECONDITION(InGameThread()); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + PyObject* py_list = PyList_New(0); + for (auto&& i : g_game->chat_messages()) { + PyList_Append(py_list, PyUnicode_FromString(i.c_str())); + } + return py_list; + BA_PYTHON_CATCH; +} + +auto PySetPartyWindowOpen(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("set_party_window_open"); + int value; + static const char* kwlist[] = {"value", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "p", + const_cast(kwlist), &value)) { + return nullptr; + } + assert(g_input); + g_ui->root_ui()->set_party_window_open(static_cast(value)); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyGetSpecialWidget(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + + Platform::SetLastPyCall("get_special_widget"); + + const char* name; + static const char* kwlist[] = {"name", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &name)) { + return nullptr; + } + RootWidget* root_widget = g_ui->root_widget(); + assert(root_widget); + Widget* w = root_widget->GetSpecialWidget(name); + if (w == nullptr) { + throw Exception("Invalid special widget name '" + std::string(name) + "'.", + PyExcType::kValue); + } + return w->NewPyRef(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +// returns an extra hash value that can be incorporated into security checks; +// this contains things like whether console commands have been run, etc. +auto PyHaveIncentivizedAd(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("have_incentivized_ad"); + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + if (g_app_globals->have_incentivized_ad) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +// this returns whether it makes sense to show an currently +auto PyCanShowAd(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + + Platform::SetLastPyCall("can_show_ad"); + + BA_PRECONDITION(InGameThread()); + // if we've got any network connections, no ads.. + // (don't want to make someone on the other end wait or risk disconnecting + // them or whatnot) also disallow ads if remote apps are connected; at least + // on android ads pause our activity which disconnects the remote app.. (could + // potentially still allow on other platforms; should verify..) + if (g_game->connection_to_host() || g_game->has_connection_to_clients() + || g_input->HaveRemoteAppController()) { + Py_RETURN_FALSE; + } + Py_RETURN_TRUE; // all systems go.. + BA_PYTHON_CATCH; +} + +auto PyHasVideoAds(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("has_video_ads"); + if (g_platform->GetHasVideoAds()) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +auto PyBackPress(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + + Platform::SetLastPyCall("back_press"); + + static const char* kwlist[] = {nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "", + const_cast(kwlist))) { + return nullptr; + } + g_input->HandleBackPress(true); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyOpenURL(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("open_url"); + const char* address = nullptr; + static const char* kwlist[] = {"address", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &address)) { + return nullptr; + } + assert(g_app); + g_app->PushOpenURLCall(address); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyOpenFileExternally(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + + Platform::SetLastPyCall("open_file_externally"); + + char* path = nullptr; + static const char* kwlist[] = {"path", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &path)) { + return nullptr; + } + g_platform->OpenFileExternally(path); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyOpenDirExternally(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + BA_PYTHON_TRY; + Platform::SetLastPyCall("open_dir_externally"); + char* path = nullptr; + static const char* kwlist[] = {"path", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &path)) { + return nullptr; + } + g_platform->OpenDirExternally(path); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyConsolePrint(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + + Platform::SetLastPyCall("console_print"); + +#if !BA_HEADLESS_BUILD + Py_ssize_t tuple_size = PyTuple_GET_SIZE(args); + PyObject* obj; + for (Py_ssize_t i = 0; i < tuple_size; i++) { + obj = PyTuple_GET_ITEM(args, i); + PyObject* str_obj = PyObject_Str(obj); + if (!str_obj) { + PyErr_Clear(); // In case this is caught without setting the py exc. + throw Exception(); + } + const char* c = PyUnicode_AsUTF8(str_obj); + g_game->PushConsolePrintCall(c); + Py_DECREF(str_obj); + } +#endif // !BA_HEADLESS_BUILD + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +auto PyIsPartyIconVisible(PyObject* self, PyObject* args, PyObject* keywds) + -> PyObject* { + Platform::SetLastPyCall("is_party_icon_visible"); + BA_PYTHON_TRY; + bool party_button_active = + (g_game->GetConnectedClientCount() > 0 || g_game->connection_to_host() + || g_ui->root_ui()->always_draw_party_icon()); + if (party_button_active) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } + BA_PYTHON_CATCH; +} + +PyMethodDef PythonMethodsUI::methods_def[] = { + {"is_party_icon_visible", (PyCFunction)PyIsPartyIconVisible, + METH_VARARGS | METH_KEYWORDS, + "is_party_icon_visible() -> bool\n" + "\n" + "(internal)"}, + + {"console_print", PyConsolePrint, METH_VARARGS, + "console_print(*args: Any) -> None\n" + "\n" + "(internal)\n" + "\n" + "Print the provided args to the game console (using str()).\n" + "For most debugging/info purposes you should just use Python's standard\n" + "print, which will show up in the game console as well."}, + + {"open_dir_externally", (PyCFunction)PyOpenDirExternally, + METH_VARARGS | METH_KEYWORDS, + "open_dir_externally(path: str) -> None\n" + "\n" + "(internal)\n" + "\n" + "Open the provided dir in the default external app."}, + + {"open_file_externally", (PyCFunction)PyOpenFileExternally, + METH_VARARGS | METH_KEYWORDS, + "open_file_externally(path: str) -> None\n" + "\n" + "(internal)\n" + "\n" + "Open the provided file in the default external app."}, + + {"open_url", (PyCFunction)PyOpenURL, METH_VARARGS | METH_KEYWORDS, + "open_url(address: str) -> None\n" + "\n" + "Open a provided URL.\n" + "\n" + "Category: General Utility Functions\n" + "\n" + "Open the provided url in a web-browser, or display the URL\n" + "string in a window if that isn't possible.\n"}, + + {"back_press", (PyCFunction)PyBackPress, METH_VARARGS | METH_KEYWORDS, + "back_press() -> None\n" + "\n" + "(internal)"}, + + {"has_video_ads", (PyCFunction)PyHasVideoAds, METH_VARARGS | METH_KEYWORDS, + "has_video_ads() -> bool\n" + "\n" + "(internal)"}, + + {"can_show_ad", (PyCFunction)PyCanShowAd, METH_VARARGS | METH_KEYWORDS, + "can_show_ad() -> bool\n" + "\n" + "(internal)"}, + + {"have_incentivized_ad", (PyCFunction)PyHaveIncentivizedAd, + METH_VARARGS | METH_KEYWORDS, + "have_incentivized_ad() -> bool\n" + "\n" + "(internal)"}, + + {"get_special_widget", (PyCFunction)PyGetSpecialWidget, + METH_VARARGS | METH_KEYWORDS, + "get_special_widget(name: str) -> Widget\n" + "\n" + "(internal)"}, + + {"set_party_window_open", (PyCFunction)PySetPartyWindowOpen, + METH_VARARGS | METH_KEYWORDS, + "set_party_window_open(value: bool) -> None\n" + "\n" + "(internal)"}, + + {"get_chat_messages", (PyCFunction)PyGetChatMessages, + METH_VARARGS | METH_KEYWORDS, + "get_chat_messages() -> List[str]\n" + "\n" + "(internal)"}, + + {"chatmessage", (PyCFunction)PyChatMessage, METH_VARARGS | METH_KEYWORDS, + "chatmessage(message: Union[str, ba.Lstr],\n" + " clients: Sequence[int] = None,\n" + " sender_override: str = None) -> None\n" + "\n" + "(internal)"}, + + {"set_party_icon_always_visible", (PyCFunction)PySetPartyIconAlwaysVisible, + METH_VARARGS | METH_KEYWORDS, + "set_party_icon_always_visible(value: bool) -> None\n" + "\n" + "(internal)"}, + + {"show_invites_ui", (PyCFunction)PyShowInvitesUI, + METH_VARARGS | METH_KEYWORDS, + "show_invites_ui() -> None\n" + "\n" + "(internal)\n" + "\n" + "Category: General Utility Functions"}, + + {"show_progress_bar", (PyCFunction)PyShowProgressBar, + METH_VARARGS | METH_KEYWORDS, + "show_progress_bar() -> None\n" + "\n" + "(internal)\n" + "\n" + "Category: General Utility Functions"}, + + {"show_app_invite", (PyCFunction)PyShowAppInvite, + METH_VARARGS | METH_KEYWORDS, + "show_app_invite(title: Union[str, ba.Lstr],\n" + " message: Union[str, ba.Lstr],\n" + " code: str) -> None\n" + "\n" + "(internal)\n" + "\n" + "Category: General Utility Functions"}, + + {"show_ad", (PyCFunction)PyShowAd, METH_VARARGS | METH_KEYWORDS, + "show_ad(purpose: str, on_completion_call: Callable[[], None] = None)\n" + " -> None\n" + "\n" + "(internal)"}, + + {"show_ad_2", (PyCFunction)PyShowAd2, METH_VARARGS | METH_KEYWORDS, + "show_ad_2(purpose: str,\n" + " on_completion_call: Callable[[bool], None] = None)\n" + " -> None\n" + "\n" + "(internal)"}, + + {"fade_screen", (PyCFunction)PyFadeScreen, METH_VARARGS | METH_KEYWORDS, + "fade_screen(to: int = 0, time: float = 0.25,\n" + " endcall: Optional[Callable[[], None]] = None) -> None\n" + "\n" + "(internal)\n" + "\n" + "Fade the local game screen in our out from black over a duration of\n" + "time. if \"to\" is 0, the screen will fade out to black. Otherwise it\n" + "will fade in from black. If endcall is provided, it will be run after a\n" + "completely faded frame is drawn."}, + + {"show_online_score_ui", (PyCFunction)PyShowOnlineScoreUI, + METH_VARARGS | METH_KEYWORDS, + "show_online_score_ui(show: str = 'general', game: str = None,\n" + " game_version: str = None) -> None\n" + "\n" + "(internal)"}, + + {"focus_window", (PyCFunction)PyFocusWindow, METH_VARARGS | METH_KEYWORDS, + "focus_window() -> None\n" + "\n" + "(internal)\n" + "\n" + "A workaround for some unintentional backgrounding that occurs on mac"}, + + {"uibounds", (PyCFunction)PyUIBounds, METH_VARARGS | METH_KEYWORDS, + "uibounds() -> Tuple[float, float, float, float]\n" + "\n" + "(internal)\n" + "\n" + "Returns a tuple of 4 values: (x-min, x-max, y-min, y-max) representing\n" + "the range of values that can be plugged into a root level\n" + "ba.ContainerWidget's stack_offset value while guaranteeing that its\n" + "center remains onscreen.\n"}, + + {"buttonwidget", (PyCFunction)PyButtonWidget, METH_VARARGS | METH_KEYWORDS, + "buttonwidget(edit: ba.Widget = None,\n" + " parent: ba.Widget = None,\n" + " size: Sequence[float] = None,\n" + " position: Sequence[float] = None,\n" + " on_activate_call: Callable = None,\n" + " label: Union[str, ba.Lstr] = None,\n" + " color: Sequence[float] = None,\n" + " down_widget: ba.Widget = None,\n" + " up_widget: ba.Widget = None,\n" + " left_widget: ba.Widget = None,\n" + " right_widget: ba.Widget = None,\n" + " texture: ba.Texture = None,\n" + " text_scale: float = None,\n" + " textcolor: Sequence[float] = None,\n" + " enable_sound: bool = None,\n" + " model_transparent: ba.Model = None,\n" + " model_opaque: ba.Model = None,\n" + " repeat: bool = None,\n" + " scale: float = None,\n" + " transition_delay: float = None,\n" + " on_select_call: Callable = None,\n" + " button_type: str = None,\n" + " extra_touch_border_scale: float = None,\n" + " selectable: bool = None,\n" + " show_buffer_top: float = None,\n" + " icon: ba.Texture = None,\n" + " iconscale: float = None,\n" + " icon_tint: float = None,\n" + " icon_color: Sequence[float] = None,\n" + " autoselect: bool = None,\n" + " mask_texture: ba.Texture = None,\n" + " tint_texture: ba.Texture = None,\n" + " tint_color: Sequence[float] = None,\n" + " tint2_color: Sequence[float] = None,\n" + " text_flatness: float = None,\n" + " text_res_scale: float = None,\n" + " enabled: bool = None) -> ba.Widget\n" + "\n" + "Create or edit a button widget.\n" + "\n" + "Category: User Interface Functions\n" + "\n" + "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget."}, + + {"checkboxwidget", (PyCFunction)PyCheckBoxWidget, + METH_VARARGS | METH_KEYWORDS, + "checkboxwidget(edit: ba.Widget = None,\n" + " parent: ba.Widget = None,\n" + " size: Sequence[float] = None,\n" + " position: Sequence[float] = None,\n" + " text: Union[ba.Lstr, str] = None,\n" + " value: bool = None,\n" + " on_value_change_call: Callable[[bool], None] = None,\n" + " on_select_call: Callable[[], None] = None,\n" + " text_scale: float = None,\n" + " textcolor: Sequence[float] = None,\n" + " scale: float = None,\n" + " is_radio_button: bool = None,\n" + " maxwidth: float = None,\n" + " autoselect: bool = None,\n" + " color: Sequence[float] = None) -> ba.Widget\n" + "\n" + "Create or edit a check-box widget.\n" + "\n" + "Category: User Interface Functions\n" + "\n" + "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget."}, + + {"imagewidget", (PyCFunction)PyImageWidget, METH_VARARGS | METH_KEYWORDS, + "imagewidget(edit: ba.Widget = None, parent: ba.Widget = None,\n" + " size: Sequence[float] = None, position: Sequence[float] = None,\n" + " color: Sequence[float] = None, texture: ba.Texture = None,\n" + " opacity: float = None, model_transparent: ba.Model = None,\n" + " model_opaque: ba.Model = None, has_alpha_channel: bool = True,\n" + " tint_texture: ba.Texture = None, tint_color: Sequence[float] = None,\n" + " transition_delay: float = None, draw_controller: ba.Widget = None,\n" + " tint2_color: Sequence[float] = None, tilt_scale: float = None,\n" + " mask_texture: ba.Texture = None, radial_amount: float = None)\n" + " -> ba.Widget\n" + "\n" + "Create or edit an image widget.\n" + "\n" + "Category: User Interface Functions\n" + "\n" + "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget."}, + + {"columnwidget", (PyCFunction)PyColumnWidget, METH_VARARGS | METH_KEYWORDS, + "columnwidget(edit: ba.Widget = None,\n" + " parent: ba.Widget = None,\n" + " size: Sequence[float] = None,\n" + " position: Sequence[float] = None,\n" + " background: bool = None,\n" + " selected_child: ba.Widget = None,\n" + " visible_child: ba.Widget = None,\n" + " single_depth: bool = None,\n" + " print_list_exit_instructions: bool = None,\n" + " left_border: float = None,\n" + " top_border: float = None,\n" + " bottom_border: float = None,\n" + " selection_loops_to_parent: bool = None,\n" + " border: float = None,\n" + " margin: float = None,\n" + " claims_left_right: bool = None,\n" + " claims_tab: bool = None) -> ba.Widget\n" + "\n" + "Create or edit a column widget.\n" + "\n" + "Category: User Interface Functions\n" + "\n" + "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget."}, + + {"containerwidget", (PyCFunction)PyContainerWidget, + METH_VARARGS | METH_KEYWORDS, + "containerwidget(edit: ba.Widget = None,\n" + " parent: ba.Widget = None,\n" + " size: Sequence[float] = None,\n" + " position: Sequence[float] = None,\n" + " background: bool = None,\n" + " selected_child: ba.Widget = None,\n" + " transition: str = None,\n" + " cancel_button: ba.Widget = None,\n" + " start_button: ba.Widget = None,\n" + " root_selectable: bool = None,\n" + " on_activate_call: Callable[[], None] = None,\n" + " claims_left_right: bool = None,\n" + " claims_tab: bool = None,\n" + " selection_loops: bool = None,\n" + " selection_loops_to_parent: bool = None,\n" + " scale: float = None,\n" + " on_outside_click_call: Callable[[], None] = None,\n" + " single_depth: bool = None,\n" + " visible_child: ba.Widget = None,\n" + " stack_offset: Sequence[float] = None,\n" + " color: Sequence[float] = None,\n" + " on_cancel_call: Callable[[], None] = None,\n" + " print_list_exit_instructions: bool = None,\n" + " click_activate: bool = None,\n" + " always_highlight: bool = None,\n" + " selectable: bool = None,\n" + " scale_origin_stack_offset: Sequence[float] = None,\n" + " toolbar_visibility: str = None,\n" + " on_select_call: Callable[[], None] = None,\n" + " claim_outside_clicks: bool = None,\n" + " claims_up_down: bool = None) -> ba.Widget\n" + "\n" + "Create or edit a container widget.\n" + "\n" + "Category: User Interface Functions\n" + "\n" + "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget."}, + + {"rowwidget", (PyCFunction)PyRowWidget, METH_VARARGS | METH_KEYWORDS, + "rowwidget(edit: Widget = None, parent: Widget = None,\n" + " size: Sequence[float] = None,\n" + " position: Sequence[float] = None,\n" + " background: bool = None, selected_child: Widget = None,\n" + " visible_child: Widget = None,\n" + " claims_left_right: bool = None,\n" + " claims_tab: bool = None,\n" + " selection_loops_to_parent: bool = None) -> Widget\n" + "\n" + "Create or edit a row widget.\n" + "\n" + "Category: User Interface Functions\n" + "\n" + "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget."}, + + {"scrollwidget", (PyCFunction)PyScrollWidget, METH_VARARGS | METH_KEYWORDS, + "scrollwidget(edit: ba.Widget = None, parent: ba.Widget = None,\n" + " size: Sequence[float] = None, position: Sequence[float] = None,\n" + " background: bool = None, selected_child: ba.Widget = None,\n" + " capture_arrows: bool = False, on_select_call: Callable = None,\n" + " center_small_content: bool = None, color: Sequence[float] = None,\n" + " highlight: bool = None, border_opacity: float = None,\n" + " simple_culling_v: float = None,\n" + " selection_loops_to_parent: bool = None,\n" + " claims_left_right: bool = None,\n" + " claims_tab: bool = None) -> ba.Widget\n" + "\n" + "Create or edit a scroll widget.\n" + "\n" + "Category: User Interface Functions\n" + "\n" + "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget."}, + + {"hscrollwidget", (PyCFunction)PyHScrollWidget, + METH_VARARGS | METH_KEYWORDS, + "hscrollwidget(edit: ba.Widget = None, parent: ba.Widget = None,\n" + " size: Sequence[float] = None, position: Sequence[float] = None,\n" + " background: bool = None, selected_child: ba.Widget = None,\n" + " capture_arrows: bool = None,\n" + " on_select_call: Callable[[], None] = None,\n" + " center_small_content: bool = None, color: Sequence[float] = None,\n" + " highlight: bool = None, border_opacity: float = None,\n" + " simple_culling_h: float = None,\n" + " claims_left_right: bool = None,\n" + " claims_tab: bool = None) -> ba.Widget\n" + "\n" + "Create or edit a horizontal scroll widget.\n" + "\n" + "Category: User Interface Functions\n" + "\n" + "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget."}, + + {"textwidget", (PyCFunction)PyTextWidget, METH_VARARGS | METH_KEYWORDS, + "textwidget(edit: Widget = None, parent: Widget = None,\n" + " size: Sequence[float] = None, position: Sequence[float] = None,\n" + " text: Union[str, ba.Lstr] = None, v_align: str = None,\n" + " h_align: str = None, editable: bool = None, padding: float = None,\n" + " on_return_press_call: Callable[[], None] = None,\n" + " on_activate_call: Callable[[], None] = None,\n" + " selectable: bool = None, query: Widget = None, max_chars: int = None,\n" + " color: Sequence[float] = None, click_activate: bool = None,\n" + " on_select_call: Callable[[], None] = None,\n" + " always_highlight: bool = None, draw_controller: Widget = None,\n" + " scale: float = None, corner_scale: float = None,\n" + " description: Union[str, ba.Lstr] = None,\n" + " transition_delay: float = None, maxwidth: float = None,\n" + " max_height: float = None, flatness: float = None,\n" + " shadow: float = None, autoselect: bool = None, rotate: float = None,\n" + " enabled: bool = None, force_internal_editing: bool = None,\n" + " always_show_carat: bool = None, big: bool = None,\n" + " extra_touch_border_scale: float = None, res_scale: float = None)\n" + " -> Widget\n" + "\n" + "Create or edit a text widget.\n" + "\n" + "Category: User Interface Functions\n" + "\n" + "Pass a valid existing ba.Widget as 'edit' to modify it; otherwise\n" + "a new one is created and returned. Arguments that are not set to None\n" + "are applied to the Widget."}, + + {"widget", (PyCFunction)PyWidgetCall, METH_VARARGS | METH_KEYWORDS, + "widget(edit: ba.Widget = None, up_widget: ba.Widget = None,\n" + " down_widget: ba.Widget = None, left_widget: ba.Widget = None,\n" + " right_widget: ba.Widget = None, show_buffer_top: float = None,\n" + " show_buffer_bottom: float = None, show_buffer_left: float = None,\n" + " show_buffer_right: float = None, autoselect: bool = None) -> None\n" + "\n" + "Edit common attributes of any widget.\n" + "\n" + "Category: User Interface Functions\n" + "\n" + "Unlike other UI calls, this can only be used to edit, not to create.\n"}, + + {nullptr, nullptr, 0, nullptr}}; + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/ballistica/python/methods/python_methods_ui.h b/src/ballistica/python/methods/python_methods_ui.h new file mode 100644 index 00000000..7070a2e1 --- /dev/null +++ b/src/ballistica/python/methods/python_methods_ui.h @@ -0,0 +1,18 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_UI_H_ +#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_UI_H_ + +#include "ballistica/python/python_sys.h" + +namespace ballistica { + +/// UI related individual python methods for our module. +class PythonMethodsUI { + public: + static PyMethodDef methods_def[]; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_UI_H_ diff --git a/src/ballistica/python/python.h b/src/ballistica/python/python.h new file mode 100644 index 00000000..6efe9fbe --- /dev/null +++ b/src/ballistica/python/python.h @@ -0,0 +1,420 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_PYTHON_H_ +#define BALLISTICA_PYTHON_PYTHON_H_ + +#include +#include +#include +#include +#include + +#include "ballistica/ballistica.h" +#include "ballistica/core/context.h" +#include "ballistica/core/object.h" +#include "ballistica/generic/buffer.h" +#include "ballistica/generic/runnable.h" +#include "ballistica/math/point2d.h" +#include "ballistica/platform/min_sdl.h" +#include "ballistica/python/python_ref.h" + +namespace ballistica { + +/// General python support/infrastructure class. +class Python { + public: + /// When calling a python callable directly, you can use the following + /// to push and pop a text label which will be printed as 'call' in errors. + class ScopedCallLabel { + public: + explicit ScopedCallLabel(const char* label) { + prev_label_ = current_label_; + } + ~ScopedCallLabel() { current_label_ = prev_label_; } + static auto current_label() -> const char* { return current_label_; } + + private: + const char* prev_label_ = nullptr; + static const char* current_label_; + BA_DISALLOW_CLASS_COPIES(ScopedCallLabel); + }; + + /// Use this to protect Python code that may be run in cases where we don't + /// hold the Global Interpreter Lock (GIL) (basically anything outside of the + /// game thread). + class ScopedInterpreterLock { + public: + ScopedInterpreterLock(); + ~ScopedInterpreterLock(); + + private: + class Impl; + Impl* impl_ = nullptr; + }; + + /// Return whether the current thread holds the global-interpreter-lock. + /// We must always hold the GIL while running python code. + /// This *should* generally be the case by default, but this can be handy for + /// sanity checking that. + static auto HaveGIL() -> bool; + + /// Attempt to print the python stack trace. + static void PrintStackTrace(); + + /// Pass any PyObject* (including nullptr) to get a readable string + /// (basically equivalent of str(foo)). + static auto ObjToString(PyObject* obj) -> std::string; + + /// Given an asset-package python object and a media name, verify + /// that the asset-package is valid in the current context and return + /// its fully qualified name if so. Throw an Exception if not. + auto ValidatedPackageAssetName(PyObject* package, const char* name) + -> std::string; + + static void LogContextForCallableLabel(const char* label); + static void LogContextEmpty(); + static void LogContextAuto(); + static void LogContextNonGameThread(); + Python(); + ~Python(); + + void Reset(bool init = true); + + auto GetContextBaseString() -> std::string; + auto GetControllerValue(InputDevice* input_device, + const std::string& value_name) -> int; + auto GetControllerFloatValue(InputDevice* input_device, + const std::string& value_name) -> float; + void HandleDeviceMenuPress(InputDevice* input_device); + auto GetLastPlayerNameFromInputDevice(InputDevice* input_device) + -> std::string; + void AcquireGIL(); + void ReleaseGIL(); + + void LaunchStringEdit(TextWidget* w); + void CaptureGamePadInput(PyObject* obj); + void ReleaseGamePadInput(); + void CaptureKeyboardInput(PyObject* obj); + void ReleaseKeyboardInput(); + void HandleFriendScoresCB(const FriendScoreSet& ss); + void IssueCallInGameThreadWarning(PyObject* call); + + /// Borrowed from python's source code: used in overriding of objects' dir() + /// results. + static auto generic_dir(PyObject* self) -> PyObject*; + + /// For use by g_game in passing events along to the python layer (for + /// captured input, etc). + auto HandleJoystickEvent(const SDL_Event& event, + InputDevice* input_device = nullptr) -> bool; + auto HandleKeyPressEvent(const SDL_Keysym& keysym) -> bool; + auto HandleKeyReleaseEvent(const SDL_Keysym& keysym) -> bool; + + auto inited() const -> bool { return inited_; } + + /// Attempt to push the log to the server - returns true if no error occurs + /// (note; this doesn't mean that the log actually made it; just that it got + /// sent off) + static auto PutLog(bool fatal, bool short_suicide_timer = true) -> bool; + + /// Filter incoming chat message from client. + /// If returns false, message should be ignored. + auto FilterChatMessage(std::string* message, int client_id) -> bool; + + /// Pass a chat message along to the python UI layer for handling.. + void HandleLocalChatMessage(const std::string& message); + + void DispatchScoresToBeatResponse( + bool success, const std::list& scores_to_beat, + void* PyCallback); + + /// Pop up an in-game window to show a url (NOT in a browser). + void ShowURL(const std::string& url); + + void AddCleanFrameCommand(const Object::Ref& c); + void RunCleanFrameCommands(); + + /// Return a minimal filename/position string such as 'foo.py:201' based + /// on the python stack state. This shouldn't be too expensive to fetch and + /// is useful as an object identifier/etc. + static auto GetPythonFileLocation(bool pretty = true) -> std::string; + + void PartyInvite(const std::string& player, const std::string& invite_id); + void PartyInviteRevoke(const std::string& invite_id); + void set_env_obj(PyObject* obj) { env_ = obj; } + auto env_obj() const -> PyObject* { + assert(env_); + return env_; + } + auto main_dict() const -> PyObject* { + assert(main_dict_); + return main_dict_; + } + void PlayMusic(const std::string& music_type, bool continuous); + + // Fetch raw values from the config dict. The default value is returned if + // the requested value is not present or not of a compatible type. + // Note: to get app config values you should generally use the bs::AppConfig + // functions (which themselves call these functions) + auto GetRawConfigValue(const char* name) + -> PyObject*; // (returns a borrowed ref) + auto GetRawConfigValue(const char* name, const char* default_value) + -> std::string; + auto GetRawConfigValue(const char* name, float default_value) -> float; + auto GetRawConfigValue(const char* name, int default_value) -> int; + auto GetRawConfigValue(const char* name, bool default_value) -> bool; + void SetRawConfigValue(const char* name, float value); + + void RunDeepLink(const std::string& url); + auto GetResource(const char* key, const char* fallback_resource = nullptr, + const char* fallback_value = nullptr) -> std::string; + auto GetTranslation(const char* category, const char* s) -> std::string; + + // For checking and pulling values out of python objects. + // These will all throw Exceptions on errors. + static auto GetPyString(PyObject* o) -> std::string; + static auto GetPyInt64(PyObject* o) -> int64_t; + static auto GetPyInt(PyObject* o) -> int; + static auto GetPyNode(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) -> Node*; + static auto GetPyNodes(PyObject* o) -> std::vector; + static auto GetPyMaterials(PyObject* o) -> std::vector; + static auto GetPyTextures(PyObject* o) -> std::vector; + static auto GetPyModels(PyObject* o) -> std::vector; + static auto GetPySounds(PyObject* o) -> std::vector; + static auto GetPyCollideModels(PyObject* o) -> std::vector; + static auto GetPyCollideModel(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) -> CollideModel*; + static auto IsPySession(PyObject* o) -> bool; + static auto GetPySession(PyObject* o) -> Session*; + static auto IsPyString(PyObject* o) -> bool; + static auto GetPyBool(PyObject* o) -> bool; + static auto GetPyHostActivity(PyObject* o) -> HostActivity*; + static auto IsPyHostActivity(PyObject* o) -> bool; + static auto GetPyInputDevice(PyObject* o) -> InputDevice*; + static auto IsPyPlayer(PyObject* o) -> bool; + static auto GetPyPlayer(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) -> Player*; + static auto GetPySessionPlayer(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) -> Player*; + static auto GetPyMaterial(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) -> Material*; + static auto GetPyTexture(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) -> Texture*; + static auto GetPyModel(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) -> Model*; + static auto GetPySound(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) -> Sound*; + static auto GetPyData(PyObject* o, bool allow_empty_ref = false, + bool allow_none = false) -> Data*; + static auto GetPyWidget(PyObject* o) -> Widget*; + static auto CanGetPyDouble(PyObject* o) -> bool; + static auto GetPyFloat(PyObject* o) -> float { + return static_cast(GetPyDouble(o)); + } + static auto GetPyDouble(PyObject* o) -> double; + static auto GetPyFloats(PyObject* o) -> std::vector; + static auto GetPyInts64(PyObject* o) -> std::vector; + static auto GetPyInts(PyObject* o) -> std::vector; + static auto GetPyStrings(PyObject* o) -> std::vector; + static auto GetPyUInts64(PyObject* o) -> std::vector; + static auto GetPyPoint2D(PyObject* o) -> Point2D; + static auto CanGetPyVector3f(PyObject* o) -> bool; + static auto GetPyVector3f(PyObject* o) -> Vector3f; + + static auto GetPyEnum_Permission(PyObject* obj) -> Permission; + static auto GetPyEnum_SpecialChar(PyObject* obj) -> SpecialChar; + static auto GetPyEnum_TimeType(PyObject* obj) -> TimeType; + static auto GetPyEnum_TimeFormat(PyObject* obj) -> TimeFormat; + static auto IsPyEnum_InputType(PyObject* obj) -> bool; + static auto GetPyEnum_InputType(PyObject* obj) -> InputType; + + static auto GetNodeAttr(Node* node, const char* attribute_name) -> PyObject*; + static void SetNodeAttr(Node* node, const char* attr_name, + PyObject* value_obj); + + static void SetPythonException(PyExcType exctype, const char* description); + + static void DoBuildNodeMessage(PyObject* args, int arg_offset, + Buffer* b, PyObject** user_message_obj); + auto DoNewNode(PyObject* args, PyObject* keywds) -> Node*; + + /// Identifiers for specific Python objects we grab references to for easy + /// access. + enum class ObjID { + kEmptyTuple, + kApp, + kEnv, + kDeepCopyCall, + kShallowCopyCall, + kShouldShatterMessageClass, + kImpactDamageMessageClass, + kPickedUpMessageClass, + kDroppedMessageClass, + kOutOfBoundsMessageClass, + kPickUpMessageClass, + kDropMessageClass, + kShowURLWindowCall, + kActivityClass, + kSessionClass, + kJsonDumpsCall, + kJsonLoadsCall, + kGetDeviceValueCall, + kDeviceMenuPressCall, + kGetLastPlayerNameFromInputDeviceCall, + kOnScreenKeyboardClass, + kFilterChatMessageCall, + kHandleLocalChatMessageCall, + kHandlePartyInviteCall, + kHandlePartyInviteRevokeCall, + kDoPlayMusicCall, + kDeepLinkCall, + kGetResourceCall, + kTranslateCall, + kLStrClass, + kCallClass, + kGarbageCollectCall, + kConfig, + kOnAppLaunchCall, + kClientInfoQueryResponseCall, + kResetToMainMenuCall, + kSetConfigFullscreenOnCall, + kSetConfigFullscreenOffCall, + kNotSignedInScreenMessageCall, + kConnectingToPartyMessageCall, + kRejectingInviteAlreadyInPartyMessageCall, + kConnectionFailedMessageCall, + kTemporarilyUnavailableMessageCall, + kInProgressMessageCall, + kErrorMessageCall, + kPurchaseNotValidErrorCall, + kPurchaseAlreadyInProgressErrorCall, + kGearVRControllerWarningCall, + kVROrientationResetCBMessageCall, + kVROrientationResetMessageCall, + kHandleAppResumeCall, + kHandleLogCall, + kLaunchMainMenuSessionCall, + kLanguageTestToggleCall, + kAwardInControlAchievementCall, + kAwardDualWieldingAchievementCall, + kPrintCorruptFileErrorCall, + kPlayGongSoundCall, + kLaunchCoopGameCall, + kPurchasesRestoredMessageCall, + kDismissWiiRemotesWindowCall, + kUnavailableMessageCall, + kSubmitAnalyticsCountsCall, + kSetLastAdNetworkCall, + kNoGameCircleMessageCall, + kEmptyCall, + kLevelIconPressCall, + kTrophyIconPressCall, + kCoinIconPressCall, + kTicketIconPressCall, + kBackButtonPressCall, + kFriendsButtonPressCall, + kPrintTraceCall, + kToggleFullscreenCall, + kPartyIconActivateCall, + kReadConfigCall, + kUIRemotePressCall, + kQuitWindowCall, + kRemoveInGameAdsMessageCall, + kTelnetAccessRequestCall, + kOnAppPauseCall, + kQuitCall, + kShutdownCall, + kGCDisableCall, + kShowPostPurchaseMessageCall, + kContextError, + kNotFoundError, + kNodeNotFoundError, + kSessionTeamNotFoundError, + kInputDeviceNotFoundError, + kDelegateNotFoundError, + kSessionPlayerNotFoundError, + kWidgetNotFoundError, + kActivityNotFoundError, + kSessionNotFoundError, + kAssetPackageClass, + kTimeFormatClass, + kTimeTypeClass, + kInputTypeClass, + kPermissionClass, + kSpecialCharClass, + kPlayerClass, + kGetPlayerIconCall, + kLstrFromJsonCall, + kLast // Sentinel; must be at end. + }; + + /// Access a particular Python object we've grabbed/stored. + auto obj(ObjID id) const -> const PythonRef& { + assert(id < ObjID::kLast); + if (g_buildconfig.debug_build()) { + if (!objs_[static_cast(id)].exists()) { + throw Exception("Python::obj() called on nonexistent val " + + std::to_string(static_cast(id))); + } + } + return objs_[static_cast(id)]; + } + + /// Return whether we have a particular Python object. + auto objexists(ObjID id) const -> bool { + assert(id < ObjID::kLast); + return objs_[static_cast(id)].exists(); + } + + /// Push a call to a preset obj to the game thread + /// (will be run in the UI context). + void PushObjCall(ObjID obj); + + /// Push a call with a single string arg. + void PushObjCall(ObjID obj, const std::string& arg); + + /// Register python location and returns true if it has not + /// yet been registered. (for print-once type stuff). + auto DoOnce() -> bool; + + /// Check values passed to timer functions; triggers warnings + /// for cases that look like they're passing milliseconds as seconds + /// or vice versa... (can remove this once things are settled in). + void TimeFormatCheck(TimeFormat time_format, PyObject* length_obj); + + private: + /// Check/set debug related initialization. + void SetupInterpreterDebugState(); + + /// Set up system paths if needed (for embedded builds). + void SetupPythonHome(); + + /// Set the value for a named object. + void SetObj(ObjID id, PyObject* pyobj, bool incref = false); + + /// Set the value for a named object and verify that it is a callable. + void SetObjCallable(ObjID id, PyObject* pyobj, bool incref = false); + + /// Set the value for a named object to the result of a Python expression. + void SetObj(ObjID id, const char* expression); + + /// Set the value for a named object to the result of a Python expression + /// and verify that it is callable. + void SetObjCallable(ObjID id, const char* expression); + + std::set do_once_locations_; + PythonRef objs_[static_cast(ObjID::kLast)]; + bool inited_{}; + std::list > clean_frame_commands_; + PythonRef game_pad_call_; + PythonRef keyboard_call_; + PyObject* empty_dict_object_{}; + PyObject* main_dict_{}; + PyObject* env_{}; + PyThreadState* thread_state_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_PYTHON_H_ diff --git a/src/ballistica/python/python_command.cc b/src/ballistica/python/python_command.cc new file mode 100644 index 00000000..5e517d83 --- /dev/null +++ b/src/ballistica/python/python_command.cc @@ -0,0 +1,202 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/python_command.h" + +#include + +#include "ballistica/python/python.h" +#include "ballistica/python/python_sys.h" + +// Save/restore current command for logging/etc. +// this isn't exception-safe, but we should never let +// exceptions bubble up through python api calls anyway +// or we'll have bigger problems on our hands. +#define PUSH_PYCOMMAND(OBJ) \ + PythonCommand* prev_pycmd = current_command_; \ + current_command_ = OBJ +#define POP_PYCOMMAND() current_command_ = prev_pycmd + +namespace ballistica { + +PythonCommand* PythonCommand::current_command_ = nullptr; + +PythonCommand::PythonCommand() = default; + +PythonCommand::PythonCommand(std::string command_in) + : command_(std::move(command_in)) {} + +PythonCommand::PythonCommand(std::string command_in, std::string file_name_in) + : command_(std::move(command_in)), file_name_(std::move(file_name_in)) {} + +PythonCommand::PythonCommand(const PythonCommand& c) : command_(c.command_) {} + +auto PythonCommand::operator=(const PythonCommand& src) -> PythonCommand& { + if (&src == this) { + return *this; + } + file_code_obj_.Release(); + eval_code_obj_.Release(); + command_ = src.command_; + return *this; +} + +auto PythonCommand::operator=(const std::string& src) -> PythonCommand& { + file_code_obj_.Release(); + eval_code_obj_.Release(); + command_ = src; + return *this; +} + +void PythonCommand::CompileForExec() { + assert(Python::HaveGIL()); + assert(file_code_obj_.get() == nullptr); + PyObject* o = + Py_CompileString(command_.c_str(), file_name_.c_str(), Py_file_input); + if (o == nullptr) { + // we pass zero here to avoid grabbing references to this exception + // which can cause objects to stick around and trip up our deletion checks + // (nodes, actors existing after their games have ended) + PyErr_PrintEx(0); + } else { + file_code_obj_.Acquire(o); + } +} + +void PythonCommand::CompileForEval(bool print_errors) { + assert(Python::HaveGIL()); + assert(eval_code_obj_.get() == nullptr); + PyObject* o = + Py_CompileString(command_.c_str(), file_name_.c_str(), Py_eval_input); + if (o == nullptr) { + if (print_errors) { + // we pass zero here to avoid grabbing references to this exception + // which can cause objects to stick around and trip up our deletion checks + // (nodes, actors existing after their games have ended) + PyErr_PrintEx(0); + } + PyErr_Clear(); + } else { + eval_code_obj_.Acquire(o); + } +} + +PythonCommand::~PythonCommand() { dead_ = true; } + +auto PythonCommand::Run() -> bool { + assert(Python::HaveGIL()); + if (!g_python) { + // This probably means the game is dying; let's not + // throw an exception here so we don't mask the original error. + Log("PythonCommand: not running due to null g_python"); + return false; + } + assert(!dead_); + if (!file_code_obj_.get()) { + CompileForExec(); + assert(!dead_); + } + if (file_code_obj_.get()) { + PUSH_PYCOMMAND(this); + PyObject* v = PyEval_EvalCode(file_code_obj_.get(), g_python->main_dict(), + g_python->main_dict()); + POP_PYCOMMAND(); + + // Technically the python call could have killed us; + // make sure that didn't happen. + assert(!dead_); + if (v == nullptr) { + // Save/restore error or it can mess with context print calls. + BA_PYTHON_ERROR_SAVE; + Log("ERROR: exception in Python call:"); + LogContext(); + BA_PYTHON_ERROR_RESTORE; + + // We pass zero here to avoid grabbing references to this exception + // which can cause objects to stick around and trip up our deletion + // checks (nodes, actors existing after their games have ended). + PyErr_PrintEx(0); + PyErr_Clear(); + } else { + Py_DECREF(v); + return true; + } + } + return false; +} + +auto PythonCommand::CanEval() -> bool { + assert(Python::HaveGIL()); + assert(g_python); + if (!eval_code_obj_.get()) { + CompileForEval(false); + } + if (!eval_code_obj_.get()) { + PyErr_Clear(); + return false; + } + PyErr_Clear(); + return true; +} + +auto PythonCommand::RunReturnObj(bool print_errors) -> PyObject* { + assert(Python::HaveGIL()); + assert(g_python); + assert(!dead_); + if (!eval_code_obj_.get()) { + CompileForEval(print_errors); + assert(!dead_); + } + if (!eval_code_obj_.get()) { + if (print_errors) { + // Save/restore error or it can mess with context print calls. + BA_PYTHON_ERROR_SAVE; + Log("ERROR: exception in Python call:"); + LogContext(); + BA_PYTHON_ERROR_RESTORE; + // We pass zero here to avoid grabbing references to this exception + // which can cause objects to stick around and trip up our deletion checks + // (nodes, actors existing after their games have ended) + PyErr_PrintEx(0); + } + + // Consider the python error handled at this point. + // If C++ land wants to throw an exception or whatnot based on this result, + // that's a totally different thing. + PyErr_Clear(); + return nullptr; + } + PUSH_PYCOMMAND(this); + PyObject* v = PyEval_EvalCode(eval_code_obj_.get(), g_python->main_dict(), + g_python->main_dict()); + POP_PYCOMMAND(); + assert(!dead_); + if (v == nullptr) { + if (print_errors) { + // save/restore error or it can mess with context print calls + BA_PYTHON_ERROR_SAVE; + Log("ERROR: exception in Python call:"); + LogContext(); + BA_PYTHON_ERROR_RESTORE; + // we pass zero here to avoid grabbing references to this exception + // which can cause objects to stick around and trip up our deletion checks + // (nodes, actors existing after their games have ended) + PyErr_PrintEx(0); + } + + // Consider the python error handled at this point. + // If C++ land wants to throw an exception or whatnot based on this result, + // that's a totally different thing. + PyErr_Clear(); + return nullptr; + } + return v; +} + +void PythonCommand::LogContext() { + assert(Python::HaveGIL()); + std::string s = std::string(" call: ") + command(); + s += g_python->GetContextBaseString(); + Log(s); +} + +} // namespace ballistica diff --git a/src/ballistica/python/python_command.h b/src/ballistica/python/python_command.h new file mode 100644 index 00000000..e6029ee0 --- /dev/null +++ b/src/ballistica/python/python_command.h @@ -0,0 +1,73 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_PYTHON_COMMAND_H_ +#define BALLISTICA_PYTHON_PYTHON_COMMAND_H_ + +#include + +#include "ballistica/ballistica.h" +#include "ballistica/python/python_ref.h" + +namespace ballistica { + +// String based python commands. +// Does not save/restore context or anything; +// for that functionality use PythonContextCall; + +// Note to self: originally I though I'd be using this in a lot of places, +// so I added the ability to compile once and run repeatedly, quietly capture +// output instead of printing it, etc. Now, however, its usage is pretty +// much limited to a few places such as handling stdin and the in-game console. +// (Most places it is much cleaner to work with proper python modules and just +// interact with PyObject* refs to them) +// I should look and see if python's default high level calls would suffice +// for these purposes and potentially kill this off. +class PythonCommand { + public: + PythonCommand(); + PythonCommand(std::string command); // NOLINT (want to allow char*) + + static auto current_command() -> PythonCommand* { return current_command_; } + // file_name will be listed on error output + PythonCommand(std::string command, std::string file_name); + PythonCommand(const PythonCommand& other); + + // copy a command + auto operator=(const PythonCommand& other) -> PythonCommand&; + + // set the command to a new command string + auto operator=(const std::string& command) -> PythonCommand&; + ~PythonCommand(); + auto command() -> const std::string& { return command_; } + + /// Run the command. + /// return true if the command was successfully run + /// (not to be confused with the command's result) + /// This works for non-eval-able commands. + auto Run() -> bool; + + /// Run thecommand and return the result as a new Python reference. + /// Only works for eval-able commands. + /// Returns nullptr on errors, but Python error state will be cleared. + auto RunReturnObj(bool print_errors = false) -> PyObject*; + + void LogContext(); + + /// Return true if the command can be evaluated; otherwise it can only be + /// executed + auto CanEval() -> bool; + void CompileForExec(); + void CompileForEval(bool print_errors); + + private: + bool dead_ = false; + PythonRef file_code_obj_; + PythonRef eval_code_obj_; + std::string command_; + std::string file_name_; + static PythonCommand* current_command_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_PYTHON_COMMAND_H_ diff --git a/src/ballistica/python/python_context_call.cc b/src/ballistica/python/python_context_call.cc new file mode 100644 index 00000000..a503c971 --- /dev/null +++ b/src/ballistica/python/python_context_call.cc @@ -0,0 +1,135 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/python_context_call.h" + +#include "ballistica/game/host_activity.h" +#include "ballistica/game/session/host_session.h" +#include "ballistica/python/python.h" +#include "ballistica/python/python_sys.h" + +namespace ballistica { + +// FIXME - should be static member var +PythonContextCall* PythonContextCall::current_call_ = nullptr; + +PythonContextCall::PythonContextCall(PyObject* obj_in) { + assert(InGameThread()); + // as a sanity test, store the current context ptr just to make sure it + // hasn't changed when we run +#if BA_DEBUG_BUILD + context_target_sanity_test_ = context_.target.get(); +#endif // BA_DEBUG_BUILD + BA_PRECONDITION(PyCallable_Check(obj_in)); + object_.Acquire(obj_in); + GetTrace(); + // ok now we need to register this call with whatever the context is; + // it can be stored in a host-activity, a host-session, or the UI context. + // whoever it is registered with will explicitly release its contents on + // shutdown and ensure that nothing gets run after that point. + if (HostActivity* ha = context_.GetHostActivity()) { + ha->RegisterCall(this); + } else if (HostSession* hs = context_.GetHostSession()) { + hs->RegisterCall(this); + } else if (context_.GetUIContext()) { + // UI context never currently dies so no registering necessary here.. + } else { + throw Exception( + "Invalid context; ContextCalls must be created in a non-expired " + "Activity, Session, or UI context. (call obj = " + + Python::ObjToString(obj_in) + ").", + PyExcType::kContext); + } +} + +PythonContextCall::~PythonContextCall() { + // lets set up context while we take our stuff down + // (we may be holding refs to actors or whatnot) + ScopedSetContext cp(context_); + object_.Release(); +} + +auto PythonContextCall::GetObjectDescription() const -> std::string { + return ""; +} + +void PythonContextCall::GetTrace() { + PyFrameObject* f = PyThreadState_GET()->frame; + if (f) { + // grab the file/line now in case we error + // (useful for debugging simple timers and callbacks and such) + file_loc_ = Python::GetPythonFileLocation(); + } +} + +// called by our owning context when it goes down +// we should clear ourself out to be a no-op if we still happen to be called +void PythonContextCall::MarkDead() { + dead_ = true; + object_.Release(); +} + +void PythonContextCall::Run(PyObject* args) { + assert(this); + + if (!g_python) { + // This probably means the game is dying; let's not + // throw an exception here so we don't mask the original error. + Log("PythonCommand: not running due to null g_python"); + return; + } + + if (dead_) { + return; + } + + // Sanity test: make sure our context didn't go away. +#if BA_DEBUG_BUILD + if (context_.target.get() != context_target_sanity_test_) { + Log("WARNING: running Call after it's context has died: " + object_.Str()); + } +#endif // BA_DEBUG_BUILD + + // Restore the context from when we were made. + ScopedSetContext cp(context_); + + // Hold a ref to this call throughout this process + // so we know it'll still exist if we need to report + // exception info and whatnot. + Object::Ref keep_alive_ref(this); + + PythonContextCall* prev_call = current_call_; + current_call_ = this; + assert(Python::HaveGIL()); + PyObject* o = PyObject_Call( + object_.get(), + args ? args : g_python->obj(Python::ObjID::kEmptyTuple).get(), nullptr); + current_call_ = prev_call; + + if (o) { + Py_DECREF(o); + } else { + // Save/restore python error or it can mess with context print calls. + BA_PYTHON_ERROR_SAVE; + + Log("ERROR: exception in Python call:"); + LogContext(); + BA_PYTHON_ERROR_RESTORE; + + // We pass zero here to avoid grabbing references to this exception + // which can cause objects to stick around and trip up our deletion checks. + // (nodes, actors existing after their games have ended). + PyErr_PrintEx(0); + PyErr_Clear(); + } +} + +void PythonContextCall::LogContext() { + assert(InGameThread()); + std::string s = std::string(" root call: ") + object().Str(); + s += ("\n root call origin: " + file_loc()); + s += g_python->GetContextBaseString(); + Log(s); +} + +} // namespace ballistica diff --git a/src/ballistica/python/python_context_call.h b/src/ballistica/python/python_context_call.h new file mode 100644 index 00000000..f91100cb --- /dev/null +++ b/src/ballistica/python/python_context_call.h @@ -0,0 +1,54 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_PYTHON_CONTEXT_CALL_H_ +#define BALLISTICA_PYTHON_PYTHON_CONTEXT_CALL_H_ + +#include + +#include "ballistica/core/context.h" +#include "ballistica/core/object.h" +#include "ballistica/python/python_ref.h" + +namespace ballistica { + +// A callable and context-state wrapped up in a convenient package. +// Handy for use with user-submitted callbacks, as it restores context +// state from when it was created and prints various useful bits of info +// on exceptions. +class PythonContextCall : public Object { + public: + static auto current_call() -> PythonContextCall* { return current_call_; } + PythonContextCall() = default; + ~PythonContextCall() override; + + /// Initialize from either a single callable object, or a tuple with a + /// callable and optionally args and keywords + explicit PythonContextCall(PyObject* callable); + void Run(PyObject* args = nullptr); + void Run(const PythonRef& args) { Run(args.get()); } + auto Exists() const -> bool { return object_.exists(); } + auto GetObjectDescription() const -> std::string override; + void MarkDead(); + auto object() const -> const PythonRef& { return object_; } + auto file_loc() const -> const std::string& { return file_loc_; } + void LogContext(); + + private: + void GetTrace(); // we try to grab basic trace info + std::string file_loc_; + int line_{}; + bool dead_ = false; + PythonRef object_; + Context context_; +#if BA_DEBUG_BUILD + ContextTarget* context_target_sanity_test_{}; +#endif + static PythonContextCall* current_call_; +}; + +// FIXME: this should be static member var +extern PythonContextCall* g_current_python_call; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_PYTHON_CONTEXT_CALL_H_ diff --git a/src/ballistica/python/python_context_call_runnable.h b/src/ballistica/python/python_context_call_runnable.h new file mode 100644 index 00000000..0d1a99a2 --- /dev/null +++ b/src/ballistica/python/python_context_call_runnable.h @@ -0,0 +1,25 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_PYTHON_CONTEXT_CALL_RUNNABLE_H_ +#define BALLISTICA_PYTHON_PYTHON_CONTEXT_CALL_RUNNABLE_H_ + +#include "ballistica/python/python_context_call.h" + +namespace ballistica { + +// a simple runnable that stores and runs a python context call +class PythonContextCallRunnable : public Runnable { + public: + explicit PythonContextCallRunnable(PyObject* o) + : call(Object::New(o)) {} + Object::Ref call; + void Run() override { + assert(call.exists()); + call->Run(); + } + virtual ~PythonContextCallRunnable() = default; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_PYTHON_CONTEXT_CALL_RUNNABLE_H_ diff --git a/src/ballistica/python/python_ref.cc b/src/ballistica/python/python_ref.cc new file mode 100644 index 00000000..edd908cc --- /dev/null +++ b/src/ballistica/python/python_ref.cc @@ -0,0 +1,185 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/python/python_ref.h" + +#include "ballistica/math/vector2f.h" +#include "ballistica/python/python.h" +#include "ballistica/python/python_sys.h" + +namespace ballistica { + +// Ignore a few things that python macros do. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "RedundantCast" + +PythonRef::PythonRef(PyObject* obj_in, ReferenceBehavior b) { + assert(g_python); + assert(Python::HaveGIL()); + switch (b) { + case kSteal: + Steal(obj_in); + break; + case kStealWeak: + if (obj_in) { + Steal(obj_in); + } + break; + case kAcquire: + Acquire(obj_in); + break; + case kAcquireWeak: + if (obj_in) { + Acquire(obj_in); + break; + } + break; + } +} + +void PythonRef::Acquire(PyObject* obj_in) { + BA_PRECONDITION(obj_in); + assert(g_python); + assert(Python::HaveGIL()); + + // Assign and increment the new one before decrementing our old + // (in case its the same one or prev gets deallocated and accesses us + // somehow). + PyObject* prev = obj_; + Py_INCREF(obj_in); + obj_ = obj_in; + if (prev) { + Py_DECREF(prev); + } +} + +void PythonRef::Steal(PyObject* obj_in) { + BA_PRECONDITION(obj_in); + assert(g_python); + assert(Python::HaveGIL()); + + // Assign before decrementing the old + // (in case prev gets deallocated and accesses us somehow). + PyObject* prev = obj_; + obj_ = obj_in; + if (prev) { + Py_DECREF(prev); + } +} + +void PythonRef::Release() { + assert(g_python); + assert(Python::HaveGIL()); + + // Py_CLEAR uses a temp variable and assigns o to nullptr first + // so we're safe if the clear triggers something that (again) releases or + // destroys us. + if (obj_) { + Py_CLEAR(obj_); + } +} + +auto PythonRef::Str() const -> std::string { + assert(Python::HaveGIL()); + if (!obj_) { + return ""; + } + PyObject* obj = PyObject_Str(obj_); + if (!obj) { + return ""; + } + PythonRef s(obj, PythonRef::kSteal); + assert(PyUnicode_Check(obj)); // NOLINT (signed with bitwise) + return PyUnicode_AsUTF8(s.get()); +} + +auto PythonRef::Repr() const -> std::string { + assert(Python::HaveGIL()); + BA_PRECONDITION(obj_); + PythonRef s(PyObject_Repr(obj_), PythonRef::kSteal); + assert(PyUnicode_Check(s.get())); // NOLINT (signed with bitwise) + return PyUnicode_AsUTF8(s.get()); +} + +auto PythonRef::ValueAsString() const -> std::string { + assert(Python::HaveGIL()); + BA_PRECONDITION(obj_); + return Python::GetPyString(obj_); +} + +auto PythonRef::ValueAsInt() const -> int64_t { + assert(Python::HaveGIL()); + BA_PRECONDITION(obj_); + return Python::GetPyInt64(obj_); +} + +auto PythonRef::GetAttr(const char* name) const -> PythonRef { + assert(g_python); + assert(Python::HaveGIL()); + BA_PRECONDITION(obj_); + PyObject* val = PyObject_GetAttrString(get(), name); + if (!val) { + PyErr_Clear(); + throw Exception("Attribute not found: '" + std::string(name) + "'.", + PyExcType::kAttribute); + } + return PythonRef(val, PythonRef::kSteal); +} + +auto PythonRef::NewRef() const -> PyObject* { + assert(Python::HaveGIL()); + if (obj_ == nullptr) { + throw Exception("PythonRef::NewRef() called with nullptr obj_"); + } + Py_INCREF(obj_); + return obj_; +} + +auto PythonRef::CallableCheck() const -> bool { + BA_PRECONDITION(obj_); + assert(Python::HaveGIL()); + return static_cast(PyCallable_Check(obj_)); +} + +auto PythonRef::Call(PyObject* args, PyObject* keywds, bool print_errors) const + -> PythonRef { + assert(obj_); + assert(g_python); + assert(Python::HaveGIL()); + assert(CallableCheck()); + assert(args); + assert(PyTuple_Check(args)); // NOLINT (signed bitwise stuff) + assert(!keywds || PyDict_Check(keywds)); // NOLINT (signed bitwise) + PyObject* out = PyObject_Call(obj_, args, keywds); + if (!out) { + if (print_errors) { + // Save/restore error or it can mess with context print calls. + BA_PYTHON_ERROR_SAVE; + Log("ERROR: exception in Python call:"); + Python::LogContextAuto(); + BA_PYTHON_ERROR_RESTORE; + + // We pass zero here to avoid grabbing references to this exception + // which can cause objects to stick around and trip up our deletion checks + // (nodes, actors existing after their games have ended). + PyErr_PrintEx(0); + } + PyErr_Clear(); + } + return out ? PythonRef(out, PythonRef::kSteal) : PythonRef(); +} + +auto PythonRef::Call() const -> PythonRef { + return Call(g_python->obj(Python::ObjID::kEmptyTuple).get()); +} + +auto PythonRef::Call(const Vector2f& val) const -> PythonRef { + assert(Python::HaveGIL()); + PythonRef args(Py_BuildValue("((ff))", val.x, val.y), PythonRef::kSteal); + return Call(args); +} + +PythonRef::~PythonRef() { Release(); } + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/ballistica/python/python_ref.h b/src/ballistica/python/python_ref.h new file mode 100644 index 00000000..5f26d38c --- /dev/null +++ b/src/ballistica/python/python_ref.h @@ -0,0 +1,130 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_PYTHON_REF_H_ +#define BALLISTICA_PYTHON_PYTHON_REF_H_ + +#include + +#include "ballistica/ballistica.h" + +namespace ballistica { + +/// A simple managed Python object reference. +class PythonRef { + public: + /// Defines referencing behavior when creating new instances. + enum ReferenceBehavior { + /// Steal the provided object reference (and throw an Exception if it is + /// nullptr). + kSteal, + /// Steal the provided object reference or set as unreferenced if it is + /// nullptr. + kStealWeak, + /// Acquire a new reference to the provided object (and throw an Exception + /// if it is nullptr). + kAcquire, + /// Acquire a new reference to the provided object or set as unreferenced if + /// it is nullptr. + kAcquireWeak + }; + + /// Creates in an unreferenced state. + PythonRef() {} // NOLINT (using '= default' here errors) + + /// See ReferenceBehavior docs. + PythonRef(PyObject* other, ReferenceBehavior behavior); + + /// Copy constructor acquires a new reference (or sets as unreferenced) + /// depending on other. + PythonRef(const PythonRef& other) { *this = other; } + virtual ~PythonRef(); + + /// Assignment from another PythonRef acquires a reference to the object + /// referenced by other if there is one. If other has no reference, any + /// reference of ours is cleared to match. + auto operator=(const PythonRef& other) -> PythonRef& { + assert(this != &other); // Shouldn't be self-assigning. + if (other.exists()) { + Acquire(other.get()); + } else { + Release(); + } + return *this; + } + + /// Comparing to another PythonRef does a pointer comparison + /// (so basically the 'is' keyword in Python). + /// Note that two unreferenced PythonRefs will be equal. + auto operator==(const PythonRef& other) const -> bool { + return (get() == other.get()); + } + auto operator!=(const PythonRef& other) const -> bool { + return !(*this == other); + } + + /// Acquire a new reference to the passed object. Throws an exception if + /// nullptr is passed. + void Acquire(PyObject* obj); + + /// Steal the passed reference. Throws an Exception if nullptr is passed. + void Steal(PyObject* obj); + + /// Release the held reference (if one is held). + void Release(); + + /// Clear the ref without decrementing its count and return the raw PyObject* + auto HandOver() -> PyObject* { + assert(obj_); + PyObject* obj = obj_; + obj_ = nullptr; + return obj; + } + + /// Return the underlying PyObject pointer. + auto get() const -> PyObject* { return obj_; } + + /// Increment the ref-count for the underlying PyObject and return it as a + /// pointer. + auto NewRef() const -> PyObject*; + + /// Return whether we are pointing to a PyObject. + auto exists() const -> bool { return obj_ != nullptr; } + + /// Return a ref to an attribute on our PyObject or throw an Exception. + auto GetAttr(const char* name) const -> PythonRef; + + /// The equivalent of calling python str() on the contained PyObject. + auto Str() const -> std::string; + + /// The equivalent of calling repr() on the contained PyObject. + auto Repr() const -> std::string; + + /// For unicode, string, and ba.Lstr types, returns a utf8 string. + /// Throws an exception for other types. + auto ValueAsString() const -> std::string; + auto ValueAsInt() const -> int64_t; + + /// Returns whether the underlying PyObject is callable. + auto CallableCheck() const -> bool; + + /// Call the PyObject. On error, (optionally) prints errors and returns empty + /// ref. + auto Call(PyObject* args, PyObject* keywds = nullptr, + bool print_errors = true) const -> PythonRef; + auto Call(const PythonRef& args, const PythonRef& keywds = PythonRef(), + bool print_errors = true) const -> PythonRef { + return Call(args.get(), keywds.get(), print_errors); + } + auto Call() const -> PythonRef; + + /// Call with various args.. + auto Call(const Vector2f& val) const + -> PythonRef; // (val will be passed as tuple) + + private: + PyObject* obj_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_PYTHON_PYTHON_REF_H_ diff --git a/src/ballistica/python/python_sys.h b/src/ballistica/python/python_sys.h new file mode 100644 index 00000000..5afb2741 --- /dev/null +++ b/src/ballistica/python/python_sys.h @@ -0,0 +1,84 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_PYTHON_PYTHON_SYS_H_ +#define BALLISTICA_PYTHON_PYTHON_SYS_H_ + +// Any code that actually runs any python logic should include this. +// This header pulls in the actual python includes and also defines some handy +// macros and functions for working with python objects. + +// This is the ONE place we actually include python. +#include +#include +#include + +#include + +// Saving/restoring Python error state; useful when function PyObject_Str() +// or other functionality is needed during error reporting; by default it +// craps out when an error is set. +#define BA_PYTHON_ERROR_SAVE \ + PyObject* pes_perr = nullptr; \ + PyObject* pes_pvalue = nullptr; \ + PyObject* pes_ptraceback = nullptr; \ + PyErr_Fetch(&pes_perr, &pes_pvalue, &pes_ptraceback) + +#define BA_PYTHON_ERROR_RESTORE \ + PyErr_Restore(pes_perr, pes_pvalue, pes_ptraceback) + +// Some macros to handle/propagate C++ exceptions within Python calls. +#define BA_PYTHON_TRY \ + try { \ + ((void)0) + +// Set Python error state based on the caught C++ exception and returns null. +#define BA_PYTHON_CATCH \ + } \ + catch (const Exception& e) { \ + e.SetPyError(); \ + return nullptr; \ + } \ + catch (const std::exception& e) { \ + PyErr_SetString(PyExc_RuntimeError, GetShortExceptionDescription(e)); \ + return nullptr; \ + } \ + ((void)0) + +// For use in tp_new; sets Python err, frees aborted self, returns null. +#define BA_PYTHON_NEW_CATCH \ + } \ + catch (const Exception& e) { \ + e.SetPyError(); \ + Py_TYPE(self)->tp_free(reinterpret_cast(self)); \ + return nullptr; \ + } \ + catch (const std::exception& e) { \ + PyErr_SetString(PyExc_RuntimeError, GetShortExceptionDescription(e)); \ + Py_TYPE(self)->tp_free(reinterpret_cast(self)); \ + return nullptr; \ + } \ + ((void)0) + +// For use in tp_dealloc; simply prints the error. +#define BA_PYTHON_DEALLOC_CATCH \ + } \ + catch (const std::exception& e) { \ + Log(std::string("Error: tp_dealloc exception: ") \ + + GetShortExceptionDescription(e)); \ + } \ + ((void)0) + +// Sets Python error and returns -1. +#define BA_PYTHON_INT_CATCH \ + } \ + catch (const Exception& e) { \ + e.SetPyError(); \ + return -1; \ + } \ + catch (const std::exception& e) { \ + PyErr_SetString(PyExc_RuntimeError, GetShortExceptionDescription(e)); \ + return -1; \ + } \ + ((void)0) + +#endif // BALLISTICA_PYTHON_PYTHON_SYS_H_ diff --git a/tools/batools/build.py b/tools/batools/build.py index 259df7f0..f80e6d6c 100644 --- a/tools/batools/build.py +++ b/tools/batools/build.py @@ -30,10 +30,10 @@ class PipRequirement: PIP_REQUIREMENTS = [ PipRequirement(modulename='pylint', minversion=[2, 6, 0]), - PipRequirement(modulename='mypy', minversion=[0, 782]), + PipRequirement(modulename='mypy', minversion=[0, 790]), PipRequirement(modulename='yapf', minversion=[0, 30, 0]), PipRequirement(modulename='cpplint', minversion=[1, 5, 4]), - PipRequirement(modulename='pytest', minversion=[6, 0, 2]), + PipRequirement(modulename='pytest', minversion=[6, 1, 1]), PipRequirement(modulename='typing_extensions'), PipRequirement(modulename='pytz'), PipRequirement(modulename='yaml', pipname='PyYAML'), diff --git a/tools/efro/util.py b/tools/efro/util.py index 1140ba27..4cbeb88f 100644 --- a/tools/efro/util.py +++ b/tools/efro/util.py @@ -306,3 +306,45 @@ def make_hash(obj: Any) -> int: # NOTE: there is sorted works correctly because it compares only # unique first values (i.e. dict keys) return hash(tuple(frozenset(sorted(new_obj.items())))) + + +def asserttype(obj: Any, typ: Type[T]) -> T: + """Return an object typed as a given type. + + Assert is used to check its actual type, so only use this when + failures are not expected. Otherwise use checktype. + """ + assert isinstance(obj, typ) + return obj + + +def checktype(obj: Any, typ: Type[T]) -> T: + """Return an object typed as a given type. + + Always checks the type at runtime with isinstance and throws a TypeError + on failure. Use asserttype for more efficient (but less safe) equivalent. + """ + if not isinstance(obj, typ): + raise TypeError(f'Expected a {typ}; got a {type(obj)}.') + return obj + + +def assert_non_optional(obj: Optional[T]) -> T: + """Return an object with Optional typing removed. + + Assert is used to check its actual type, so only use this when + failures are not expected. Use check_non_optional otherwise. + """ + assert obj is not None + return obj + + +def check_non_optional(obj: Optional[T]) -> T: + """Return an object with Optional typing removed. + + Always checks the actual type and throws a TypeError on failure. + Use assert_non_optional for a more efficient (but less safe) equivalent. + """ + if obj is None: + raise TypeError('Got None value in check_non_optional.') + return obj