mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-02-07 08:03:30 +08:00
Separating out ads functionality
This commit is contained in:
parent
d098291948
commit
eba70f0c81
@ -3932,24 +3932,24 @@
|
|||||||
"assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/b5/85/f8b6d0558ddb87267f34254b1450",
|
"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/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",
|
"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/72/93/a41a9777570bee533ab3259f1597",
|
"build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/dd/d1/e12c5331256ecffcd7684d6d2963",
|
||||||
"build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/2d/40/964c6b36393b12b459433dcda36c",
|
"build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c5/35/8d811eec1a47f4e70d4b27eb3bf5",
|
||||||
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e3/d6/e286d413e60a2e6a43b5773d6441",
|
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/6f/43/d0f61fb34a76e11b9ebd4334038d",
|
||||||
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/20/97/d4a3ccde682ec984a67b3d683f54",
|
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a4/56/614e09d8ab86355f0d65c1ce76fe",
|
||||||
"build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/82/25/c1d9b277444a9aa46be5f1eec44a",
|
"build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/f7/c5/f695953295c2d79dfb01555aea89",
|
||||||
"build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/9c/05/36d55f280f9676e3098ed9aa6a78",
|
"build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/7b/19/ff1bba3a148b6e0f0823d7d7c7bf",
|
||||||
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/8a/8c/0a3d0b30186fec5b189a5f0407cb",
|
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f7/dd/d5e4d872192060d46821d6911c13",
|
||||||
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/30/1c/f554338c290026fb45bd52e9e1a7",
|
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/64/58/962c2e707ee66d310848f375796a",
|
||||||
"build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/fa/e5/e624e868b0fc00a2413ae4b9432c",
|
"build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/db/58/65c487facba1de6ba8ce65f19f01",
|
||||||
"build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/e8/1e/418931e11d12072c869808579592",
|
"build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/40/cb/afd71350fdc9827ca99baab73001",
|
||||||
"build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/21/21/de5c6e124de4675b11d0c73ceb28",
|
"build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/f2/05/e9252b68a96ee418da35dd4d2530",
|
||||||
"build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/11/f3/037c95d8bbb74730bdef686526a5",
|
"build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/bb/a2/e1f5b3f561a08bb09cc3ffb2492f",
|
||||||
"build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1a/4a/bd980abb5c7078d1e144d19fa63d",
|
"build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/66/c9/3b04209f599dea8b8ca4be7d3404",
|
||||||
"build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fd/cd/3397d744c7405740df4d4ae567f0",
|
"build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0d/3b/b7b46c3131cff8a40dfaa001af38",
|
||||||
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/34/c2/24a9ab7d513acdb8ebaa3251611f",
|
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/57/40/0c1d88af3ce14e0f8870ab9ac7ad",
|
||||||
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/54/40/8e0cc564f49f8963803834ec7995",
|
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8a/72/02b4eddf662001f05f98288d4ad4",
|
||||||
"build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/62/55/765d22f045d0d4d31a02aeffec7b",
|
"build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fc/bc/51529aac7531d1a62cf13eb79153",
|
||||||
"build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4b/76/aa0554648b65da797eb30b9c4699",
|
"build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ea/7b/de9ce5284627cc77b2fffc354a66",
|
||||||
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/91/dc/c415fc19cfa0395cc200a5a72e2c",
|
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/96/00/78b64146e33ec35dcde9c278328a",
|
||||||
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cd/d8/ec9c1c0955cc62ea574a4b42a707"
|
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/6d/25/0e5918fa1eb7285538d14051761c"
|
||||||
}
|
}
|
||||||
12
.idea/dictionaries/ericf.xml
generated
12
.idea/dictionaries/ericf.xml
generated
@ -29,8 +29,8 @@
|
|||||||
<w>achname</w>
|
<w>achname</w>
|
||||||
<w>achs</w>
|
<w>achs</w>
|
||||||
<w>acinstance</w>
|
<w>acinstance</w>
|
||||||
<w>ack'ed</w>
|
|
||||||
<w>ack</w>
|
<w>ack</w>
|
||||||
|
<w>ack'ed</w>
|
||||||
<w>acked</w>
|
<w>acked</w>
|
||||||
<w>acks</w>
|
<w>acks</w>
|
||||||
<w>acnt</w>
|
<w>acnt</w>
|
||||||
@ -152,8 +152,8 @@
|
|||||||
<w>bacommon</w>
|
<w>bacommon</w>
|
||||||
<w>badguy</w>
|
<w>badguy</w>
|
||||||
<w>bafoundation</w>
|
<w>bafoundation</w>
|
||||||
<w>ballistica's</w>
|
|
||||||
<w>ballistica</w>
|
<w>ballistica</w>
|
||||||
|
<w>ballistica's</w>
|
||||||
<w>ballisticacore</w>
|
<w>ballisticacore</w>
|
||||||
<w>ballisticacorecb</w>
|
<w>ballisticacorecb</w>
|
||||||
<w>bamaster</w>
|
<w>bamaster</w>
|
||||||
@ -800,8 +800,8 @@
|
|||||||
<w>gamedata</w>
|
<w>gamedata</w>
|
||||||
<w>gameinstance</w>
|
<w>gameinstance</w>
|
||||||
<w>gamemap</w>
|
<w>gamemap</w>
|
||||||
<w>gamepad's</w>
|
|
||||||
<w>gamepad</w>
|
<w>gamepad</w>
|
||||||
|
<w>gamepad's</w>
|
||||||
<w>gamepadadvanced</w>
|
<w>gamepadadvanced</w>
|
||||||
<w>gamepads</w>
|
<w>gamepads</w>
|
||||||
<w>gamepadselect</w>
|
<w>gamepadselect</w>
|
||||||
@ -1189,8 +1189,8 @@
|
|||||||
<w>lsqlite</w>
|
<w>lsqlite</w>
|
||||||
<w>lssl</w>
|
<w>lssl</w>
|
||||||
<w>lstart</w>
|
<w>lstart</w>
|
||||||
<w>lstr's</w>
|
|
||||||
<w>lstr</w>
|
<w>lstr</w>
|
||||||
|
<w>lstr's</w>
|
||||||
<w>lstrs</w>
|
<w>lstrs</w>
|
||||||
<w>lsval</w>
|
<w>lsval</w>
|
||||||
<w>ltex</w>
|
<w>ltex</w>
|
||||||
@ -1823,8 +1823,8 @@
|
|||||||
<w>sessionname</w>
|
<w>sessionname</w>
|
||||||
<w>sessionplayer</w>
|
<w>sessionplayer</w>
|
||||||
<w>sessionplayers</w>
|
<w>sessionplayers</w>
|
||||||
<w>sessionteam's</w>
|
|
||||||
<w>sessionteam</w>
|
<w>sessionteam</w>
|
||||||
|
<w>sessionteam's</w>
|
||||||
<w>sessionteams</w>
|
<w>sessionteams</w>
|
||||||
<w>sessiontype</w>
|
<w>sessiontype</w>
|
||||||
<w>setactivity</w>
|
<w>setactivity</w>
|
||||||
@ -2156,8 +2156,8 @@
|
|||||||
<w>txtw</w>
|
<w>txtw</w>
|
||||||
<w>typeargs</w>
|
<w>typeargs</w>
|
||||||
<w>typecheck</w>
|
<w>typecheck</w>
|
||||||
<w>typechecker's</w>
|
|
||||||
<w>typechecker</w>
|
<w>typechecker</w>
|
||||||
|
<w>typechecker's</w>
|
||||||
<w>typedval</w>
|
<w>typedval</w>
|
||||||
<w>typeshed</w>
|
<w>typeshed</w>
|
||||||
<w>typestr</w>
|
<w>typestr</w>
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
- Plugin functionality has been consolidated into a PluginSubsystem obj at ba.app.plugins
|
- Plugin functionality has been consolidated into a PluginSubsystem obj at ba.app.plugins
|
||||||
- Ditto with AccountSubsystem and ba.app.accounts
|
- Ditto with AccountSubsystem and ba.app.accounts
|
||||||
- Ditto with MetadataSubsystem and ba.app.meta
|
- Ditto with MetadataSubsystem and ba.app.meta
|
||||||
|
- Ditto with AdsSubsystem and ba.app.ads
|
||||||
|
|
||||||
### 1.5.26 (20217)
|
### 1.5.26 (20217)
|
||||||
- Simplified licensing header on python scripts.
|
- Simplified licensing header on python scripts.
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
"ba_data/python/ba/__pycache__/_activity.cpython-38.opt-1.pyc",
|
"ba_data/python/ba/__pycache__/_activity.cpython-38.opt-1.pyc",
|
||||||
"ba_data/python/ba/__pycache__/_activitytypes.cpython-38.opt-1.pyc",
|
"ba_data/python/ba/__pycache__/_activitytypes.cpython-38.opt-1.pyc",
|
||||||
"ba_data/python/ba/__pycache__/_actor.cpython-38.opt-1.pyc",
|
"ba_data/python/ba/__pycache__/_actor.cpython-38.opt-1.pyc",
|
||||||
|
"ba_data/python/ba/__pycache__/_ads.cpython-38.opt-1.pyc",
|
||||||
"ba_data/python/ba/__pycache__/_analytics.cpython-38.opt-1.pyc",
|
"ba_data/python/ba/__pycache__/_analytics.cpython-38.opt-1.pyc",
|
||||||
"ba_data/python/ba/__pycache__/_app.cpython-38.opt-1.pyc",
|
"ba_data/python/ba/__pycache__/_app.cpython-38.opt-1.pyc",
|
||||||
"ba_data/python/ba/__pycache__/_appconfig.cpython-38.opt-1.pyc",
|
"ba_data/python/ba/__pycache__/_appconfig.cpython-38.opt-1.pyc",
|
||||||
@ -67,6 +68,7 @@
|
|||||||
"ba_data/python/ba/_activity.py",
|
"ba_data/python/ba/_activity.py",
|
||||||
"ba_data/python/ba/_activitytypes.py",
|
"ba_data/python/ba/_activitytypes.py",
|
||||||
"ba_data/python/ba/_actor.py",
|
"ba_data/python/ba/_actor.py",
|
||||||
|
"ba_data/python/ba/_ads.py",
|
||||||
"ba_data/python/ba/_analytics.py",
|
"ba_data/python/ba/_analytics.py",
|
||||||
"ba_data/python/ba/_app.py",
|
"ba_data/python/ba/_app.py",
|
||||||
"ba_data/python/ba/_appconfig.py",
|
"ba_data/python/ba/_appconfig.py",
|
||||||
|
|||||||
@ -137,6 +137,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \
|
|||||||
build/ba_data/python/ba/_activity.py \
|
build/ba_data/python/ba/_activity.py \
|
||||||
build/ba_data/python/ba/_activitytypes.py \
|
build/ba_data/python/ba/_activitytypes.py \
|
||||||
build/ba_data/python/ba/_actor.py \
|
build/ba_data/python/ba/_actor.py \
|
||||||
|
build/ba_data/python/ba/_ads.py \
|
||||||
build/ba_data/python/ba/_analytics.py \
|
build/ba_data/python/ba/_analytics.py \
|
||||||
build/ba_data/python/ba/_app.py \
|
build/ba_data/python/ba/_app.py \
|
||||||
build/ba_data/python/ba/_appconfig.py \
|
build/ba_data/python/ba/_appconfig.py \
|
||||||
@ -375,6 +376,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \
|
|||||||
build/ba_data/python/ba/__pycache__/_activity.cpython-38.opt-1.pyc \
|
build/ba_data/python/ba/__pycache__/_activity.cpython-38.opt-1.pyc \
|
||||||
build/ba_data/python/ba/__pycache__/_activitytypes.cpython-38.opt-1.pyc \
|
build/ba_data/python/ba/__pycache__/_activitytypes.cpython-38.opt-1.pyc \
|
||||||
build/ba_data/python/ba/__pycache__/_actor.cpython-38.opt-1.pyc \
|
build/ba_data/python/ba/__pycache__/_actor.cpython-38.opt-1.pyc \
|
||||||
|
build/ba_data/python/ba/__pycache__/_ads.cpython-38.opt-1.pyc \
|
||||||
build/ba_data/python/ba/__pycache__/_analytics.cpython-38.opt-1.pyc \
|
build/ba_data/python/ba/__pycache__/_analytics.cpython-38.opt-1.pyc \
|
||||||
build/ba_data/python/ba/__pycache__/_app.cpython-38.opt-1.pyc \
|
build/ba_data/python/ba/__pycache__/_app.cpython-38.opt-1.pyc \
|
||||||
build/ba_data/python/ba/__pycache__/_appconfig.cpython-38.opt-1.pyc \
|
build/ba_data/python/ba/__pycache__/_appconfig.cpython-38.opt-1.pyc \
|
||||||
|
|||||||
@ -40,11 +40,10 @@ class EndSessionActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||||||
def on_begin(self) -> None:
|
def on_begin(self) -> None:
|
||||||
# pylint: disable=cyclic-import
|
# pylint: disable=cyclic-import
|
||||||
from bastd.mainmenu import MainMenuSession
|
from bastd.mainmenu import MainMenuSession
|
||||||
from ba._apputils import call_after_ad
|
|
||||||
from ba._general import Call
|
from ba._general import Call
|
||||||
super().on_begin()
|
super().on_begin()
|
||||||
_ba.unlock_all_input()
|
_ba.unlock_all_input()
|
||||||
call_after_ad(Call(_ba.new_host_session, MainMenuSession))
|
_ba.app.ads.call_after_ad(Call(_ba.new_host_session, MainMenuSession))
|
||||||
|
|
||||||
|
|
||||||
class JoinActivity(Activity[EmptyPlayer, EmptyTeam]):
|
class JoinActivity(Activity[EmptyPlayer, EmptyTeam]):
|
||||||
|
|||||||
186
assets/src/ba_data/python/ba/_ads.py
Normal file
186
assets/src/ba_data/python/ba/_ads.py
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
# Released under the MIT License. See LICENSE for details.
|
||||||
|
#
|
||||||
|
"""Functionality related to ads."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import _ba
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing import Optional, Callable, Any
|
||||||
|
|
||||||
|
|
||||||
|
class AdsSubsystem:
|
||||||
|
"""Subsystem for ads functionality in the app.
|
||||||
|
|
||||||
|
Category: App Classes
|
||||||
|
|
||||||
|
Access the single shared instance of this class at 'ba.app.ads'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.last_ad_network = 'unknown'
|
||||||
|
self.last_ad_network_set_time = time.time()
|
||||||
|
self.ad_amt: Optional[float] = None
|
||||||
|
self.last_ad_purpose = 'invalid'
|
||||||
|
self.attempted_first_ad = False
|
||||||
|
self.last_in_game_ad_remove_message_show_time: Optional[float] = None
|
||||||
|
self.last_ad_completion_time: Optional[float] = None
|
||||||
|
self.last_ad_was_short = False
|
||||||
|
|
||||||
|
def do_remove_in_game_ads_message(self) -> None:
|
||||||
|
"""(internal)"""
|
||||||
|
from ba._language import Lstr
|
||||||
|
from ba._enums import TimeType
|
||||||
|
|
||||||
|
# Print this message once every 10 minutes at most.
|
||||||
|
tval = _ba.time(TimeType.REAL)
|
||||||
|
if (self.last_in_game_ad_remove_message_show_time is None or
|
||||||
|
(tval - self.last_in_game_ad_remove_message_show_time > 60 * 10)):
|
||||||
|
self.last_in_game_ad_remove_message_show_time = tval
|
||||||
|
with _ba.Context('ui'):
|
||||||
|
_ba.timer(
|
||||||
|
1.0,
|
||||||
|
lambda: _ba.screenmessage(Lstr(
|
||||||
|
resource='removeInGameAdsText',
|
||||||
|
subs=[('${PRO}',
|
||||||
|
Lstr(resource='store.bombSquadProNameText')),
|
||||||
|
('${APP_NAME}', Lstr(resource='titleText'))]),
|
||||||
|
color=(1, 1, 0)),
|
||||||
|
timetype=TimeType.REAL)
|
||||||
|
|
||||||
|
def show_ad(self,
|
||||||
|
purpose: str,
|
||||||
|
on_completion_call: Callable[[], Any] = None) -> None:
|
||||||
|
"""(internal)"""
|
||||||
|
self.last_ad_purpose = purpose
|
||||||
|
_ba.show_ad(purpose, on_completion_call)
|
||||||
|
|
||||||
|
def show_ad_2(self,
|
||||||
|
purpose: str,
|
||||||
|
on_completion_call: Callable[[bool], Any] = None) -> None:
|
||||||
|
"""(internal)"""
|
||||||
|
self.last_ad_purpose = purpose
|
||||||
|
_ba.show_ad_2(purpose, on_completion_call)
|
||||||
|
|
||||||
|
def call_after_ad(self, call: Callable[[], Any]) -> None:
|
||||||
|
"""Run a call after potentially showing an ad."""
|
||||||
|
# pylint: disable=too-many-statements
|
||||||
|
# pylint: disable=too-many-branches
|
||||||
|
# pylint: disable=too-many-locals
|
||||||
|
from ba._enums import TimeType
|
||||||
|
app = _ba.app
|
||||||
|
show = True
|
||||||
|
|
||||||
|
# No ads without net-connections, etc.
|
||||||
|
if not _ba.can_show_ad():
|
||||||
|
show = False
|
||||||
|
if app.accounts.have_pro():
|
||||||
|
show = False # Pro disables interstitials.
|
||||||
|
try:
|
||||||
|
session = _ba.get_foreground_host_session()
|
||||||
|
assert session is not None
|
||||||
|
is_tournament = session.tournament_id is not None
|
||||||
|
except Exception:
|
||||||
|
is_tournament = False
|
||||||
|
if is_tournament:
|
||||||
|
show = False # Never show ads during tournaments.
|
||||||
|
|
||||||
|
if show:
|
||||||
|
interval: Optional[float]
|
||||||
|
launch_count = app.config.get('launchCount', 0)
|
||||||
|
|
||||||
|
# If we're seeing short ads we may want to space them differently.
|
||||||
|
interval_mult = (_ba.get_account_misc_read_val(
|
||||||
|
'ads.shortIntervalMult', 1.0)
|
||||||
|
if self.last_ad_was_short else 1.0)
|
||||||
|
if self.ad_amt is None:
|
||||||
|
if launch_count <= 1:
|
||||||
|
self.ad_amt = _ba.get_account_misc_read_val(
|
||||||
|
'ads.startVal1', 0.99)
|
||||||
|
else:
|
||||||
|
self.ad_amt = _ba.get_account_misc_read_val(
|
||||||
|
'ads.startVal2', 1.0)
|
||||||
|
interval = None
|
||||||
|
else:
|
||||||
|
# So far we're cleared to show; now calc our
|
||||||
|
# ad-show-threshold and see if we should *actually* show
|
||||||
|
# (we reach our threshold faster the longer we've been
|
||||||
|
# playing).
|
||||||
|
base = 'ads' if _ba.has_video_ads() else 'ads2'
|
||||||
|
min_lc = _ba.get_account_misc_read_val(base + '.minLC', 0.0)
|
||||||
|
max_lc = _ba.get_account_misc_read_val(base + '.maxLC', 5.0)
|
||||||
|
min_lc_scale = (_ba.get_account_misc_read_val(
|
||||||
|
base + '.minLCScale', 0.25))
|
||||||
|
max_lc_scale = (_ba.get_account_misc_read_val(
|
||||||
|
base + '.maxLCScale', 0.34))
|
||||||
|
min_lc_interval = (_ba.get_account_misc_read_val(
|
||||||
|
base + '.minLCInterval', 360))
|
||||||
|
max_lc_interval = (_ba.get_account_misc_read_val(
|
||||||
|
base + '.maxLCInterval', 300))
|
||||||
|
if launch_count < min_lc:
|
||||||
|
lc_amt = 0.0
|
||||||
|
elif launch_count > max_lc:
|
||||||
|
lc_amt = 1.0
|
||||||
|
else:
|
||||||
|
lc_amt = ((float(launch_count) - min_lc) /
|
||||||
|
(max_lc - min_lc))
|
||||||
|
incr = (1.0 - lc_amt) * min_lc_scale + lc_amt * max_lc_scale
|
||||||
|
interval = ((1.0 - lc_amt) * min_lc_interval +
|
||||||
|
lc_amt * max_lc_interval)
|
||||||
|
self.ad_amt += incr
|
||||||
|
assert self.ad_amt is not None
|
||||||
|
if self.ad_amt >= 1.0:
|
||||||
|
self.ad_amt = self.ad_amt % 1.0
|
||||||
|
self.attempted_first_ad = True
|
||||||
|
|
||||||
|
# After we've reached the traditional show-threshold once,
|
||||||
|
# try again whenever its been INTERVAL since our last successful
|
||||||
|
# show.
|
||||||
|
elif (
|
||||||
|
self.attempted_first_ad and
|
||||||
|
(self.last_ad_completion_time is None or
|
||||||
|
(interval is not None
|
||||||
|
and _ba.time(TimeType.REAL) - self.last_ad_completion_time >
|
||||||
|
(interval * interval_mult)))):
|
||||||
|
# Reset our other counter too in this case.
|
||||||
|
self.ad_amt = 0.0
|
||||||
|
else:
|
||||||
|
show = False
|
||||||
|
|
||||||
|
# If we're *still* cleared to show, actually tell the system to show.
|
||||||
|
if show:
|
||||||
|
# As a safety-check, set up an object that will run
|
||||||
|
# the completion callback if we've returned and sat for 10 seconds
|
||||||
|
# (in case some random ad network doesn't properly deliver its
|
||||||
|
# completion callback).
|
||||||
|
class _Payload:
|
||||||
|
|
||||||
|
def __init__(self, pcall: Callable[[], Any]):
|
||||||
|
self._call = pcall
|
||||||
|
self._ran = False
|
||||||
|
|
||||||
|
def run(self, fallback: bool = False) -> None:
|
||||||
|
"""Run fallback call (and issue a warning about it)."""
|
||||||
|
if not self._ran:
|
||||||
|
if fallback:
|
||||||
|
print(
|
||||||
|
('ERROR: relying on fallback ad-callback! '
|
||||||
|
'last network: ' + app.ads.last_ad_network +
|
||||||
|
' (set ' + str(
|
||||||
|
int(time.time() -
|
||||||
|
app.ads.last_ad_network_set_time)) +
|
||||||
|
's ago); purpose=' + app.ads.last_ad_purpose))
|
||||||
|
_ba.pushcall(self._call)
|
||||||
|
self._ran = True
|
||||||
|
|
||||||
|
payload = _Payload(call)
|
||||||
|
with _ba.Context('ui'):
|
||||||
|
_ba.timer(5.0,
|
||||||
|
lambda: payload.run(fallback=True),
|
||||||
|
timetype=TimeType.REAL)
|
||||||
|
self.show_ad('between_game', on_completion_call=payload.run)
|
||||||
|
else:
|
||||||
|
_ba.pushcall(call) # Just run the callback without the ad.
|
||||||
@ -3,7 +3,6 @@
|
|||||||
"""Functionality related to the high level state of the app."""
|
"""Functionality related to the high level state of the app."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
|
||||||
import random
|
import random
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@ -173,6 +172,7 @@ class App:
|
|||||||
from ba._plugin import PluginSubsystem
|
from ba._plugin import PluginSubsystem
|
||||||
from ba._account import AccountSubsystem
|
from ba._account import AccountSubsystem
|
||||||
from ba._meta import MetadataSubsystem
|
from ba._meta import MetadataSubsystem
|
||||||
|
from ba._ads import AdsSubsystem
|
||||||
|
|
||||||
# Config.
|
# Config.
|
||||||
self.config_file_healthy = False
|
self.config_file_healthy = False
|
||||||
@ -199,12 +199,9 @@ class App:
|
|||||||
# Misc.
|
# Misc.
|
||||||
self.tips: List[str] = []
|
self.tips: List[str] = []
|
||||||
self.stress_test_reset_timer: Optional[ba.Timer] = None
|
self.stress_test_reset_timer: Optional[ba.Timer] = None
|
||||||
self.last_ad_completion_time: Optional[float] = None
|
|
||||||
self.last_ad_was_short = False
|
|
||||||
self.did_weak_call_warning = False
|
self.did_weak_call_warning = False
|
||||||
self.ran_on_app_launch = False
|
self.ran_on_app_launch = False
|
||||||
|
|
||||||
self.last_in_game_ad_remove_message_show_time: Optional[float] = None
|
|
||||||
self.log_have_new = False
|
self.log_have_new = False
|
||||||
self.log_upload_timer_started = False
|
self.log_upload_timer_started = False
|
||||||
self._config: Optional[ba.AppConfig] = None
|
self._config: Optional[ba.AppConfig] = None
|
||||||
@ -223,13 +220,6 @@ class App:
|
|||||||
# Server Mode.
|
# Server Mode.
|
||||||
self.server: Optional[ba.ServerController] = None
|
self.server: Optional[ba.ServerController] = None
|
||||||
|
|
||||||
# Ads.
|
|
||||||
self.last_ad_network = 'unknown'
|
|
||||||
self.last_ad_network_set_time = time.time()
|
|
||||||
self.ad_amt: Optional[float] = None
|
|
||||||
self.last_ad_purpose = 'invalid'
|
|
||||||
self.attempted_first_ad = False
|
|
||||||
|
|
||||||
self.meta = MetadataSubsystem()
|
self.meta = MetadataSubsystem()
|
||||||
self.accounts = AccountSubsystem()
|
self.accounts = AccountSubsystem()
|
||||||
self.plugins = PluginSubsystem()
|
self.plugins = PluginSubsystem()
|
||||||
@ -237,6 +227,7 @@ class App:
|
|||||||
self.lang = LanguageSubsystem()
|
self.lang = LanguageSubsystem()
|
||||||
self.ach = AchievementSubsystem()
|
self.ach = AchievementSubsystem()
|
||||||
self.ui = UISubsystem()
|
self.ui = UISubsystem()
|
||||||
|
self.ads = AdsSubsystem()
|
||||||
|
|
||||||
# Lobby.
|
# Lobby.
|
||||||
self.lobby_random_profile_index: int = 1
|
self.lobby_random_profile_index: int = 1
|
||||||
@ -552,27 +543,6 @@ class App:
|
|||||||
_ba.fade_screen(False, endcall=_fade_end)
|
_ba.fade_screen(False, endcall=_fade_end)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def do_remove_in_game_ads_message(self) -> None:
|
|
||||||
"""(internal)"""
|
|
||||||
from ba._language import Lstr
|
|
||||||
from ba._enums import TimeType
|
|
||||||
|
|
||||||
# Print this message once every 10 minutes at most.
|
|
||||||
tval = _ba.time(TimeType.REAL)
|
|
||||||
if (self.last_in_game_ad_remove_message_show_time is None or
|
|
||||||
(tval - self.last_in_game_ad_remove_message_show_time > 60 * 10)):
|
|
||||||
self.last_in_game_ad_remove_message_show_time = tval
|
|
||||||
with _ba.Context('ui'):
|
|
||||||
_ba.timer(
|
|
||||||
1.0,
|
|
||||||
lambda: _ba.screenmessage(Lstr(
|
|
||||||
resource='removeInGameAdsText',
|
|
||||||
subs=[('${PRO}',
|
|
||||||
Lstr(resource='store.bombSquadProNameText')),
|
|
||||||
('${APP_NAME}', Lstr(resource='titleText'))]),
|
|
||||||
color=(1, 1, 0)),
|
|
||||||
timetype=TimeType.REAL)
|
|
||||||
|
|
||||||
def on_app_shutdown(self) -> None:
|
def on_app_shutdown(self) -> None:
|
||||||
"""(internal)"""
|
"""(internal)"""
|
||||||
self.music.on_app_shutdown()
|
self.music.on_app_shutdown()
|
||||||
|
|||||||
@ -225,133 +225,3 @@ def print_corrupt_file_error() -> None:
|
|||||||
_ba.timer(2.0,
|
_ba.timer(2.0,
|
||||||
Call(_ba.playsound, _ba.getsound('error')),
|
Call(_ba.playsound, _ba.getsound('error')),
|
||||||
timetype=TimeType.REAL)
|
timetype=TimeType.REAL)
|
||||||
|
|
||||||
|
|
||||||
def show_ad(purpose: str,
|
|
||||||
on_completion_call: Callable[[], Any] = None) -> None:
|
|
||||||
"""(internal)"""
|
|
||||||
_ba.app.last_ad_purpose = purpose
|
|
||||||
_ba.show_ad(purpose, on_completion_call)
|
|
||||||
|
|
||||||
|
|
||||||
def show_ad_2(purpose: str,
|
|
||||||
on_completion_call: Callable[[bool], Any] = None) -> None:
|
|
||||||
"""(internal)"""
|
|
||||||
_ba.app.last_ad_purpose = purpose
|
|
||||||
_ba.show_ad_2(purpose, on_completion_call)
|
|
||||||
|
|
||||||
|
|
||||||
def call_after_ad(call: Callable[[], Any]) -> None:
|
|
||||||
"""Run a call after potentially showing an ad."""
|
|
||||||
# pylint: disable=too-many-statements
|
|
||||||
# pylint: disable=too-many-branches
|
|
||||||
# pylint: disable=too-many-locals
|
|
||||||
from ba._enums import TimeType
|
|
||||||
import time
|
|
||||||
app = _ba.app
|
|
||||||
show = True
|
|
||||||
|
|
||||||
# No ads without net-connections, etc.
|
|
||||||
if not _ba.can_show_ad():
|
|
||||||
show = False
|
|
||||||
if app.accounts.have_pro():
|
|
||||||
show = False # Pro disables interstitials.
|
|
||||||
try:
|
|
||||||
session = _ba.get_foreground_host_session()
|
|
||||||
assert session is not None
|
|
||||||
is_tournament = session.tournament_id is not None
|
|
||||||
except Exception:
|
|
||||||
is_tournament = False
|
|
||||||
if is_tournament:
|
|
||||||
show = False # Never show ads during tournaments.
|
|
||||||
|
|
||||||
if show:
|
|
||||||
interval: Optional[float]
|
|
||||||
launch_count = app.config.get('launchCount', 0)
|
|
||||||
|
|
||||||
# If we're seeing short ads we may want to space them differently.
|
|
||||||
interval_mult = (_ba.get_account_misc_read_val(
|
|
||||||
'ads.shortIntervalMult', 1.0) if app.last_ad_was_short else 1.0)
|
|
||||||
if app.ad_amt is None:
|
|
||||||
if launch_count <= 1:
|
|
||||||
app.ad_amt = _ba.get_account_misc_read_val(
|
|
||||||
'ads.startVal1', 0.99)
|
|
||||||
else:
|
|
||||||
app.ad_amt = _ba.get_account_misc_read_val(
|
|
||||||
'ads.startVal2', 1.0)
|
|
||||||
interval = None
|
|
||||||
else:
|
|
||||||
# So far we're cleared to show; now calc our ad-show-threshold and
|
|
||||||
# see if we should *actually* show (we reach our threshold faster
|
|
||||||
# the longer we've been playing).
|
|
||||||
base = 'ads' if _ba.has_video_ads() else 'ads2'
|
|
||||||
min_lc = _ba.get_account_misc_read_val(base + '.minLC', 0.0)
|
|
||||||
max_lc = _ba.get_account_misc_read_val(base + '.maxLC', 5.0)
|
|
||||||
min_lc_scale = (_ba.get_account_misc_read_val(
|
|
||||||
base + '.minLCScale', 0.25))
|
|
||||||
max_lc_scale = (_ba.get_account_misc_read_val(
|
|
||||||
base + '.maxLCScale', 0.34))
|
|
||||||
min_lc_interval = (_ba.get_account_misc_read_val(
|
|
||||||
base + '.minLCInterval', 360))
|
|
||||||
max_lc_interval = (_ba.get_account_misc_read_val(
|
|
||||||
base + '.maxLCInterval', 300))
|
|
||||||
if launch_count < min_lc:
|
|
||||||
lc_amt = 0.0
|
|
||||||
elif launch_count > max_lc:
|
|
||||||
lc_amt = 1.0
|
|
||||||
else:
|
|
||||||
lc_amt = ((float(launch_count) - min_lc) / (max_lc - min_lc))
|
|
||||||
incr = (1.0 - lc_amt) * min_lc_scale + lc_amt * max_lc_scale
|
|
||||||
interval = ((1.0 - lc_amt) * min_lc_interval +
|
|
||||||
lc_amt * max_lc_interval)
|
|
||||||
app.ad_amt += incr
|
|
||||||
assert app.ad_amt is not None
|
|
||||||
if app.ad_amt >= 1.0:
|
|
||||||
app.ad_amt = app.ad_amt % 1.0
|
|
||||||
app.attempted_first_ad = True
|
|
||||||
|
|
||||||
# After we've reached the traditional show-threshold once,
|
|
||||||
# try again whenever its been INTERVAL since our last successful show.
|
|
||||||
elif (app.attempted_first_ad
|
|
||||||
and (app.last_ad_completion_time is None or
|
|
||||||
(interval is not None
|
|
||||||
and _ba.time(TimeType.REAL) - app.last_ad_completion_time >
|
|
||||||
(interval * interval_mult)))):
|
|
||||||
# Reset our other counter too in this case.
|
|
||||||
app.ad_amt = 0.0
|
|
||||||
else:
|
|
||||||
show = False
|
|
||||||
|
|
||||||
# If we're *still* cleared to show, actually tell the system to show.
|
|
||||||
if show:
|
|
||||||
# As a safety-check, set up an object that will run
|
|
||||||
# the completion callback if we've returned and sat for 10 seconds
|
|
||||||
# (in case some random ad network doesn't properly deliver its
|
|
||||||
# completion callback).
|
|
||||||
class _Payload:
|
|
||||||
|
|
||||||
def __init__(self, pcall: Callable[[], Any]):
|
|
||||||
self._call = pcall
|
|
||||||
self._ran = False
|
|
||||||
|
|
||||||
def run(self, fallback: bool = False) -> None:
|
|
||||||
"""Run the fallback call (and issues a warning about it)."""
|
|
||||||
if not self._ran:
|
|
||||||
if fallback:
|
|
||||||
print((
|
|
||||||
'ERROR: relying on fallback ad-callback! '
|
|
||||||
'last network: ' + app.last_ad_network + ' (set ' +
|
|
||||||
str(int(time.time() -
|
|
||||||
app.last_ad_network_set_time)) +
|
|
||||||
's ago); purpose=' + app.last_ad_purpose))
|
|
||||||
_ba.pushcall(self._call)
|
|
||||||
self._ran = True
|
|
||||||
|
|
||||||
payload = _Payload(call)
|
|
||||||
with _ba.Context('ui'):
|
|
||||||
_ba.timer(5.0,
|
|
||||||
lambda: payload.run(fallback=True),
|
|
||||||
timetype=TimeType.REAL)
|
|
||||||
show_ad('between_game', on_completion_call=payload.run)
|
|
||||||
else:
|
|
||||||
_ba.pushcall(call) # Just run the callback without the ad.
|
|
||||||
|
|||||||
@ -178,8 +178,8 @@ def submit_analytics_counts(sval: str) -> None:
|
|||||||
|
|
||||||
def set_last_ad_network(sval: str) -> None:
|
def set_last_ad_network(sval: str) -> None:
|
||||||
import time
|
import time
|
||||||
_ba.app.last_ad_network = sval
|
_ba.app.ads.last_ad_network = sval
|
||||||
_ba.app.last_ad_network_set_time = time.time()
|
_ba.app.ads.last_ad_network_set_time = time.time()
|
||||||
|
|
||||||
|
|
||||||
def no_game_circle_message() -> None:
|
def no_game_circle_message() -> None:
|
||||||
@ -263,7 +263,7 @@ def quit_window() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def remove_in_game_ads_message() -> None:
|
def remove_in_game_ads_message() -> None:
|
||||||
_ba.app.do_remove_in_game_ads_message()
|
_ba.app.ads.do_remove_in_game_ads_message()
|
||||||
|
|
||||||
|
|
||||||
def telnet_access_request() -> None:
|
def telnet_access_request() -> None:
|
||||||
|
|||||||
@ -612,7 +612,7 @@ class Session:
|
|||||||
def transitioning_out_activity_was_freed(
|
def transitioning_out_activity_was_freed(
|
||||||
self, can_show_ad_on_death: bool) -> None:
|
self, can_show_ad_on_death: bool) -> None:
|
||||||
"""(internal)"""
|
"""(internal)"""
|
||||||
from ba._apputils import garbage_collect, call_after_ad
|
from ba._apputils import garbage_collect
|
||||||
|
|
||||||
# Since things should be generally still right now, it's a good time
|
# Since things should be generally still right now, it's a good time
|
||||||
# to run garbage collection to clear out any circular dependency
|
# to run garbage collection to clear out any circular dependency
|
||||||
@ -622,7 +622,7 @@ class Session:
|
|||||||
|
|
||||||
with _ba.Context(self):
|
with _ba.Context(self):
|
||||||
if can_show_ad_on_death:
|
if can_show_ad_on_death:
|
||||||
call_after_ad(self.begin_next_activity)
|
_ba.app.ads.call_after_ad(self.begin_next_activity)
|
||||||
else:
|
else:
|
||||||
_ba.pushcall(self.begin_next_activity)
|
_ba.pushcall(self.begin_next_activity)
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ from ba._input import (get_device_value, get_input_map_hash,
|
|||||||
from ba._general import getclass, json_prep, get_type_name
|
from ba._general import getclass, json_prep, get_type_name
|
||||||
from ba._activitytypes import JoinActivity, ScoreScreenActivity
|
from ba._activitytypes import JoinActivity, ScoreScreenActivity
|
||||||
from ba._apputils import (is_browser_likely_available, get_remote_app_name,
|
from ba._apputils import (is_browser_likely_available, get_remote_app_name,
|
||||||
should_submit_debug_info, show_ad, show_ad_2)
|
should_submit_debug_info)
|
||||||
from ba._benchmark import (run_gpu_benchmark, run_cpu_benchmark,
|
from ba._benchmark import (run_gpu_benchmark, run_cpu_benchmark,
|
||||||
run_media_reload_benchmark, run_stress_test)
|
run_media_reload_benchmark, run_stress_test)
|
||||||
from ba._campaign import getcampaign
|
from ba._campaign import getcampaign
|
||||||
|
|||||||
@ -551,7 +551,6 @@ class GetCurrencyWindow(ba.Window):
|
|||||||
|
|
||||||
# actually start the purchase locally..
|
# actually start the purchase locally..
|
||||||
def _do_purchase(self, item: str) -> None:
|
def _do_purchase(self, item: str) -> None:
|
||||||
from ba.internal import show_ad
|
|
||||||
if item == 'ad':
|
if item == 'ad':
|
||||||
import datetime
|
import datetime
|
||||||
# if ads are disabled until some time, error..
|
# if ads are disabled until some time, error..
|
||||||
@ -568,7 +567,7 @@ class GetCurrencyWindow(ba.Window):
|
|||||||
resource='getTicketsWindow.unavailableTemporarilyText'),
|
resource='getTicketsWindow.unavailableTemporarilyText'),
|
||||||
color=(1, 0, 0))
|
color=(1, 0, 0))
|
||||||
elif self._enable_ad_button:
|
elif self._enable_ad_button:
|
||||||
show_ad('tickets')
|
_ba.app.ads.show_ad('tickets')
|
||||||
else:
|
else:
|
||||||
_ba.purchase(item)
|
_ba.purchase(item)
|
||||||
|
|
||||||
|
|||||||
@ -434,8 +434,9 @@ def show_offer() -> bool:
|
|||||||
# Space things out a bit so we don't hit the poor user with an ad and
|
# Space things out a bit so we don't hit the poor user with an ad and
|
||||||
# then an in-game offer.
|
# then an in-game offer.
|
||||||
has_been_long_enough_since_ad = True
|
has_been_long_enough_since_ad = True
|
||||||
if (app.last_ad_completion_time is not None and
|
if (app.ads.last_ad_completion_time is not None and
|
||||||
(ba.time(ba.TimeType.REAL) - app.last_ad_completion_time < 30.0)):
|
(ba.time(ba.TimeType.REAL) - app.ads.last_ad_completion_time <
|
||||||
|
30.0)):
|
||||||
has_been_long_enough_since_ad = False
|
has_been_long_enough_since_ad = False
|
||||||
|
|
||||||
if app.special_offer is not None and has_been_long_enough_since_ad:
|
if app.special_offer is not None and has_been_long_enough_since_ad:
|
||||||
|
|||||||
@ -525,7 +525,6 @@ class TournamentEntryWindow(popup.PopupWindow):
|
|||||||
self._launch()
|
self._launch()
|
||||||
|
|
||||||
def _on_pay_with_ad_press(self) -> None:
|
def _on_pay_with_ad_press(self) -> None:
|
||||||
from ba.internal import show_ad_2
|
|
||||||
|
|
||||||
# If we're already entering, ignore.
|
# If we're already entering, ignore.
|
||||||
if self._entering:
|
if self._entering:
|
||||||
@ -547,8 +546,9 @@ class TournamentEntryWindow(popup.PopupWindow):
|
|||||||
cur_time = ba.time(ba.TimeType.REAL)
|
cur_time = ba.time(ba.TimeType.REAL)
|
||||||
if cur_time - self._last_ad_press_time > 5.0:
|
if cur_time - self._last_ad_press_time > 5.0:
|
||||||
self._last_ad_press_time = cur_time
|
self._last_ad_press_time = cur_time
|
||||||
show_ad_2('tournament_entry',
|
_ba.app.ads.show_ad_2('tournament_entry',
|
||||||
on_completion_call=ba.WeakCall(self._on_ad_complete))
|
on_completion_call=ba.WeakCall(
|
||||||
|
self._on_ad_complete))
|
||||||
|
|
||||||
def _on_ad_complete(self, actually_showed: bool) -> None:
|
def _on_ad_complete(self, actually_showed: bool) -> None:
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
|
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
|
||||||
<h4><em>last updated on 2020-10-16 for Ballistica version 1.5.27 build 20219</em></h4>
|
<h4><em>last updated on 2020-10-17 for Ballistica version 1.5.27 build 20223</em></h4>
|
||||||
<p>This page documents the Python classes and functions in the 'ba' module,
|
<p>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 <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
|
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 <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
|
||||||
<hr>
|
<hr>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user