diff --git a/.efrocachemap b/.efrocachemap index 1bda2c16..90f2648f 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -3932,24 +3932,24 @@ "assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/b5/85/f8b6d0558ddb87267f34254b1450", "assets/build/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/1c/e1/4a1a2eddda2f4aebd5f8b64ab08e", "assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f", - "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/62/71/6b45cf5a4a3ed2c2f21853dd4ef5", - "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/8a/f5/60056a8ded058f143929f50aad4d", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/8f/87/5297d9be37d6faed52315a4098b6", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/86/8e/418f87c669cbef6b031e84caa24f", - "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/f0/c3/17bd74632240ce445290ad1d1e6b", - "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/13/5c/59fad7dcebaf88a92372b54ef35d", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b5/41/10ddaf61d0199cf19764b365233d", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/0c/62/1d6189df26dbc8293ccceae1763d", - "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/d3/1f/5944ecb4991c8c7bbcf4cc5b28e8", - "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/a1/bc/9ac66ab00360dd4c9353e67fdabf", - "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/e0/eb/0de6c758f379fc4d12ec30ed3e79", - "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/33/ca/c57c3668af1215c22fc29a52dd4f", + "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/f4/dc/e060b50e18cf9ea793a8024c71ea", + "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/8c/ba/6ff3079ed32a9bcf0b19ffdd5014", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/91/f9/fb6d662e6a5b517405e952cebb47", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/4f/bb/a249dad07ac24d901e263d4728ce", + "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/8d/39/9484a05296f6c1815f48a19f7f80", + "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/ce/13/3d5a5634dc85a9fff77f40eea079", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/64/a5/49ea3e527d625ec9891ac2d8dde8", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/62/13/c626103f654d4f30156b6440b547", + "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/33/ce/f6ce9603a9f24fa46363c19b0995", + "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/80/72/6844abdad72330180d9401023729", + "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/d6/db/e9ca1e2278872e8d49614597ca41", + "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/07/66/1eb6b6a7141a2d6a9a9dcb12573e", "build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c3/d7/627d518a92951cf7fe9fc0b9b3a0", "build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/de/49/2cfc34ac856737d903954db5571b", "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e8/9a/67cf9ece361ca2f5d338009bdbfc", "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/86/de/7d9c9a2b7bba34c630130ed759c9", - "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/28/7e/3aa3d21bb007a3fd44fee67d03e2", - "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/47/46/9a4ad3c7200e90fb58861ede9900", - "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3c/50/c9941b2059c3783861a6093f71a9", - "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/72/d9/4b66bf3eb0e3d0aa2e53de8d07e8" + "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3f/20/a97fd2c5fd3a3f0332e4a822c112", + "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d6/d2/6e5f87b787c1a33a3c1947f589e1", + "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a6/b6/e09b9690415a84f0602db60ef4de", + "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f0/f1/aa367945b9b426569dd8372a565e" } \ No newline at end of file diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index c695c388..94a8a102 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -637,6 +637,7 @@ evel eventid ewww + ewwww excludepowerups excludetypes excstr diff --git a/assets/src/ba_data/python/ba/__init__.py b/assets/src/ba_data/python/ba/__init__.py index b4a0c97b..8e3a6e9f 100644 --- a/assets/src/ba_data/python/ba/__init__.py +++ b/assets/src/ba_data/python/ba/__init__.py @@ -55,7 +55,7 @@ from ba._dualteamsession import DualTeamSession from ba._achievement import Achievement, AchievementSubsystem from ba._appconfig import AppConfig from ba._appdelegate import AppDelegate -from ba._apputils import is_browser_likely_available +from ba._apputils import is_browser_likely_available, garbage_collect from ba._campaign import Campaign from ba._gameutils import (GameTip, animate, animate_array, show_damage_count, timestring, cameraflash) diff --git a/assets/src/ba_data/python/ba/_apputils.py b/assets/src/ba_data/python/ba/_apputils.py index 422c2813..f3698a4e 100644 --- a/assets/src/ba_data/python/ba/_apputils.py +++ b/assets/src/ba_data/python/ba/_apputils.py @@ -3,6 +3,7 @@ """Utility functionality related to the overall operation of the app.""" from __future__ import annotations +import gc import os from typing import TYPE_CHECKING @@ -150,9 +151,8 @@ def handle_leftover_log_file() -> None: _error.print_exception('Error handling leftover log file.') -def garbage_collect(session_end: bool = True) -> None: - """Run an explicit pass of garbage collection.""" - import gc +def garbage_collect_session_end() -> None: + """Run explicit garbage collection with extra checks for session end.""" gc.collect() # Can be handy to print this to check for leaks between games. @@ -162,8 +162,19 @@ def garbage_collect(session_end: bool = True) -> None: print('PYTHON GC FOUND', len(gc.garbage), 'UNCOLLECTIBLE OBJECTS:') for i, obj in enumerate(gc.garbage): print(str(i) + ':', obj) - if session_end: - print_live_object_warnings('after session shutdown') + print_live_object_warnings('after session shutdown') + + +def garbage_collect() -> None: + """Run an explicit pass of garbage collection. + + category: General Utility Functions + + May also print warnings/etc. if collection takes too long or if + uncollectible objects are found (so use this instead of simply + gc.collect(). + """ + gc.collect() def print_live_object_warnings(when: Any, @@ -171,7 +182,6 @@ def print_live_object_warnings(when: Any, ignore_activity: ba.Activity = None) -> None: """Print warnings for remaining objects in the current context.""" # pylint: disable=cyclic-import - import gc from ba._session import Session from ba._actor import Actor from ba._activity import Activity diff --git a/assets/src/ba_data/python/ba/_general.py b/assets/src/ba_data/python/ba/_general.py index c88ffa19..a4ad9fdb 100644 --- a/assets/src/ba_data/python/ba/_general.py +++ b/assets/src/ba_data/python/ba/_general.py @@ -299,6 +299,27 @@ def verify_object_death(obj: object) -> None: timetype=TimeType.REAL) +def print_active_refs(obj: Any) -> None: + """Print info about things referencing a given object. + + Category: General Utility Functions + + Useful for tracking down cyclical references and causes for zombie objects. + """ + from types import FrameType + refs = list(gc.get_referrers(obj)) + print(f'{Clr.YLW}Active referrers to {obj}:{Clr.RST}') + for i, ref in enumerate(refs): + print(f'{Clr.YLW}#{i+1}:{Clr.BLU} {ref}{Clr.RST}') + # For certain types of objects such as stack frames, show what is + # keeping *them* alive too. + if isinstance(ref, FrameType): + print(f'{Clr.YLW} Active referrers to #{i+1}:{Clr.RST}') + refs2 = list(gc.get_referrers(ref)) + for j, ref2 in enumerate(refs2): + print(f'{Clr.YLW} #{j+1}:{Clr.BLU} {ref2}{Clr.RST}') + + def _verify_object_death(wref: ReferenceType) -> None: obj = wref() if obj is None: @@ -312,12 +333,7 @@ def _verify_object_death(wref: ReferenceType) -> None: print(f'{Clr.RED}Error: {name} not dying' f' when expected to: {Clr.BLD}{obj}{Clr.RST}') - refs = list(gc.get_referrers(obj)) - print(f'{Clr.YLW}Active References:{Clr.RST}') - i = 1 - for ref in refs: - print(f'{Clr.YLW} reference {i}:{Clr.BLU} {ref}{Clr.RST}') - i += 1 + print_active_refs(obj) def storagename(suffix: str = None) -> str: diff --git a/assets/src/ba_data/python/ba/_session.py b/assets/src/ba_data/python/ba/_session.py index 758b2333..fbed5f85 100644 --- a/assets/src/ba_data/python/ba/_session.py +++ b/assets/src/ba_data/python/ba/_session.py @@ -618,7 +618,7 @@ class Session: # to run garbage collection to clear out any circular dependency # loops. We keep this disabled normally to avoid non-deterministic # hitches. - garbage_collect(session_end=False) + garbage_collect() with _ba.Context(self): if can_show_ad_on_death: diff --git a/assets/src/ba_data/python/ba/ui/__init__.py b/assets/src/ba_data/python/ba/ui/__init__.py index 2907950c..22157577 100644 --- a/assets/src/ba_data/python/ba/ui/__init__.py +++ b/assets/src/ba_data/python/ba/ui/__init__.py @@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, cast, Type import _ba from ba._enums import TimeType +from ba._general import print_active_refs if TYPE_CHECKING: from typing import Optional, List, Any @@ -216,7 +217,9 @@ def ui_upkeep() -> None: print( 'WARNING:', obj, 'is still alive 5 second after its widget died;' - ' you probably have a memory leak.') + ' you might have a memory leak.') + print_active_refs(obj) + else: remainingchecks.append(check) ui.cleanupchecks = remainingchecks diff --git a/assets/src/ba_data/python/bastd/ui/coop/browser.py b/assets/src/ba_data/python/bastd/ui/coop/browser.py index a09f4f54..c9867e80 100644 --- a/assets/src/ba_data/python/bastd/ui/coop/browser.py +++ b/assets/src/ba_data/python/bastd/ui/coop/browser.py @@ -285,6 +285,10 @@ class CoopBrowserWindow(ba.Window): import bastd.ui.play as _unused10 def _update(self) -> None: + # Do nothing if we've somehow outlived our actual UI. + if not self._root_widget: + return + cur_time = ba.time(ba.TimeType.REAL) # If its been a while since we got a tournament update, consider the diff --git a/assets/src/ba_data/python/bastd/ui/gather/__init__.py b/assets/src/ba_data/python/bastd/ui/gather/__init__.py index 201e729f..7fb255a6 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/__init__.py +++ b/assets/src/ba_data/python/bastd/ui/gather/__init__.py @@ -240,7 +240,6 @@ class GatherWindow(ba.Window): ba.print_exception(f'Error saving state for {self}.') def _restore_state(self) -> None: - # pylint: disable=too-many-branches try: for tab in self._tabs.values(): tab.restore_state() @@ -251,24 +250,17 @@ class GatherWindow(ba.Window): assert isinstance(sel_name, (str, type(None))) current_tab = self.TabID.ABOUT gather_tab_val = ba.app.config.get('Gather Tab') - - if bool(False): - # EWWW: normally would just do this, but it seems to result in - # a reference to self sticking around somewhere. (presumably - # in the exception?). Should get to the bottom of this. - try: - stored_tab = self.TabID(gather_tab_val) - if stored_tab in self._tab_row.tabs: - current_tab = stored_tab - except ValueError: - pass - else: - # Falling back to this for now. - tab_vals = {t.value for t in self.TabID} - if gather_tab_val in tab_vals: - stored_tab = self.TabID(gather_tab_val) - if stored_tab in self._tab_row.tabs: - current_tab = stored_tab + try: + stored_tab = self.TabID(gather_tab_val) + if stored_tab in self._tab_row.tabs: + current_tab = stored_tab + except ValueError: + # EWWWW; this exception causes a dependency loop that won't + # go away until the next cyclical collection, which can + # keep us alive. Perhaps should rethink our garbage + # collection strategy, but for now just explicitly running + # a cycle. + ba.pushcall(ba.garbage_collect) self._set_tab(current_tab) if sel_name == 'Back': sel = self._back_button @@ -279,6 +271,12 @@ class GatherWindow(ba.Window): sel_tab_id = self.TabID(sel_name.split(':')[-1]) except ValueError: sel_tab_id = self.TabID.ABOUT + # EWWWW; this exception causes a dependency loop that won't + # go away until the next cyclical collection, which can + # keep us alive. Perhaps should rethink our garbage + # collection strategy, but for now just explicitly running + # a cycle. + ba.pushcall(ba.garbage_collect) sel = self._tab_row.tabs[sel_tab_id].button else: sel = self._tab_row.tabs[current_tab].button diff --git a/assets/src/ba_data/python/bastd/ui/gather/manualtab.py b/assets/src/ba_data/python/bastd/ui/gather/manualtab.py index 64c0c2e3..45dbb22f 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/manualtab.py +++ b/assets/src/ba_data/python/bastd/ui/gather/manualtab.py @@ -184,6 +184,12 @@ class ManualGatherTab(GatherTab): try: port = int(cast(str, ba.textwidget(query=port_textwidget))) except ValueError: + # EWWWW; this exception causes a dependency loop that won't + # go away until the next cyclical collection, which can + # keep us alive. Perhaps should rethink our garbage + # collection strategy, but for now just explicitly running + # a cycle. + ba.pushcall(ba.garbage_collect) port = -1 if port > 65535 or port < 0: ba.screenmessage(ba.Lstr(resource='internal.invalidPortErrorText'), diff --git a/assets/src/ba_data/python/bastd/ui/store/browser.py b/assets/src/ba_data/python/bastd/ui/store/browser.py index 12be6a00..3067dabc 100644 --- a/assets/src/ba_data/python/bastd/ui/store/browser.py +++ b/assets/src/ba_data/python/bastd/ui/store/browser.py @@ -1028,6 +1028,13 @@ class StoreBrowserWindow(ba.Window): current_tab = self.TabID(ba.app.config.get('Store Tab')) except ValueError: current_tab = self.TabID.CHARACTERS + + # EWWWW; this exception causes a dependency loop that won't + # go away until the next cyclical collection, which can keep + # us alive. Perhaps should rethink our garbage collection + # strategy, but for now just explicitly running a cycle. + ba.pushcall(ba.garbage_collect) + if self._show_tab is not None: current_tab = self._show_tab if sel_name == 'GetTickets' and self._get_tickets_button: diff --git a/assets/src/ba_data/python/bastd/ui/watch.py b/assets/src/ba_data/python/bastd/ui/watch.py index 011932f1..f27a1819 100644 --- a/assets/src/ba_data/python/bastd/ui/watch.py +++ b/assets/src/ba_data/python/bastd/ui/watch.py @@ -511,6 +511,12 @@ class WatchWindow(ba.Window): try: current_tab = self.TabID(ba.app.config.get('Watch Tab')) except ValueError: + # EWWWW; this exception causes a dependency loop that won't + # go away until the next cyclical collection, which can + # keep us alive. Perhaps should rethink our garbage + # collection strategy, but for now just explicitly running + # a cycle. + ba.pushcall(ba.garbage_collect) current_tab = self.TabID.MY_REPLAYS self._set_tab(current_tab) @@ -522,6 +528,12 @@ class WatchWindow(ba.Window): try: sel_tab_id = self.TabID(sel_name.split(':')[-1]) except ValueError: + # EWWWW; this exception causes a dependency loop that won't + # go away until the next cyclical collection, which can + # keep us alive. Perhaps should rethink our garbage + # collection strategy, but for now just explicitly running + # a cycle. + ba.pushcall(ba.garbage_collect) sel_tab_id = self.TabID.MY_REPLAYS sel = self._tab_row.tabs[sel_tab_id].button else: diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml index 3f1bbe00..de0b1de7 100644 --- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml @@ -13,8 +13,8 @@ ack'ed acked acks - aclass aclass's + aclass activityplayer addrs adjoint @@ -146,8 +146,8 @@ cmath cmds cmdvals - codewarrior codewarrior's + codewarrior cofnodes collapseable collidable diff --git a/docs/ba_module.md b/docs/ba_module.md index aad98319..25a9627a 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,5 +1,5 @@ -

last updated on 2020-10-29 for Ballistica version 1.5.27 build 20234

+

last updated on 2020-10-31 for Ballistica version 1.5.27 build 20236

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!


@@ -86,6 +86,7 @@