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