From 05a4793cd1c0687addf48b4011b6432c16b23587 Mon Sep 17 00:00:00 2001
From: Eric Froemling
Date: Sun, 24 Nov 2019 22:38:35 -0800
Subject: [PATCH] initial work on cloudtool functionality
---
.idea/ballisticacore.iml | 5 +-
.idea/dictionaries/ericf.xml | 12 +
Makefile | 15 +-
assets/.asset_manifest_1.json | 8 +-
assets/Makefile | 26 +-
assets/src/data/scripts/ba/__init__.py | 5 +-
assets/src/data/scripts/ba/_activity.py | 4 +-
assets/src/data/scripts/ba/_app.py | 6 +-
assets/src/data/scripts/ba/_benchmark.py | 2 +-
assets/src/data/scripts/ba/_coopsession.py | 2 +-
assets/src/data/scripts/ba/_dep.py | 523 ------------------
assets/src/data/scripts/ba/_dependency.py | 437 +++++++++++++++
assets/src/data/scripts/ba/_gameactivity.py | 20 +-
.../src/data/scripts/ba/{_maps.py => _map.py} | 0
assets/src/data/scripts/ba/_playlist.py | 6 +-
assets/src/data/scripts/ba/_session.py | 9 +-
assets/src/data/scripts/ba/_store.py | 4 +-
.../src/data/scripts/ba/_teambasesession.py | 2 +-
assets/src/data/scripts/ba/internal.py | 6 +-
.../src/data/scripts/bafoundation/__init__.py | 2 +-
assets/src/data/scripts/bafoundation/util.py | 8 +-
assets/src/data/scripts/bastd/mainmenu.py | 4 +-
docs/ba_module.md | 169 +++---
tools/cloudtool | 199 +++++++
tools/efrotools/snippets.py | 6 +-
tools/snippets | 24 +-
tools/update_project | 3 +-
27 files changed, 822 insertions(+), 685 deletions(-)
delete mode 100644 assets/src/data/scripts/ba/_dep.py
create mode 100644 assets/src/data/scripts/ba/_dependency.py
rename assets/src/data/scripts/ba/{_maps.py => _map.py} (100%)
create mode 100755 tools/cloudtool
diff --git a/.idea/ballisticacore.iml b/.idea/ballisticacore.iml
index f952ec4f..228f604b 100644
--- a/.idea/ballisticacore.iml
+++ b/.idea/ballisticacore.iml
@@ -5,6 +5,7 @@
+
@@ -26,13 +27,13 @@
+
-
@@ -66,4 +67,4 @@
-
+
\ No newline at end of file
diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml
index ec6ad318..c28b9a2e 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -14,6 +14,7 @@
abebabotabtn
+ accountnameaccountuiaccumaccumkillcount
@@ -94,6 +95,7 @@
audioopautodeskautogenerate
+ autonoassetsautoremoveautoretainautoselect
@@ -245,6 +247,8 @@
clionbinclioncodeclionroot
+ cloudtool
+ cloudtoolcmdclrbluclrendclrgrn
@@ -326,6 +330,7 @@
datamoduledatanamedatetimemodule
+ datetimesdaynumdayoffsetdbapi
@@ -339,6 +344,7 @@
depclsdepdatadepdatas
+ depentrydepsdepsetdepsets
@@ -871,6 +877,7 @@
locationvallocslogcat
+ logintokenlogitechlogputloofa
@@ -1087,6 +1094,7 @@
ouyapackagedirpackagedirs
+ packagenamepainttxtattrpalmospandoc
@@ -1365,6 +1373,7 @@
servercallthreadservercallthreadtypeservercfg
+ servercmdserverdialogservergetserverput
@@ -1552,6 +1561,7 @@
testmtestmagicmethodstestmock
+ testobjtestpatchtestpttestsealable
@@ -1678,6 +1688,7 @@
useragentstringuserbaseuserfunctions
+ utcnowutimensatvalidpgpkeysvalnew
@@ -1737,6 +1748,7 @@
wmsbewooooworkdir
+ workflowswpathwriteclasseswritefuncs
diff --git a/Makefile b/Makefile
index 89ad9da1..20aff64c 100644
--- a/Makefile
+++ b/Makefile
@@ -54,32 +54,31 @@ prereqs-clean:
assets: prereqs
@cd assets && make -j${CPUS}
-# Build only assets required for cmake builds (linux, mac)
+# Build assets required for cmake builds (linux, mac)
assets-cmake: prereqs
@cd assets && $(MAKE) -j${CPUS} cmake
-# Build only assets required for windows builds.
-# (honoring the WINDOWS_PLATFORM value)
+# Build assets required for WINDOWS_PLATFORM windows builds.
assets-windows: prereqs
@cd assets && $(MAKE) -j${CPUS} win-${WINDOWS_PLATFORM}
-# Build only assets required for Win32 windows builds.
+# Build assets required for Win32 windows builds.
assets-windows-Win32: prereqs
@cd assets && $(MAKE) -j${CPUS} win-Win32
-# Build only assets required for x64 windows builds.
+# Build assets required for x64 windows builds.
assets-windows-x64: prereqs
@cd assets && $(MAKE) -j${CPUS} win-x64
-# Build only assets required for mac xcode builds
+# Build assets required for mac xcode builds
assets-mac: prereqs
@cd assets && $(MAKE) -j${CPUS} mac
-# Build only assets required for ios.
+# Build assets required for ios.
assets-ios: prereqs
@cd assets && $(MAKE) -j${CPUS} ios
-# Build only assets required for android.
+# Build assets required for android.
assets-android: prereqs
@cd assets && $(MAKE) -j${CPUS} android
diff --git a/assets/.asset_manifest_1.json b/assets/.asset_manifest_1.json
index 32387d66..655304cf 100644
--- a/assets/.asset_manifest_1.json
+++ b/assets/.asset_manifest_1.json
@@ -14,7 +14,7 @@
"data/scripts/ba/__pycache__/_campaign.cpython-37.opt-1.pyc",
"data/scripts/ba/__pycache__/_coopgame.cpython-37.opt-1.pyc",
"data/scripts/ba/__pycache__/_coopsession.cpython-37.opt-1.pyc",
- "data/scripts/ba/__pycache__/_dep.cpython-37.opt-1.pyc",
+ "data/scripts/ba/__pycache__/_dependency.cpython-37.opt-1.pyc",
"data/scripts/ba/__pycache__/_enums.cpython-37.opt-1.pyc",
"data/scripts/ba/__pycache__/_error.cpython-37.opt-1.pyc",
"data/scripts/ba/__pycache__/_freeforallsession.cpython-37.opt-1.pyc",
@@ -27,7 +27,7 @@
"data/scripts/ba/__pycache__/_lang.cpython-37.opt-1.pyc",
"data/scripts/ba/__pycache__/_level.cpython-37.opt-1.pyc",
"data/scripts/ba/__pycache__/_lobby.cpython-37.opt-1.pyc",
- "data/scripts/ba/__pycache__/_maps.cpython-37.opt-1.pyc",
+ "data/scripts/ba/__pycache__/_map.cpython-37.opt-1.pyc",
"data/scripts/ba/__pycache__/_math.cpython-37.opt-1.pyc",
"data/scripts/ba/__pycache__/_messages.cpython-37.opt-1.pyc",
"data/scripts/ba/__pycache__/_meta.cpython-37.opt-1.pyc",
@@ -62,7 +62,7 @@
"data/scripts/ba/_campaign.py",
"data/scripts/ba/_coopgame.py",
"data/scripts/ba/_coopsession.py",
- "data/scripts/ba/_dep.py",
+ "data/scripts/ba/_dependency.py",
"data/scripts/ba/_enums.py",
"data/scripts/ba/_error.py",
"data/scripts/ba/_freeforallsession.py",
@@ -75,7 +75,7 @@
"data/scripts/ba/_lang.py",
"data/scripts/ba/_level.py",
"data/scripts/ba/_lobby.py",
- "data/scripts/ba/_maps.py",
+ "data/scripts/ba/_map.py",
"data/scripts/ba/_math.py",
"data/scripts/ba/_messages.py",
"data/scripts/ba/_meta.py",
diff --git a/assets/Makefile b/assets/Makefile
index 11b571b8..30647a4b 100644
--- a/assets/Makefile
+++ b/assets/Makefile
@@ -170,6 +170,7 @@ SCRIPT_TARGETS_PY_1 = \
build/data/scripts/ba/_profile.py \
build/data/scripts/ba/_error.py \
build/data/scripts/ba/_achievement.py \
+ build/data/scripts/ba/_map.py \
build/data/scripts/ba/_teambasesession.py \
build/data/scripts/ba/_gameutils.py \
build/data/scripts/ba/_activity.py \
@@ -197,14 +198,13 @@ SCRIPT_TARGETS_PY_1 = \
build/data/scripts/ba/_lobby.py \
build/data/scripts/ba/_stats.py \
build/data/scripts/ba/_input.py \
- build/data/scripts/ba/_dep.py \
build/data/scripts/ba/_level.py \
+ build/data/scripts/ba/_dependency.py \
build/data/scripts/ba/_general.py \
build/data/scripts/ba/_server.py \
build/data/scripts/ba/_account.py \
build/data/scripts/ba/_music.py \
build/data/scripts/ba/_lang.py \
- build/data/scripts/ba/_maps.py \
build/data/scripts/ba/_teamgame.py \
build/data/scripts/ba/ui/__init__.py \
build/data/scripts/bastd/mainmenu.py \
@@ -405,6 +405,7 @@ SCRIPT_TARGETS_PYC_1 = \
build/data/scripts/ba/__pycache__/_profile.cpython-37.opt-1.pyc \
build/data/scripts/ba/__pycache__/_error.cpython-37.opt-1.pyc \
build/data/scripts/ba/__pycache__/_achievement.cpython-37.opt-1.pyc \
+ build/data/scripts/ba/__pycache__/_map.cpython-37.opt-1.pyc \
build/data/scripts/ba/__pycache__/_teambasesession.cpython-37.opt-1.pyc \
build/data/scripts/ba/__pycache__/_gameutils.cpython-37.opt-1.pyc \
build/data/scripts/ba/__pycache__/_activity.cpython-37.opt-1.pyc \
@@ -432,14 +433,13 @@ SCRIPT_TARGETS_PYC_1 = \
build/data/scripts/ba/__pycache__/_lobby.cpython-37.opt-1.pyc \
build/data/scripts/ba/__pycache__/_stats.cpython-37.opt-1.pyc \
build/data/scripts/ba/__pycache__/_input.cpython-37.opt-1.pyc \
- build/data/scripts/ba/__pycache__/_dep.cpython-37.opt-1.pyc \
build/data/scripts/ba/__pycache__/_level.cpython-37.opt-1.pyc \
+ build/data/scripts/ba/__pycache__/_dependency.cpython-37.opt-1.pyc \
build/data/scripts/ba/__pycache__/_general.cpython-37.opt-1.pyc \
build/data/scripts/ba/__pycache__/_server.cpython-37.opt-1.pyc \
build/data/scripts/ba/__pycache__/_account.cpython-37.opt-1.pyc \
build/data/scripts/ba/__pycache__/_music.cpython-37.opt-1.pyc \
build/data/scripts/ba/__pycache__/_lang.cpython-37.opt-1.pyc \
- build/data/scripts/ba/__pycache__/_maps.cpython-37.opt-1.pyc \
build/data/scripts/ba/__pycache__/_teamgame.cpython-37.opt-1.pyc \
build/data/scripts/ba/ui/__pycache__/__init__.cpython-37.opt-1.pyc \
build/data/scripts/bastd/__pycache__/mainmenu.cpython-37.opt-1.pyc \
@@ -764,6 +764,11 @@ build/data/scripts/ba/__pycache__/_achievement.cpython-37.opt-1.pyc: \
@echo Compiling script: $^
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
+build/data/scripts/ba/__pycache__/_map.cpython-37.opt-1.pyc: \
+ build/data/scripts/ba/_map.py
+ @echo Compiling script: $^
+ @rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
+
build/data/scripts/ba/__pycache__/_teambasesession.cpython-37.opt-1.pyc: \
build/data/scripts/ba/_teambasesession.py
@echo Compiling script: $^
@@ -899,13 +904,13 @@ build/data/scripts/ba/__pycache__/_input.cpython-37.opt-1.pyc: \
@echo Compiling script: $^
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
-build/data/scripts/ba/__pycache__/_dep.cpython-37.opt-1.pyc: \
- build/data/scripts/ba/_dep.py
+build/data/scripts/ba/__pycache__/_level.cpython-37.opt-1.pyc: \
+ build/data/scripts/ba/_level.py
@echo Compiling script: $^
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
-build/data/scripts/ba/__pycache__/_level.cpython-37.opt-1.pyc: \
- build/data/scripts/ba/_level.py
+build/data/scripts/ba/__pycache__/_dependency.cpython-37.opt-1.pyc: \
+ build/data/scripts/ba/_dependency.py
@echo Compiling script: $^
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
@@ -934,11 +939,6 @@ build/data/scripts/ba/__pycache__/_lang.cpython-37.opt-1.pyc: \
@echo Compiling script: $^
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
-build/data/scripts/ba/__pycache__/_maps.cpython-37.opt-1.pyc: \
- build/data/scripts/ba/_maps.py
- @echo Compiling script: $^
- @rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
-
build/data/scripts/ba/__pycache__/_teamgame.cpython-37.opt-1.pyc: \
build/data/scripts/ba/_teamgame.py
@echo Compiling script: $^
diff --git a/assets/src/data/scripts/ba/__init__.py b/assets/src/data/scripts/ba/__init__.py
index 2598afb1..21b84b0a 100644
--- a/assets/src/data/scripts/ba/__init__.py
+++ b/assets/src/data/scripts/ba/__init__.py
@@ -43,7 +43,8 @@ from ba._actor import Actor
from ba._app import App
from ba._coopgame import CoopGameActivity
from ba._coopsession import CoopSession
-from ba._dep import Dep, Dependency, DepComponent, DepSet, AssetPackage
+from ba._dependency import (Dependency, DependencyComponent, DependencySet,
+ AssetPackage)
from ba._enums import TimeType, Permission, TimeFormat, SpecialChar
from ba._error import (UNHANDLED, print_exception, print_error, NotFoundError,
PlayerNotFoundError, NodeNotFoundError,
@@ -55,7 +56,7 @@ from ba._freeforallsession import FreeForAllSession
from ba._gameactivity import GameActivity
from ba._gameresults import TeamGameResults
from ba._lang import Lstr, setlanguage, get_valid_languages
-from ba._maps import Map, getmaps
+from ba._map import Map, getmaps
from ba._session import Session
from ba._stats import PlayerScoredMessage, PlayerRecord, Stats
from ba._team import Team
diff --git a/assets/src/data/scripts/ba/_activity.py b/assets/src/data/scripts/ba/_activity.py
index 1ec4a5e4..7fcd0b3c 100644
--- a/assets/src/data/scripts/ba/_activity.py
+++ b/assets/src/data/scripts/ba/_activity.py
@@ -25,7 +25,7 @@ import weakref
from typing import TYPE_CHECKING
import _ba
-from ba._dep import InstancedDepComponent
+from ba._dependency import DependencyComponent
if TYPE_CHECKING:
from weakref import ReferenceType
@@ -34,7 +34,7 @@ if TYPE_CHECKING:
from bastd.actor.respawnicon import RespawnIcon
-class Activity(InstancedDepComponent):
+class Activity(DependencyComponent):
"""Units of execution wrangled by a ba.Session.
Category: Gameplay Classes
diff --git a/assets/src/data/scripts/ba/_app.py b/assets/src/data/scripts/ba/_app.py
index 420462c6..29e09f4b 100644
--- a/assets/src/data/scripts/ba/_app.py
+++ b/assets/src/data/scripts/ba/_app.py
@@ -422,7 +422,7 @@ class App:
from ba import _appconfig
from ba import ui as bsui
from ba import _achievement
- from ba import _maps
+ from ba import _map
from ba import _meta
from ba import _music
from ba import _campaign
@@ -451,7 +451,7 @@ class App:
stdmaps.CragCastle, stdmaps.TowerD, stdmaps.HappyThoughts,
stdmaps.StepRightUp, stdmaps.Courtyard, stdmaps.Rampage
]:
- _maps.register_map(maptype)
+ _map.register_map(maptype)
if self.debug_build:
_apputils.suppress_debug_reports()
@@ -590,7 +590,7 @@ class App:
self.ran_on_launch = True
- from ba._dep import test_depset
+ from ba._dependency import test_depset
test_depset()
# print('GAME TYPES ARE', meta.get_game_types())
# _bs.quit()
diff --git a/assets/src/data/scripts/ba/_benchmark.py b/assets/src/data/scripts/ba/_benchmark.py
index f1418589..1d9ba1e7 100644
--- a/assets/src/data/scripts/ba/_benchmark.py
+++ b/assets/src/data/scripts/ba/_benchmark.py
@@ -43,7 +43,7 @@ def run_cpu_benchmark() -> None:
def __init__(self) -> None:
print('FIXME: BENCHMARK SESSION WOULD CALC DEPS.')
- depsets: Sequence[ba.DepSet] = []
+ depsets: Sequence[ba.DependencySet] = []
super().__init__(depsets)
diff --git a/assets/src/data/scripts/ba/_coopsession.py b/assets/src/data/scripts/ba/_coopsession.py
index c68aa044..0bb87e8b 100644
--- a/assets/src/data/scripts/ba/_coopsession.py
+++ b/assets/src/data/scripts/ba/_coopsession.py
@@ -73,7 +73,7 @@ class CoopSession(Session):
max_players = 4
print('FIXME: COOP SESSION WOULD CALC DEPS.')
- depsets: Sequence[ba.DepSet] = []
+ depsets: Sequence[ba.DependencySet] = []
super().__init__(depsets,
team_names=TEAM_NAMES,
diff --git a/assets/src/data/scripts/ba/_dep.py b/assets/src/data/scripts/ba/_dep.py
deleted file mode 100644
index 07d96fd3..00000000
--- a/assets/src/data/scripts/ba/_dep.py
+++ /dev/null
@@ -1,523 +0,0 @@
-# Copyright (c) 2011-2019 Eric Froemling
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-# -----------------------------------------------------------------------------
-"""Functionality related to object/asset dependencies."""
-
-from __future__ import annotations
-
-import weakref
-from typing import (Generic, TypeVar, TYPE_CHECKING, cast, Type, overload)
-
-import _ba
-from ba import _general
-
-if TYPE_CHECKING:
- from typing import Optional, Any, Dict, List, Set
- import ba
-
-T = TypeVar('T', bound='DepComponent')
-TI = TypeVar('TI', bound='InstancedDepComponent')
-TS = TypeVar('TS', bound='StaticDepComponent')
-
-
-class Dependency(Generic[T]):
- """A dependency on a DepComponent (with an optional config).
-
- Category: Dependency Classes
-
- This class is used to request and access functionality provided
- by other DepComponent classes from a DepComponent class.
- The class functions as a descriptor, allowing dependencies to
- be added at a class level much the same as properties or methods
- and then used with class instances to access those dependencies.
- For instance, if you do 'floofcls = ba.Dependency(FloofClass)' you
- would then be able to instantiate a FloofClass in your class's
- methods via self.floofcls().
- """
-
- def __init__(self, cls: Type[T], config: Any = None):
- """Instantiate a Dependency given a ba.DepComponent subtype.
-
- Optionally, an arbitrary object can be passed as 'config' to
- influence dependency calculation for the target class.
- """
- self.cls: Type[T] = cls
- self.config = config
- self._hash: Optional[int] = None
-
- def get_hash(self) -> int:
- """Return the dependency's hash, calculating it if necessary."""
- if self._hash is None:
- self._hash = _general.make_hash((self.cls, self.config))
- return self._hash
-
- # NOTE: it appears that mypy is currently not able to do overloads based
- # on the type of 'self', otherwise we could just overload this to
- # return different things based on self's type and avoid the need for
- # the fake dep classes below.
- # See https://github.com/python/mypy/issues/5320
- def __get__(self, obj: Any, cls: Any = None) -> Any:
- if obj is None:
- raise TypeError("Dependency must be accessed through an instance.")
-
- # We expect to be instantiated from an already living DepComponent
- # with valid dep-data in place..
- assert type is not None
- depdata = getattr(obj, '_depdata')
- if depdata is None:
- raise RuntimeError("Invalid dependency access.")
- assert isinstance(depdata, DepData)
-
- # Now look up the data for this particular dep
- depset = depdata.depset()
- assert isinstance(depset, DepSet)
- assert self._hash in depset.depdatas
- depdata = depset.depdatas[self._hash]
- assert isinstance(depdata, DepData)
- if depdata.valid is False:
- raise RuntimeError(
- f'Accessing DepComponent {depdata.cls} in an invalid state.')
- assert self.cls.dep_get_payload(depdata) is not None
- return self.cls.dep_get_payload(depdata)
-
-
-# We define a 'Dep' which at runtime simply aliases the Dependency class
-# but in type-checking points to two overloaded functions based on the argument
-# type. This lets the type system know what type of object the Dep represents.
-# (object instances in the case of StaticDep classes or object types in the
-# case of regular deps) At some point hopefully we can replace this with a
-# simple overload in Dependency.__get__ based on the type of self
-# (see note above).
-if not TYPE_CHECKING:
- Dep = Dependency
-else:
-
- class _InstanceDep(Dependency[TI]):
- """Fake stub we use to tell the type system we provide a type."""
-
- def __get__(self, obj: Any, cls: Any = None) -> Type[TI]:
- return cast(Type[TI], None)
-
- class _StaticDep(Dependency[TS]):
- """Fake stub we use to tell the type system we provide an instance."""
-
- def __get__(self, obj: Any, cls: Any = None) -> TS:
- return cast(TS, None)
-
- # pylint: disable=invalid-name
- # noinspection PyPep8Naming
- @overload
- def Dep(cls: Type[TI], config: Any = None) -> _InstanceDep[TI]:
- """test"""
- return _InstanceDep(cls, config)
-
- # noinspection PyPep8Naming
- @overload
- def Dep(cls: Type[TS], config: Any = None) -> _StaticDep[TS]:
- """test"""
- return _StaticDep(cls, config)
-
- # noinspection PyPep8Naming
- def Dep(cls: Any, config: Any = None) -> Any:
- """test"""
- return Dependency(cls, config)
-
- # pylint: enable=invalid-name
-
-
-class BoundDepComponent:
- """A DepComponent class bound to its DepSet data.
-
- Can be called to instantiate the class with its data properly in place."""
-
- def __init__(self, cls: Any, depdata: DepData):
- self.cls = cls
- # BoundDepComponents can be stored on depdatas so we use weakrefs
- # to avoid dependency cycles.
- self.depdata = weakref.ref(depdata)
-
- def __call__(self, *args: Any, **keywds: Any) -> Any:
- # We don't simply call our target type to instantiate it;
- # instead we manually call __new__ and then __init__.
- # This allows us to inject its data properly before __init__().
- obj = self.cls.__new__(self.cls, *args, **keywds)
- obj._depdata = self.depdata()
- assert isinstance(obj._depdata, DepData)
- obj.__init__(*args, **keywds)
- return obj
-
-
-class DepComponent:
- """Base class for all classes that can act as or use dependencies.
-
- category: Dependency Classes
- """
-
- _depdata: DepData
-
- def __init__(self) -> None:
- """Instantiate a DepComponent."""
-
- # For now lets issue a warning if these are instantiated without
- # data; we'll make this an error once we're no longer seeing warnings.
- depdata = getattr(self, '_depdata', None)
- if depdata is None:
- print(f'FIXME: INSTANTIATING DEP CLASS {type(self)} DIRECTLY.')
-
- self.context = _ba.Context('current')
-
- @classmethod
- def is_present(cls, config: Any = None) -> bool:
- """Return whether this component/config is present on this device."""
- del config # Unused here.
- return True
-
- @classmethod
- def get_dynamic_deps(cls, config: Any = None) -> List[Dependency]:
- """Return any dynamically-calculated deps for this component/config.
-
- Deps declared statically as part of the class do not need to be
- included here; this is only for additional deps that may vary based
- on the dep config value. (for instance a map required by a game type)
- """
- del config # Unused here.
- return []
-
- @classmethod
- def dep_get_payload(cls, depdata: DepData) -> Any:
- """Return user-facing data for a loaded dep.
-
- If this dep does not yet have a 'payload' value, it should
- be generated and cached. Otherwise the existing value
- should be returned.
- This is the value given for a DepComponent when accessed
- through a Dependency instance on a live object, etc.
- """
- del depdata # Unused here.
-
-
-class DepData:
- """Data associated with a dependency in a dependency set."""
-
- def __init__(self, depset: DepSet, dep: Dependency[T]):
- # Note: identical Dep/config pairs will share data, so the dep
- # entry on a given Dep may not point to.
- self.cls = dep.cls
- self.config = dep.config
-
- # Arbitrary data for use by dependencies in the resolved set
- # (the static instance for static-deps, etc).
- self.payload: Any = None
- self.valid: bool = False
-
- # Weakref to the depset that includes us (to avoid ref loop).
- self.depset = weakref.ref(depset)
-
-
-class DepSet(Generic[TI]):
- """Set of resolved dependencies and their associated data."""
-
- def __init__(self, root: Dependency[TI]):
- self.root = root
- self._resolved = False
-
- # Dependency data indexed by hash.
- self.depdatas: Dict[int, DepData] = {}
-
- # Instantiated static-components.
- self.static_instances: List[StaticDepComponent] = []
-
- def __del__(self) -> None:
- # When our dep-set goes down, clear out all dep-data payloads
- # so we can throw errors if anyone tries to use them anymore.
- for depdata in self.depdatas.values():
- depdata.payload = None
- depdata.valid = False
-
- def resolve(self) -> None:
- """Resolve the total set of required dependencies for the set.
-
- Raises a ba.DependencyError if dependencies are missing (or other
- Exception types on other errors).
- """
-
- if self._resolved:
- raise Exception("DepSet has already been resolved.")
-
- print('RESOLVING DEP SET')
-
- # First, recursively expand out all dependencies.
- self._resolve(self.root, 0)
-
- # Now, if any dependencies are not present, raise an Exception
- # telling exactly which ones (so hopefully they'll be able to be
- # downloaded/etc.
- missing = [
- Dependency(entry.cls, entry.config)
- for entry in self.depdatas.values()
- if not entry.cls.is_present(entry.config)
- ]
- if missing:
- from ba._error import DependencyError
- raise DependencyError(missing)
-
- self._resolved = True
- print('RESOLVE SUCCESS!')
-
- def get_asset_package_ids(self) -> Set[str]:
- """Return the set of asset-package-ids required by this dep-set.
-
- Must be called on a resolved dep-set.
- """
- ids: Set[str] = set()
- if not self._resolved:
- raise Exception('Must be called on a resolved dep-set.')
- for entry in self.depdatas.values():
- if issubclass(entry.cls, AssetPackage):
- assert isinstance(entry.config, str)
- ids.add(entry.config)
- return ids
-
- def load(self) -> Type[TI]:
- """Attach the resolved set to the current context.
-
- Returns a wrapper which can be used to instantiate the root dep.
- """
- # NOTE: stuff below here should probably go in a separate 'instantiate'
- # method or something.
- if not self._resolved:
- raise Exception("Can't instantiate an unresolved DepSet")
-
- # Go through all of our dep entries and give them a chance to
- # preload whatever they want.
- for entry in self.depdatas.values():
- # First mark everything as valid so recursive loads don't fail.
- assert entry.valid is False
- entry.valid = True
- for entry in self.depdatas.values():
- # Do a get on everything which will init all payloads
- # in the proper order recursively.
- # NOTE: should we guard for recursion here?...
- entry.cls.dep_get_payload(entry)
-
- # NOTE: like above, we're cheating here and telling the type
- # system we're simply returning the root dependency class, when
- # actually it's a bound-dependency wrapper containing its data/etc.
- # ..Should fix if/when mypy is smart enough to preserve type safety
- # on the wrapper's __call__()
- rootdata = self.depdatas[self.root.get_hash()]
- return cast(Type[TI], BoundDepComponent(self.root.cls, rootdata))
-
- def _resolve(self, dep: Dependency[T], recursion: int) -> None:
-
- # Watch for wacky infinite dep loops.
- if recursion > 10:
- raise Exception('Max recursion reached')
-
- hashval = dep.get_hash()
-
- if hashval in self.depdatas:
- # Found an already resolved one; we're done here.
- return
-
- # Add our entry before we recurse so we don't repeat add it if
- # there's a dependency loop.
- self.depdatas[hashval] = DepData(self, dep)
-
- # Grab all Dependency instances we find in the class.
- subdeps = [
- cls for cls in dep.cls.__dict__.values()
- if isinstance(cls, Dependency)
- ]
-
- # ..and add in any dynamic ones it provides.
- subdeps += dep.cls.get_dynamic_deps(dep.config)
- for subdep in subdeps:
- self._resolve(subdep, recursion + 1)
-
-
-class InstancedDepComponent(DepComponent):
- """Base class for DepComponents intended to be instantiated as needed."""
-
- @classmethod
- def dep_get_payload(cls, depdata: DepData) -> Any:
- """Data provider override; returns a BoundDepComponent."""
- if depdata.payload is None:
- # The payload we want for ourself in the dep-set is simply
- # the bound-def that users can use to instantiate our class
- # with its data properly intact. We could also just store
- # the class and instantiate one of these each time.
- depdata.payload = BoundDepComponent(cls, depdata)
- return depdata.payload
-
-
-class StaticDepComponent(DepComponent):
- """Base for DepComponents intended to be instantiated once and shared."""
-
- @classmethod
- def dep_get_payload(cls, depdata: DepData) -> Any:
- """Data provider override; returns shared instance."""
- if depdata.payload is None:
- # We want to share a single instance of our object with anything
- # in the set that requested it, so create a temp bound-dep and
- # create an instance from that.
- depcls = BoundDepComponent(cls, depdata)
-
- # Instances have a strong ref to depdata so we can't give
- # depdata a strong reference to it without creating a cycle.
- # We also can't just weak-ref the instance or else it won't be
- # kept alive. Our solution is to stick strong refs to all static
- # components somewhere on the DepSet.
- instance = depcls()
- assert depdata.depset
- depset2 = depdata.depset()
- assert depset2 is not None
- depset2.static_instances.append(instance)
- depdata.payload = weakref.ref(instance)
- assert isinstance(depdata.payload, weakref.ref)
- payload = depdata.payload()
- if payload is None:
- raise RuntimeError(
- f'Accessing DepComponent {cls} in an invalid state.')
- return payload
-
-
-class AssetPackage(StaticDepComponent):
- """DepComponent representing a bundled package of game assets."""
-
- def __init__(self) -> None:
- super().__init__()
- # pylint: disable=no-member
- assert isinstance(self._depdata.config, str)
- self.package_id = self._depdata.config
- print(f'LOADING ASSET PACKAGE {self.package_id}')
-
- @classmethod
- def is_present(cls, config: Any = None) -> bool:
- assert isinstance(config, str)
-
- # Temp: hard-coding for a single asset-package at the moment.
- if config == 'stdassets@1':
- return True
- return False
-
- def gettexture(self, name: str) -> ba.Texture:
- """Load a named ba.Texture from the AssetPackage.
-
- Behavior is similar to ba.gettexture()
- """
- return _ba.get_package_texture(self, name)
-
- def getmodel(self, name: str) -> ba.Model:
- """Load a named ba.Model from the AssetPackage.
-
- Behavior is similar to ba.getmodel()
- """
- return _ba.get_package_model(self, name)
-
- def getcollidemodel(self, name: str) -> ba.CollideModel:
- """Load a named ba.CollideModel from the AssetPackage.
-
- Behavior is similar to ba.getcollideModel()
- """
- return _ba.get_package_collide_model(self, name)
-
- def getsound(self, name: str) -> ba.Sound:
- """Load a named ba.Sound from the AssetPackage.
-
- Behavior is similar to ba.getsound()
- """
- return _ba.get_package_sound(self, name)
-
- def getdata(self, name: str) -> ba.Data:
- """Load a named ba.Data from the AssetPackage.
-
- Behavior is similar to ba.getdata()
- """
- return _ba.get_package_data(self, name)
-
-
-class TestClassFactory(StaticDepComponent):
- """Another test dep-obj."""
-
- _assets = Dep(AssetPackage, 'stdassets@1')
-
- def __init__(self) -> None:
- super().__init__()
- print("Instantiating TestClassFactory")
- self.tex = self._assets.gettexture('black')
- self.model = self._assets.getmodel('landMine')
- self.sound = self._assets.getsound('error')
- self.data = self._assets.getdata('langdata')
-
-
-class TestClassObj(InstancedDepComponent):
- """Another test dep-obj."""
-
-
-class TestClass(InstancedDepComponent):
- """A test dep-obj."""
-
- _actorclass = Dep(TestClassObj)
- _factoryclass = Dep(TestClassFactory, 123)
- _factoryclass2 = Dep(TestClassFactory, 124)
-
- def __init__(self, arg: int) -> None:
- super().__init__()
- del arg
- self._actor = self._actorclass()
- print('got actor', self._actor)
- print('have factory', self._factoryclass)
- print('have factory2', self._factoryclass2)
-
-
-def test_depset() -> None:
- """Test call to try this stuff out..."""
- # noinspection PyUnreachableCode
- if False: # pylint: disable=using-constant-test
- print('running test_depset()...')
-
- def doit() -> None:
- from ba._error import DependencyError
- depset = DepSet(Dep(TestClass))
- resolved = False
- try:
- depset.resolve()
- resolved = True
- except DependencyError as exc:
- for dep in exc.deps:
- if dep.cls is AssetPackage:
- print('MISSING PACKAGE', dep.config)
- else:
- raise Exception('unknown dependency error for ' +
- str(dep.cls))
- except Exception as exc:
- print('DepSet resolve failed with exc type:', type(exc))
- if resolved:
- testclass = depset.load()
- instance = testclass(123)
- print("INSTANTIATED ROOT:", instance)
-
- doit()
-
- # To test this, add prints on __del__ for stuff used above;
- # everything should be dead at this point if we have no cycles.
- print('everything should be cleaned up...')
- _ba.quit()
diff --git a/assets/src/data/scripts/ba/_dependency.py b/assets/src/data/scripts/ba/_dependency.py
new file mode 100644
index 00000000..9e4a45f3
--- /dev/null
+++ b/assets/src/data/scripts/ba/_dependency.py
@@ -0,0 +1,437 @@
+# Copyright (c) 2011-2019 Eric Froemling
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+# -----------------------------------------------------------------------------
+"""Functionality related to object/asset dependencies."""
+
+from __future__ import annotations
+
+import weakref
+from typing import (Generic, TypeVar, TYPE_CHECKING)
+
+import _ba
+from ba import _general
+
+if TYPE_CHECKING:
+ from typing import Optional, Any, Dict, List, Set, Type
+ from weakref import ReferenceType
+ import ba
+
+T = TypeVar('T', bound='DependencyComponent')
+
+
+class Dependency(Generic[T]):
+ """A dependency on a DependencyComponent (with an optional config).
+
+ Category: Dependency Classes
+
+ This class is used to request and access functionality provided
+ by other DependencyComponent classes from a DependencyComponent class.
+ The class functions as a descriptor, allowing dependencies to
+ be added at a class level much the same as properties or methods
+ and then used with class instances to access those dependencies.
+ For instance, if you do 'floofcls = ba.Dependency(FloofClass)' you
+ would then be able to instantiate a FloofClass in your class's
+ methods via self.floofcls().
+ """
+
+ def __init__(self, cls: Type[T], config: Any = None):
+ """Instantiate a Dependency given a ba.DependencyComponent type.
+
+ Optionally, an arbitrary object can be passed as 'config' to
+ influence dependency calculation for the target class.
+ """
+ self.cls: Type[T] = cls
+ self.config = config
+ self._hash: Optional[int] = None
+
+ def get_hash(self) -> int:
+ """Return the dependency's hash, calculating it if necessary."""
+ if self._hash is None:
+ self._hash = _general.make_hash((self.cls, self.config))
+ return self._hash
+
+ def __get__(self, obj: Any, cls: Any = None) -> T:
+ if not isinstance(obj, DependencyComponent):
+ if obj is None:
+ raise TypeError(
+ "Dependency must be accessed through an instance.")
+ raise TypeError(
+ f"Dependency cannot be added to class of type {type(obj)}"
+ " (class must inherit from ba.DependencyComponent).")
+
+ # We expect to be instantiated from an already living
+ # DependencyComponent with valid dep-data in place..
+ assert cls is not None
+
+ # Get the DependencyEntry this instance is associated with and from
+ # there get back to the DependencySet
+ entry = getattr(obj, '_dep_entry')
+ if entry is None:
+ raise RuntimeError("Invalid dependency access.")
+ entry = entry()
+ assert isinstance(entry, DependencyEntry)
+ depset = entry.depset()
+ assert isinstance(depset, DependencySet)
+
+ if not depset.resolved:
+ raise RuntimeError(
+ "Can't access data on an unresolved DependencySet.")
+
+ # Look up the data in the set based on the hash for this Dependency.
+ assert self._hash in depset.entries
+ entry = depset.entries[self._hash]
+ assert isinstance(entry, DependencyEntry)
+ retval = entry.get_component()
+ assert isinstance(retval, self.cls)
+ return retval
+
+
+class DependencyComponent:
+ """Base class for all classes that can act as or use dependencies.
+
+ category: Dependency Classes
+ """
+
+ _dep_entry: ReferenceType[DependencyEntry]
+
+ def __init__(self) -> None:
+ """Instantiate a DependencyComponent."""
+
+ # For now lets issue a warning if these are instantiated without
+ # a dep-entry; we'll make this an error once we're no longer
+ # seeing warnings.
+ entry = getattr(self, '_dep_entry', None)
+ if entry is None:
+ print(f'FIXME: INSTANTIATING DEP CLASS {type(self)} DIRECTLY.')
+
+ @classmethod
+ def dep_is_present(cls, config: Any = None) -> bool:
+ """Return whether this component/config is present on this device."""
+ del config # Unused here.
+ return True
+
+ @classmethod
+ def get_dynamic_deps(cls, config: Any = None) -> List[Dependency]:
+ """Return any dynamically-calculated deps for this component/config.
+
+ Deps declared statically as part of the class do not need to be
+ included here; this is only for additional deps that may vary based
+ on the dep config value. (for instance a map required by a game type)
+ """
+ del config # Unused here.
+ return []
+
+
+class DependencyEntry:
+ """Data associated with a dependency/config pair in a ba.DependencySet."""
+
+ def __del__(self) -> None:
+ print('~DepEntry()', self.cls)
+
+ def __init__(self, depset: DependencySet, dep: Dependency[T]):
+ print("DepEntry()", dep.cls)
+ self.cls = dep.cls
+ self.config = dep.config
+
+ # Arbitrary data for use by dependencies in the resolved set
+ # (the static instance for static-deps, etc).
+ self.component: Optional[DependencyComponent] = None
+
+ # Weakref to the depset that includes us (to avoid ref loop).
+ self.depset = weakref.ref(depset)
+
+ def get_component(self) -> DependencyComponent:
+ """Return the component instance, creating it if necessary."""
+ if self.component is None:
+ # We don't simply call our type to instantiate our instance;
+ # instead we manually call __new__ and then __init__.
+ # This allows us to inject its data properly before __init__().
+ print('creating', self.cls)
+ instance = self.cls.__new__(self.cls)
+ # pylint: disable=protected-access
+ instance._dep_entry = weakref.ref(self)
+ instance.__init__()
+
+ assert self.depset
+ depset = self.depset()
+ assert depset is not None
+ self.component = instance
+ component = self.component
+ assert isinstance(component, self.cls)
+ if component is None:
+ raise RuntimeError(f'Accessing DependencyComponent {self.cls} '
+ 'in an invalid state.')
+ return component
+
+
+class DependencySet(Generic[T]):
+ """Set of resolved dependencies and their associated data.
+
+ To use DependencyComponents, a set must be created, resolved, and then
+ loaded. The DependencyComponents are only valid while the set remains
+ in existence.
+ """
+
+ def __init__(self, root_dependency: Dependency[T]):
+ print('DepSet()')
+ self._root_dependency = root_dependency
+ self._resolved = False
+ self._loaded = False
+
+ # Dependency data indexed by hash.
+ self.entries: Dict[int, DependencyEntry] = {}
+
+ def __del__(self) -> None:
+ print("~DepSet()")
+
+ def resolve(self) -> None:
+ """Resolve the complete set of required dependencies for this set.
+
+ Raises a ba.DependencyError if dependencies are missing (or other
+ Exception types on other errors).
+ """
+
+ if self._resolved:
+ raise Exception("DependencySet has already been resolved.")
+
+ print('RESOLVING DEP SET')
+
+ # First, recursively expand out all dependencies.
+ self._resolve(self._root_dependency, 0)
+
+ # Now, if any dependencies are not present, raise an Exception
+ # telling exactly which ones (so hopefully they'll be able to be
+ # downloaded/etc.
+ missing = [
+ Dependency(entry.cls, entry.config)
+ for entry in self.entries.values()
+ if not entry.cls.dep_is_present(entry.config)
+ ]
+ if missing:
+ from ba._error import DependencyError
+ raise DependencyError(missing)
+
+ self._resolved = True
+ print('RESOLVE SUCCESS!')
+
+ @property
+ def resolved(self) -> bool:
+ """Whether this set has been successfully resolved."""
+ return self._resolved
+
+ def get_asset_package_ids(self) -> Set[str]:
+ """Return the set of asset-package-ids required by this dep-set.
+
+ Must be called on a resolved dep-set.
+ """
+ ids: Set[str] = set()
+ if not self._resolved:
+ raise Exception('Must be called on a resolved dep-set.')
+ for entry in self.entries.values():
+ if issubclass(entry.cls, AssetPackage):
+ assert isinstance(entry.config, str)
+ ids.add(entry.config)
+ return ids
+
+ def load(self) -> None:
+ """Instantiate all DependencyComponents in the set.
+
+ Returns a wrapper which can be used to instantiate the root dep.
+ """
+ # NOTE: stuff below here should probably go in a separate 'instantiate'
+ # method or something.
+ if not self._resolved:
+ raise RuntimeError("Can't load an unresolved DependencySet")
+
+ for entry in self.entries.values():
+ # Do a get on everything which will init all payloads
+ # in the proper order recursively.
+ entry.get_component()
+
+ self._loaded = True
+
+ @property
+ def root(self) -> T:
+ """The instantiated root DependencyComponent instance for the set."""
+ if not self._loaded:
+ raise RuntimeError("DependencySet is not loaded.")
+
+ rootdata = self.entries[self._root_dependency.get_hash()].component
+ assert isinstance(rootdata, self._root_dependency.cls)
+ return rootdata
+
+ def _resolve(self, dep: Dependency[T], recursion: int) -> None:
+
+ # Watch for wacky infinite dep loops.
+ if recursion > 10:
+ raise Exception('Max recursion reached')
+
+ hashval = dep.get_hash()
+
+ if hashval in self.entries:
+ # Found an already resolved one; we're done here.
+ return
+
+ # Add our entry before we recurse so we don't repeat add it if
+ # there's a dependency loop.
+ self.entries[hashval] = DependencyEntry(self, dep)
+
+ # Grab all Dependency instances we find in the class.
+ subdeps = [
+ cls for cls in dep.cls.__dict__.values()
+ if isinstance(cls, Dependency)
+ ]
+
+ # ..and add in any dynamic ones it provides.
+ subdeps += dep.cls.get_dynamic_deps(dep.config)
+ for subdep in subdeps:
+ self._resolve(subdep, recursion + 1)
+
+
+class AssetPackage(DependencyComponent):
+ """DependencyComponent representing a bundled package of game assets."""
+
+ def __init__(self) -> None:
+ super().__init__()
+ # pylint: disable=no-member
+
+ # This is used internally by the get_package_xxx calls.
+ self.context = _ba.Context('current')
+
+ entry = self._dep_entry()
+ assert entry is not None
+ assert isinstance(entry.config, str)
+ self.package_id = entry.config
+ print(f'LOADING ASSET PACKAGE {self.package_id}')
+
+ @classmethod
+ def dep_is_present(cls, config: Any = None) -> bool:
+ assert isinstance(config, str)
+
+ # Temp: hard-coding for a single asset-package at the moment.
+ if config == 'stdassets@1':
+ return True
+ return False
+
+ def gettexture(self, name: str) -> ba.Texture:
+ """Load a named ba.Texture from the AssetPackage.
+
+ Behavior is similar to ba.gettexture()
+ """
+ return _ba.get_package_texture(self, name)
+
+ def getmodel(self, name: str) -> ba.Model:
+ """Load a named ba.Model from the AssetPackage.
+
+ Behavior is similar to ba.getmodel()
+ """
+ return _ba.get_package_model(self, name)
+
+ def getcollidemodel(self, name: str) -> ba.CollideModel:
+ """Load a named ba.CollideModel from the AssetPackage.
+
+ Behavior is similar to ba.getcollideModel()
+ """
+ return _ba.get_package_collide_model(self, name)
+
+ def getsound(self, name: str) -> ba.Sound:
+ """Load a named ba.Sound from the AssetPackage.
+
+ Behavior is similar to ba.getsound()
+ """
+ return _ba.get_package_sound(self, name)
+
+ def getdata(self, name: str) -> ba.Data:
+ """Load a named ba.Data from the AssetPackage.
+
+ Behavior is similar to ba.getdata()
+ """
+ return _ba.get_package_data(self, name)
+
+
+class TestClassFactory(DependencyComponent):
+ """Another test dep-obj."""
+
+ _assets = Dependency(AssetPackage, 'stdassets@1')
+
+ def __init__(self) -> None:
+ super().__init__()
+ print("Instantiating TestClassFactory")
+ self.tex = self._assets.gettexture('black')
+ self.model = self._assets.getmodel('landMine')
+ self.sound = self._assets.getsound('error')
+ self.data = self._assets.getdata('langdata')
+
+
+class TestClassObj(DependencyComponent):
+ """Another test dep-obj."""
+
+
+class TestClass(DependencyComponent):
+ """A test dep-obj."""
+
+ _testclass = Dependency(TestClassObj)
+ _factoryclass = Dependency(TestClassFactory, 123)
+ _factoryclass2 = Dependency(TestClassFactory, 123)
+
+ def __del__(self) -> None:
+ print("~TestClass()")
+
+ def __init__(self) -> None:
+ super().__init__()
+ print('TestClass()')
+ self._actor = self._testclass
+ print('got actor', self._actor)
+ print('have factory', self._factoryclass)
+ print('have factory2', self._factoryclass2)
+
+
+def test_depset() -> None:
+ """Test call to try this stuff out..."""
+ # noinspection PyUnreachableCode
+ if False: # pylint: disable=using-constant-test
+ print('running test_depset()...')
+
+ def doit() -> None:
+ from ba._error import DependencyError
+ depset = DependencySet(Dependency(TestClass))
+ try:
+ depset.resolve()
+ except DependencyError as exc:
+ for dep in exc.deps:
+ if dep.cls is AssetPackage:
+ print('MISSING ASSET PACKAGE', dep.config)
+ else:
+ raise Exception('unknown dependency error for ' +
+ str(dep.cls))
+ except Exception as exc:
+ print('DependencySet resolve failed with exc type:', type(exc))
+ if depset.resolved:
+ depset.load()
+ testobj = depset.root
+ # instance = testclass(123)
+ print("INSTANTIATED ROOT:", testobj)
+
+ doit()
+
+ # To test this, add prints on __del__ for stuff used above;
+ # everything should be dead at this point if we have no cycles.
+ print('everything should be cleaned up...')
+ _ba.quit()
diff --git a/assets/src/data/scripts/ba/_gameactivity.py b/assets/src/data/scripts/ba/_gameactivity.py
index b6b052e1..51e092f0 100644
--- a/assets/src/data/scripts/ba/_gameactivity.py
+++ b/assets/src/data/scripts/ba/_gameactivity.py
@@ -250,9 +250,9 @@ class GameActivity(Activity):
implementation; should return a list of map names valid
for this game-type for the given ba.Session type.
"""
- from ba import _maps
+ from ba import _map
del sessiontype # unused arg
- return _maps.getmaps("melee")
+ return _map.getmaps("melee")
@classmethod
def get_config_display_string(cls, config: Dict[str, Any]) -> ba.Lstr:
@@ -261,7 +261,7 @@ class GameActivity(Activity):
This is used when viewing game-lists or showing what game
is up next in a series.
"""
- from ba import _maps
+ from ba import _map
name = cls.get_display_string(config['settings'])
# in newer configs, map is in settings; it used to be in the
@@ -270,15 +270,15 @@ class GameActivity(Activity):
sval = Lstr(value="${NAME} @ ${MAP}",
subs=[('${NAME}', name),
('${MAP}',
- _maps.get_map_display_string(
- _maps.get_filtered_map_name(
+ _map.get_map_display_string(
+ _map.get_filtered_map_name(
config['settings']['map'])))])
elif 'map' in config:
sval = Lstr(value="${NAME} @ ${MAP}",
subs=[('${NAME}', name),
('${MAP}',
- _maps.get_map_display_string(
- _maps.get_filtered_map_name(config['map'])))
+ _map.get_map_display_string(
+ _map.get_filtered_map_name(config['map'])))
])
else:
print('invalid game config - expected map entry under settings')
@@ -295,7 +295,7 @@ class GameActivity(Activity):
def __init__(self, settings: Dict[str, Any]):
"""Instantiate the Activity."""
- from ba import _maps
+ from ba import _map
super().__init__(settings)
# Set some defaults.
@@ -313,7 +313,7 @@ class GameActivity(Activity):
else:
# If settings doesn't specify a map, pick a random one from the
# list of supported ones.
- unowned_maps = _maps.get_unowned_maps()
+ unowned_maps = _map.get_unowned_maps()
valid_maps: List[str] = [
m for m in self.get_supported_maps(type(self.session))
if m not in unowned_maps
@@ -322,7 +322,7 @@ class GameActivity(Activity):
_ba.screenmessage(Lstr(resource='noValidMapsErrorText'))
raise Exception("No valid maps")
map_name = valid_maps[random.randrange(len(valid_maps))]
- self._map_type = _maps.get_map_class(map_name)
+ self._map_type = _map.get_map_class(map_name)
self._map_type.preload()
self._map: Optional[ba.Map] = None
self._powerup_drop_timer: Optional[ba.Timer] = None
diff --git a/assets/src/data/scripts/ba/_maps.py b/assets/src/data/scripts/ba/_map.py
similarity index 100%
rename from assets/src/data/scripts/ba/_maps.py
rename to assets/src/data/scripts/ba/_map.py
diff --git a/assets/src/data/scripts/ba/_playlist.py b/assets/src/data/scripts/ba/_playlist.py
index f06d013b..33e84f32 100644
--- a/assets/src/data/scripts/ba/_playlist.py
+++ b/assets/src/data/scripts/ba/_playlist.py
@@ -47,13 +47,13 @@ def filter_playlist(playlist: PlaylistType,
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
from ba import _meta
- from ba import _maps
+ from ba import _map
from ba import _general
from ba import _gameactivity
goodlist: List[Dict] = []
unowned_maps: Sequence[str]
if remove_unowned or mark_unowned:
- unowned_maps = _maps.get_unowned_maps()
+ unowned_maps = _map.get_unowned_maps()
unowned_game_types = _meta.get_unowned_game_types()
else:
unowned_maps = []
@@ -69,7 +69,7 @@ def filter_playlist(playlist: PlaylistType,
entry['settings']['map'] = entry['map']
del entry['map']
# update old map names to new ones
- entry['settings']['map'] = _maps.get_filtered_map_name(
+ entry['settings']['map'] = _map.get_filtered_map_name(
entry['settings']['map'])
if remove_unowned and entry['settings']['map'] in unowned_maps:
continue
diff --git a/assets/src/data/scripts/ba/_session.py b/assets/src/data/scripts/ba/_session.py
index 19ab3b1a..f48008bf 100644
--- a/assets/src/data/scripts/ba/_session.py
+++ b/assets/src/data/scripts/ba/_session.py
@@ -83,7 +83,7 @@ class Session:
players: List[ba.Player]
def __init__(self,
- depsets: Sequence[ba.DepSet],
+ depsets: Sequence[ba.DependencySet],
team_names: Sequence[str] = None,
team_colors: Sequence[Sequence[float]] = None,
use_team_colors: bool = True,
@@ -94,7 +94,7 @@ class Session:
# pylint: disable=too-many-branches
"""Instantiate a session.
- depsets should be a sequence of successfully resolved ba.DepSet
+ depsets should be a sequence of successfully resolved ba.DependencySet
instances; one for each ba.Activity the session may potentially run.
"""
# pylint: disable=too-many-locals
@@ -105,7 +105,7 @@ class Session:
from ba._gameactivity import GameActivity
from ba._team import Team
from ba._error import DependencyError
- from ba._dep import Dep, AssetPackage
+ from ba._dependency import Dependency, AssetPackage
print(' WOULD LOOK AT DEP SETS', depsets)
@@ -131,7 +131,8 @@ class Session:
# throw a combined exception if we found anything missing
if missing_asset_packages:
raise DependencyError([
- Dep(AssetPackage, set_id) for set_id in missing_asset_packages
+ Dependency(AssetPackage, set_id)
+ for set_id in missing_asset_packages
])
# ok; looks like our dependencies check out.
diff --git a/assets/src/data/scripts/ba/_store.py b/assets/src/data/scripts/ba/_store.py
index d7e570a4..c91d0a85 100644
--- a/assets/src/data/scripts/ba/_store.py
+++ b/assets/src/data/scripts/ba/_store.py
@@ -40,7 +40,7 @@ def get_store_item_name_translated(item_name: str) -> ba.Lstr:
"""Return a ba.Lstr for a store item name."""
# pylint: disable=cyclic-import
from ba import _lang
- from ba import _maps
+ from ba import _map
item_info = get_store_item(item_name)
if item_name.startswith('characters.'):
return _lang.Lstr(translate=('characterNames', item_info['character']))
@@ -50,7 +50,7 @@ def get_store_item_name_translated(item_name: str) -> ba.Lstr:
_lang.Lstr(resource='titleText'))])
if item_name.startswith('maps.'):
map_type: Type[ba.Map] = item_info['map_type']
- return _maps.get_map_display_string(map_type.name)
+ return _map.get_map_display_string(map_type.name)
if item_name.startswith('games.'):
gametype: Type[ba.GameActivity] = item_info['gametype']
return gametype.get_display_string()
diff --git a/assets/src/data/scripts/ba/_teambasesession.py b/assets/src/data/scripts/ba/_teambasesession.py
index 559d5cb0..82e805d0 100644
--- a/assets/src/data/scripts/ba/_teambasesession.py
+++ b/assets/src/data/scripts/ba/_teambasesession.py
@@ -66,7 +66,7 @@ class TeamBaseSession(Session):
team_colors = None
print('FIXME: TEAM BASE SESSION WOULD CALC DEPS.')
- depsets: Sequence[ba.DepSet] = []
+ depsets: Sequence[ba.DependencySet] = []
super().__init__(depsets,
team_names=team_names,
team_colors=team_colors,
diff --git a/assets/src/data/scripts/ba/internal.py b/assets/src/data/scripts/ba/internal.py
index ec461d37..e4ac82ab 100644
--- a/assets/src/data/scripts/ba/internal.py
+++ b/assets/src/data/scripts/ba/internal.py
@@ -27,9 +27,9 @@ defensively) in mods.
# pylint: disable=unused-import
-from ba._maps import (get_unowned_maps, get_map_class, register_map,
- preload_map_preview_media, get_map_display_string,
- get_filtered_map_name)
+from ba._map import (get_unowned_maps, get_map_class, register_map,
+ preload_map_preview_media, get_map_display_string,
+ get_filtered_map_name)
from ba._appconfig import commit_app_config
from ba._input import (get_device_value, get_input_map_hash,
get_input_device_config)
diff --git a/assets/src/data/scripts/bafoundation/__init__.py b/assets/src/data/scripts/bafoundation/__init__.py
index 23088c51..1a006083 100644
--- a/assets/src/data/scripts/bafoundation/__init__.py
+++ b/assets/src/data/scripts/bafoundation/__init__.py
@@ -18,4 +18,4 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
-"""Functionality shared between Ballistica client and server components."""
+"""Functionality shared between all Ballistica clients, servers, and tools."""
diff --git a/assets/src/data/scripts/bafoundation/util.py b/assets/src/data/scripts/bafoundation/util.py
index 2c8650d5..8af8f0cd 100644
--- a/assets/src/data/scripts/bafoundation/util.py
+++ b/assets/src/data/scripts/bafoundation/util.py
@@ -36,7 +36,13 @@ TRET = TypeVar('TRET')
def utc_now() -> datetime.datetime:
- """Get offset-aware current utc time."""
+ """Get offset-aware current utc time.
+
+ This should be used for all datetimes getting sent over the network,
+ used with the entity system, etc.
+ (datetime.utcnow() gives a utc time value, but it is not timezone-aware
+ which makes it less safe to use)
+ """
return datetime.datetime.now(datetime.timezone.utc)
diff --git a/assets/src/data/scripts/bastd/mainmenu.py b/assets/src/data/scripts/bastd/mainmenu.py
index ad083671..441c1b46 100644
--- a/assets/src/data/scripts/bastd/mainmenu.py
+++ b/assets/src/data/scripts/bastd/mainmenu.py
@@ -46,7 +46,7 @@ if TYPE_CHECKING:
class MainMenuActivity(ba.Activity):
"""Activity showing the rotating main menu bg stuff."""
- _stdassets = ba.Dep(ba.AssetPackage, 'stdassets@1')
+ _stdassets = ba.Dependency(ba.AssetPackage, 'stdassets@1')
def on_transition_in(self) -> None:
super().on_transition_in()
@@ -903,7 +903,7 @@ class MainMenuSession(ba.Session):
def __init__(self) -> None:
# Gather dependencies we'll need (just our activity).
- self._activity_deps = ba.DepSet(ba.Dep(MainMenuActivity))
+ self._activity_deps = ba.DependencySet(ba.Dependency(MainMenuActivity))
super().__init__([self._activity_deps])
self._locked = False
diff --git a/docs/ba_module.md b/docs/ba_module.md
index f3af8e73..f65d20f4 100644
--- a/docs/ba_module.md
+++ b/docs/ba_module.md
@@ -1,6 +1,6 @@
-
-
last updated on 2019-11-12 for Ballistica version 1.5.0 build 20001
+
+
last updated on 2019-11-21 for Ballistica version 1.5.0 build 20001
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!
@@ -1386,59 +1386,15 @@ Note that this call will block if the data has not yet been loaded,
so it can be beneficial to plan a short bit of time between when
the data object is requested and when it's value is accessed.
If this dep does not yet have a 'payload' value, it should
-be generated and cached. Otherwise the existing value
-should be returned.
-This is the value given for a DepComponent when accessed
-through a Dependency instance on a live object, etc.
get_dynamic_deps(config: Any = None) -> List[Dependency]
-
-
Return any dynamically-calculated deps for this component/config.
-
-
Deps declared statically as part of the class do not need to be
-included here; this is only for additional deps that may vary based
-on the dep config value. (for instance a map required by a game type)
This class is used to request and access functionality provided
- by other DepComponent classes from a DepComponent class.
+ by other DependencyComponent classes from a DependencyComponent class.
The class functions as a descriptor, allowing dependencies to
be added at a class level much the same as properties or methods
and then used with class instances to access those dependencies.
@@ -1452,7 +1408,7 @@ on the dep config value. (for instance a map required by a game type)
get_dynamic_deps(config: Any = None) -> List[Dependency]
+
+
Return any dynamically-calculated deps for this component/config.
+
+
Deps declared statically as part of the class do not need to be
+included here; this is only for additional deps that may vary based
+on the dep config value. (for instance a map required by a game type)
To use DependencyComponents, a set must be created, resolved, and then
+ loaded. The DependencyComponents are only valid while the set remains
+ in existence.
+
diff --git a/tools/cloudtool b/tools/cloudtool
new file mode 100755
index 00000000..fe4b47a1
--- /dev/null
+++ b/tools/cloudtool
@@ -0,0 +1,199 @@
+#!/usr/bin/env python3.7
+# Copyright (c) 2011-2019 Eric Froemling
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+# -----------------------------------------------------------------------------
+"""A tool for interacting with ballistica's cloud services.
+This facilitates workflows such as creating asset-bundles, etc.
+"""
+
+from __future__ import annotations
+
+import sys
+import os
+from typing import TYPE_CHECKING
+import urllib.request
+import urllib.parse
+import urllib.error
+from dataclasses import dataclass
+from pathlib import Path
+import json
+
+if TYPE_CHECKING:
+ from typing import Optional, Dict, Any
+
+# Version is sent to the master-server with all commands. Can be incremented
+# if we need to change behavior server-side to go along with client changes.
+VERSION = 1
+
+TOOL_NAME = 'cloudtool'
+
+# Set CLOUDTOOL_LOCAL env var to 1 to test with a locally-run master-server.
+MASTER_SERVER_ADDRESS = ('http://localhost:23524'
+ if os.environ.get('CLOUDTOOL_LOCAL') == '1' else
+ 'https://bamaster.appspot.com')
+USER_AGENT_STRING = 'cloudtool'
+CACHE_DIR = Path('.cache/cloudtool')
+CACHE_DATA_PATH = Path(CACHE_DIR, 'state')
+
+CLRHDR = '\033[95m' # Header.
+CLRGRN = '\033[92m' # Green.
+CLRBLU = '\033[94m' # Glue.
+CLRRED = '\033[91m' # Red.
+CLREND = '\033[0m' # End.
+
+CMD_LOGIN = 'login'
+CMD_LOGOUT = 'logout'
+CMD_HELP = 'help'
+
+
+@dataclass
+class StateData:
+ """Persistent state data stored to disk."""
+ login_token: Optional[str] = None
+
+
+@dataclass
+class Response:
+ """Response data from the master server for a command."""
+ message: Optional[str]
+ error: Optional[str]
+ data: Any
+
+
+class CleanError(Exception):
+ """Exception resulting in a clean error string print and exit."""
+
+
+class App:
+ """Context for a run of the tool."""
+
+ def __init__(self) -> None:
+ self._state = StateData()
+
+ def run(self) -> None:
+ """Run the tool."""
+
+ # Make reasonably sure we're being run from project root.
+ if not os.path.exists(f'tools/{TOOL_NAME}'):
+ raise CleanError(
+ 'This tool must be run from ballistica project root.')
+
+ self._load_cache()
+
+ if len(sys.argv) < 2:
+ raise CleanError(
+ f'Invalid args. Run "cloudtool help" for usage info.')
+
+ cmd = sys.argv[1]
+ if cmd == CMD_LOGIN:
+ self.do_login()
+ elif cmd == CMD_LOGOUT:
+ self.do_logout()
+ else:
+ # For all other commands, simply pass them to the server verbatim.
+ self.do_misc_command()
+
+ self._save_cache()
+
+ def _load_cache(self) -> None:
+ if not os.path.exists(CACHE_DATA_PATH):
+ return
+ try:
+ with open(CACHE_DATA_PATH, 'r') as infile:
+ self._state = StateData(**json.loads(infile.read()))
+ except Exception:
+ print(CLRRED +
+ f'Error loading {TOOL_NAME} data; resetting to defaults.' +
+ CLREND)
+
+ def _save_cache(self) -> None:
+ if not CACHE_DIR.exists():
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
+ with open(CACHE_DATA_PATH, 'w') as outfile:
+ outfile.write(json.dumps(self._state.__dict__))
+
+ def _servercmd(self, cmd: str, data: Dict) -> Response:
+ """Issue a command to the server and get a response."""
+
+ # We do all communication through POST requests to the server.
+ response_raw = urllib.request.urlopen(
+ urllib.request.Request(
+ (MASTER_SERVER_ADDRESS + '/cloudtoolcmd'),
+ urllib.parse.urlencode({
+ 'c': cmd,
+ 'v': VERSION,
+ 't': json.dumps(self._state.login_token),
+ 'd': json.dumps(data)
+ }).encode(), {'User-Agent': USER_AGENT_STRING}))
+ output = json.loads(response_raw.read().decode())
+ assert isinstance(output, dict)
+ response = Response(message=output['m'],
+ data=output['d'],
+ error=output['e'])
+
+ # Handle errors and messages which are common to all command types.
+ if response.error is not None:
+ raise CleanError(response.error)
+
+ if response.message is not None:
+ print(response.message)
+
+ return response
+
+ def do_login(self) -> None:
+ """Run the login command."""
+
+ if len(sys.argv) != 3:
+ raise CleanError('Expected a login code.')
+
+ login_code = sys.argv[2]
+ response = self._servercmd('login', {'c': login_code})
+
+ # If the command returned cleanly, we should have a token we can use
+ # to log in.
+ token = response.data['logintoken']
+ assert isinstance(token, str)
+
+ aname = response.data['accountname']
+ assert isinstance(aname, str)
+
+ print(f'{CLRGRN}Now logged in as {aname}.{CLREND}')
+ self._state.login_token = token
+
+ def do_logout(self) -> None:
+ """Run the logout command."""
+ self._state.login_token = None
+ print(f'{CLRGRN}Cloudtool is now logged out.{CLREND}')
+
+ def do_misc_command(self) -> None:
+ """Run a miscellaneous command."""
+
+ # We don't do anything special with the response here; the normal
+ # error-handling/message-printing is all that happens.
+ self._servercmd('misc', {'a': sys.argv[1:]})
+
+
+if __name__ == '__main__':
+ try:
+ App().run()
+ except CleanError as exc:
+ if str(exc):
+ print(f'{CLRRED}{exc}{CLREND}')
+ sys.exit(-1)
diff --git a/tools/efrotools/snippets.py b/tools/efrotools/snippets.py
index 293612c3..3f5e2121 100644
--- a/tools/efrotools/snippets.py
+++ b/tools/efrotools/snippets.py
@@ -447,9 +447,9 @@ def makefile_target_list() -> None:
return ' - ' + doc
return doc
- print('--------------------------\n'
- 'Available Makefile Targets\n'
- '--------------------------')
+ print('----------------------\n'
+ 'Available Make Targets\n'
+ '----------------------')
entries: List[_Entry] = []
for i, line in enumerate(lines):
diff --git a/tools/snippets b/tools/snippets
index e19b3a04..536fff9a 100755
--- a/tools/snippets
+++ b/tools/snippets
@@ -711,14 +711,16 @@ def checkenv() -> None:
raise CleanError('pip (for {python_bin}) is required.')
# Check for some required python modules.
- for modname, minver in [
- ('pylint', [2, 4, 3]),
- ('mypy', [0, 740]),
- ('yapf', [0, 28, 0]),
- ('typing_extensions', None),
- ('pytz', None),
+ for modname, minver, packagename in [
+ ('pylint', [2, 4, 4], None),
+ ('mypy', [0, 740], None),
+ ('yapf', [0, 28, 0], None),
+ ('typing_extensions', None, None),
+ ('pytz', None, None),
+ ('yaml', None, 'PyYAML'),
]:
-
+ if packagename is None:
+ packagename = modname
if minver is not None:
results = subprocess.run(f'{python_bin} -m {modname} --version',
shell=True,
@@ -730,9 +732,9 @@ def checkenv() -> None:
check=False,
capture_output=True)
if results.returncode != 0:
- raise CleanError(
- f'{modname} (for {python_bin}) is required.\n'
- f'To install it, try: "{python_bin} -m pip install {modname}"')
+ raise CleanError(f'{packagename} (for {python_bin}) is required.\n'
+ f'To install it, try: "{python_bin}'
+ f' -m pip install {packagename}"')
if minver is not None:
ver_line = results.stdout.decode().splitlines()[0]
assert modname in ver_line
@@ -740,7 +742,7 @@ def checkenv() -> None:
assert len(vnums) == len(minver)
if vnums < minver:
raise CleanError(
- f'{modname} ver. {_vstr(minver)} or newer required;'
+ f'{packagename} ver. {_vstr(minver)} or newer required;'
f' found {_vstr(vnums)}')
print('Environment ok.', flush=True)
diff --git a/tools/update_project b/tools/update_project
index afe1cb22..f25c9d6e 100755
--- a/tools/update_project
+++ b/tools/update_project
@@ -19,8 +19,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
-"""
-This script acts as a 'meta' Makefile for the project. It is in charge
+"""This script acts as a 'meta' Makefile for the project. It is in charge
of generating Makefiles, IDE project files, procedurally generated source
files, etc. based on the current structure of the project.
It can also perform sanity checks or cleanup tasks.