This commit is contained in:
Eric Froemling 2020-10-01 16:23:23 -05:00
parent 87b6716d59
commit 0809dbcbfb
13 changed files with 1456 additions and 10 deletions

View File

@ -3934,14 +3934,14 @@
"assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f",
"build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ac/96/c3b9934061393fe09cc90ff24b8d",
"build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/38/2b/5641b3b40846f74f232771ac0457",
"build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e3/43/5df3f99b46aa9b6aff2db87fabb4",
"build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/b1/06/b993362a65a5760ddfb10f2c59b9",
"build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/4f/99/2234c89a65d7e5d8463556d9df5e",
"build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e4/96/f289ec31cdada6f41de5dce382bd",
"build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/62/8d/6f2752d858b097c318920ff09cf5",
"build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/77/1d/7986d62c92d9f34ca85396168923",
"build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/a3/da/d5f88e92926543f61bd5dd80321e",
"build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/60/8e/d7efd5e7d7a94439453909527d4e",
"build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/d3/b3/8124ea626db8dbd7b1744c335c7c",
"build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/27/f7/87a5d3b8648352ac40398f86dc8e"
"build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/d0/21/cb738075207c3dab9f2389455a69",
"build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/cd/44/d59a1f7d6ee05b58e80ebc3469bf",
"build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b4/56/6d1f390d348ddf65937af3374d76",
"build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/31/b9/176ba76e48c23d064b6583963a0e",
"build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/37/18/56c6d3b8788362ceb97005b6b3b6",
"build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/a6/bd/da7954fe559403d021b15df72967",
"build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/97/10/138625f676793653620a84cd712b",
"build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/ef/a8/4b532f37f74929b834ffd6c2ac5d",
"build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/3f/6f/c49674367960a82b19a45641daa6",
"build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/df/a3/90e19f2ef6b2edd17d0f936245f2"
}

483
src/ballistica/app/app.cc Normal file
View File

@ -0,0 +1,483 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/app/app.h"
#include "ballistica/core/thread.h"
#include "ballistica/game/game.h"
#include "ballistica/graphics/graphics_server.h"
#include "ballistica/graphics/renderer.h"
#include "ballistica/input/device/touch_input.h"
#include "ballistica/input/input.h"
#include "ballistica/networking/network_reader.h"
#include "ballistica/networking/networking.h"
#include "ballistica/networking/telnet_server.h"
#include "ballistica/python/python.h"
namespace ballistica {
App::App(Thread* thread) : Module("app", thread) {
assert(g_app == nullptr);
g_app = this;
// So anyone who needs to use the 'main' thread id can get at it...
Thread::UpdateMainThreadID();
// We modify some app behavior when run under the server manager.
auto* envval = getenv("BA_SERVER_WRAPPER_MANAGED");
server_wrapper_managed_ = (envval && strcmp(envval, "1") == 0);
}
void App::PostInit() {
// If we've got a nice themed hardware cursor, show it.
// Otherwise hide the hardware cursor; we'll draw it in software.
// (need to run this in postinit because SDL/etc may not be inited yet
// as of App::App().
g_platform->SetHardwareCursorVisible(g_buildconfig.hardware_cursor());
}
App::~App() = default;
auto App::UsesEventLoop() const -> bool {
// We have 2 redundant values for essentially the same thing;
// should get rid of IsEventPushMode() once we've created
// App subclasses for our various platforms.
return !g_platform->IsEventPushMode();
}
void App::PushInterruptSignalSetupCall() {
g_platform->SetupInterruptHandling();
}
void App::RunRenderUpkeepCycle() {
// This should only be used in cases where the OS is handling the event loop.
assert(!UsesEventLoop());
if (UsesEventLoop()) {
return;
}
// Pump thread messages (we're being driven by frame-draw callbacks
// so this is the only place that it gets done at).
thread()->RunEventLoop(true); // Single pass only.
// Now do the general app event cycle for whoever needs to process things.
RunEvents();
}
void App::RebuildLostGLContext() {
assert(InMainThread());
assert(g_graphics_server);
if (g_graphics_server) {
g_graphics_server->RebuildLostContext();
}
}
void App::DrawFrame(bool during_resize) {
assert(InMainThread());
// Its possible to receive frames before we're ready to draw.
if (!g_graphics_server || !g_graphics_server->renderer()) {
return;
}
millisecs_t starttime = GetRealTime();
// A resize-draw event means that we're drawing due to a window resize.
// In this case we ignore regular draw events for a short while
// afterwards which makes resizing smoother.
// FIXME: should figure out the *correct* way to handle this;
// I believe the underlying cause here is some sort of context contention
// across threads.
if (during_resize) {
last_resize_draw_event_time_ = starttime;
} else {
if (starttime - last_resize_draw_event_time_ < (1000 / 30)) {
return;
}
}
g_graphics_server->TryRender();
RunRenderUpkeepCycle();
}
void App::SetScreenResolution(float width, float height) {
assert(InMainThread());
if (!HeadlessMode()) {
g_graphics_server->VideoResize(width, height);
}
}
void App::PushShutdownCompleteCall() {
PushCall([this] { ShutdownComplete(); });
}
void App::ShutdownComplete() {
assert(g_platform);
// Need to call our cleanup stuff that would otherwise get called in main.
g_platform->FinalCleanup();
done_ = true;
// Kill our own event loop (or tell the OS to kill its).
if (UsesEventLoop()) {
thread()->Quit();
} else {
g_platform->QuitApp();
}
}
void App::RunEvents() {
if (!HeadlessMode()) {
// there's probably a better place for this...
UpdateStressTesting();
}
// Give platforms a chance to pump/handle their own events.
// FIXME: now that we have app class overrides, platform should really
// not be doing event handling. (need to fix rift build).
g_platform->RunEvents();
}
void App::UpdatePauseResume() {
if (actually_paused_) {
// Unpause if no one wants pause.
if (!sys_paused_app_ && !user_paused_app_) {
OnResume();
actually_paused_ = false;
}
} else {
// Pause if anyone wants.
if (sys_paused_app_ || user_paused_app_) {
OnPause();
actually_paused_ = true;
}
}
}
void App::OnPause() {
assert(InMainThread());
// Avoid reading gyro values for a short time to avoid hitches when restored.
g_graphics->SetGyroEnabled(false);
// IMPORTANT: Any on-pause related stuff that threads need to do must
// must be done from their HandleThreadPause(). If we push runnables to them
// they may or may not be called before the thread is actually paused.
Thread::SetThreadsPaused(true);
assert(g_networking);
g_networking->Pause();
assert(g_network_reader);
if (g_network_reader) {
g_network_reader->Pause();
}
if (g_app_globals->telnet_server) {
g_app_globals->telnet_server->Pause();
}
g_platform->OnAppPause();
}
void App::OnResume() {
assert(InMainThread());
last_app_resume_time_ = GetRealTime();
Thread::SetThreadsPaused(false);
g_platform->OnAppResume();
g_networking->Resume();
g_network_reader->Resume();
if (g_app_globals->telnet_server) {
g_app_globals->telnet_server->Resume();
}
// Also let the Python layer do what it needs to
// (starting/stopping music, etc).
g_python->PushObjCall(Python::ObjID::kHandleAppResumeCall);
g_game->PushOnAppResumeCall();
g_graphics->SetGyroEnabled(true);
// When resuming from a paused state, we may want to
// pause whatever game was running when we last were active.
// TODO(efro): we should make this smarter so it doesn't happen if
// we're in a network game or something that we can't pause;
// bringing up the menu doesn't really accomplish anything there.
if (g_app_globals->should_pause) {
g_app_globals->should_pause = false;
// If we've been completely backgrounded,
// send a menu-press command to the game; this will
// bring up a pause menu if we're in the game/etc.
g_game->PushMainMenuPressCall(nullptr);
}
}
auto App::GetProductPrice(const std::string& product) -> std::string {
std::lock_guard<std::mutex> lock(product_prices_mutex_);
auto i = product_prices_.find(product);
if (i == product_prices_.end()) {
return "";
} else {
return i->second;
}
}
void App::SetProductPrice(const std::string& product,
const std::string& price) {
std::lock_guard<std::mutex> lock(product_prices_mutex_);
product_prices_[product] = price;
}
void App::PauseApp() {
assert(InMainThread());
Platform::DebugLog("PauseApp@"
+ std::to_string(Platform::GetCurrentMilliseconds()));
assert(!sys_paused_app_);
sys_paused_app_ = true;
UpdatePauseResume();
}
void App::ResumeApp() {
assert(InMainThread());
Platform::DebugLog("ResumeApp@"
+ std::to_string(Platform::GetCurrentMilliseconds()));
assert(sys_paused_app_);
sys_paused_app_ = false;
UpdatePauseResume();
}
void App::DidFinishRenderingFrame(FrameDef* frame) {}
void App::PrimeEventPump() {
assert(!UsesEventLoop());
// Pump events manually until a screen gets created.
// At that point we use frame-draws to drive our event loop.
while (!g_graphics_server->initial_screen_created()) {
g_main_thread->RunEventLoop(true);
Platform::SleepMS(1);
}
}
#pragma mark Push-Calls
void App::PushShowOnlineScoreUICall(const std::string& show,
const std::string& game,
const std::string& game_version) {
PushCall([this, show, game, game_version] {
assert(InMainThread());
g_platform->ShowOnlineScoreUI(show, game, game_version);
});
}
void App::PushNetworkSetupCall(int port, int telnet_port, bool enable_telnet,
const std::string& telnet_password) {
PushCall([this, port, telnet_port, enable_telnet, telnet_password] {
assert(InMainThread());
// Kick these off if they don't exist.
// (do we want to support changing ports on existing ones?)
if (g_network_reader == nullptr) {
new NetworkReader(port);
}
if (g_app_globals->telnet_server == nullptr && enable_telnet) {
new TelnetServer(telnet_port);
assert(g_app_globals->telnet_server);
if (telnet_password.empty()) {
g_app_globals->telnet_server->SetPassword(nullptr);
} else {
g_app_globals->telnet_server->SetPassword(telnet_password.c_str());
}
}
});
}
void App::PushPurchaseAckCall(const std::string& purchase,
const std::string& order_id) {
PushCall([this, purchase, order_id] {
g_platform->PurchaseAck(purchase, order_id);
});
}
void App::PushGetScoresToBeatCall(const std::string& level,
const std::string& config,
void* py_callback) {
PushCall([this, level, config, py_callback] {
assert(InMainThread());
g_platform->GetScoresToBeat(level, config, py_callback);
});
}
void App::PushPurchaseCall(const std::string& item) {
PushCall([this, item] {
assert(InMainThread());
g_platform->Purchase(item);
});
}
void App::PushRestorePurchasesCall() {
PushCall([this] {
assert(InMainThread());
g_platform->RestorePurchases();
});
}
void App::PushOpenURLCall(const std::string& url) {
PushCall([this, url] { g_platform->OpenURL(url); });
}
void App::PushGetFriendScoresCall(const std::string& game,
const std::string& game_version, void* data) {
PushCall([this, game, game_version, data] {
g_platform->GetFriendScores(game, game_version, data);
});
}
void App::PushSubmitScoreCall(const std::string& game,
const std::string& game_version, int64_t score) {
PushCall([this, game, game_version, score] {
g_platform->SubmitScore(game, game_version, score);
});
}
void App::PushAchievementReportCall(const std::string& achievement) {
PushCall([this, achievement] { g_platform->ReportAchievement(achievement); });
}
void App::PushStringEditCall(const std::string& name, const std::string& value,
int max_chars) {
PushCall([this, name, value, max_chars] {
static millisecs_t last_edit_time = 0;
millisecs_t t = GetRealTime();
// Ignore if too close together.
// (in case second request comes in before first takes effect).
if (t - last_edit_time < 1000) {
return;
}
last_edit_time = t;
assert(InMainThread());
g_platform->EditText(name, value, max_chars);
});
}
void App::PushSetStressTestingCall(bool enable, int player_count) {
PushCall([this, enable, player_count] {
bool was_stress_testing = stress_testing_;
stress_testing_ = enable;
stress_test_player_count_ = player_count;
// If we're turning on, reset our intervals and things.
if (!was_stress_testing && stress_testing_) {
// So our first sample is 1 interval from now...
last_stress_test_update_time_ = GetRealTime();
// Reset our frames-rendered tally.
if (g_graphics && g_graphics_server->renderer()) {
last_total_frames_rendered_ =
g_graphics_server->renderer()->total_frames_rendered();
} else {
// Assume zero if there's no graphics yet.
last_total_frames_rendered_ = 0;
}
}
});
}
void App::PushResetAchievementsCall() {
PushCall([this] { g_platform->ResetAchievements(); });
}
void App::OnBootstrapComplete() {
assert(InMainThread());
assert(g_input);
if (!HeadlessMode()) {
// On desktop systems we just assume keyboard input exists and add it
// immediately.
if (g_platform->IsRunningOnDesktop()) {
g_input->PushCreateKeyboardInputDevices();
}
// On non-tv, non-desktop, non-vr systems, create a touchscreen input.
if (!g_platform->IsRunningOnTV() && !IsVRMode()
&& !g_platform->IsRunningOnDesktop()) {
g_input->CreateTouchInput();
}
}
}
void App::PushCursorUpdate(bool vis) {
PushCall([this, vis] {
assert(InMainThread());
g_platform->SetHardwareCursorVisible(vis);
});
}
void App::UpdateStressTesting() {
// Handle a little misc stuff here.
// If we're currently running stress-tests, update that stuff.
if (stress_testing_ && g_input) {
// Update our fake inputs to make our dudes run around.
g_input->ProcessStressTesting(stress_test_player_count_);
// Every 10 seconds update our stress-test stats.
millisecs_t t = GetRealTime();
if (t - last_stress_test_update_time_ >= 10000) {
if (stress_test_stats_file_ == nullptr) {
assert(g_platform);
std::string f_name =
g_platform->GetUserPythonDirectory() + "/stress_test_stats.csv";
stress_test_stats_file_ = g_platform->FOpen(f_name.c_str(), "wb");
if (stress_test_stats_file_ != nullptr) {
fprintf(stress_test_stats_file_,
"time,averageFps,nodes,models,collideModels,textures,sounds,"
"pssMem,sharedDirtyMem,privateDirtyMem\n");
fflush(stress_test_stats_file_);
if (g_buildconfig.ostype_android()) {
// On android, let the OS know we've added or removed a file
// (limit to android or we'll get an unimplemented warning)
g_platform->AndroidRefreshFile(f_name);
}
}
}
if (stress_test_stats_file_ != nullptr) {
// See how many frames we've rendered this past interval.
int total_frames_rendered;
if (g_graphics && g_graphics_server->renderer()) {
total_frames_rendered =
g_graphics_server->renderer()->total_frames_rendered();
} else {
total_frames_rendered = last_total_frames_rendered_;
}
float avg =
static_cast<float>(total_frames_rendered
- last_total_frames_rendered_)
/ (static_cast<float>(t - last_stress_test_update_time_) / 1000.0f);
last_total_frames_rendered_ = total_frames_rendered;
uint32_t model_count = 0;
uint32_t collide_model_count = 0;
uint32_t texture_count = 0;
uint32_t sound_count = 0;
uint32_t node_count = 0;
if (g_media) {
model_count = g_media->total_model_count();
collide_model_count = g_media->total_collide_model_count();
texture_count = g_media->total_texture_count();
sound_count = g_media->total_sound_count();
}
assert(g_game);
std::string mem_usage = g_platform->GetMemUsageInfo();
fprintf(stress_test_stats_file_, "%d,%.1f,%d,%d,%d,%d,%d,%s\n",
static_cast<int>(GetRealTime()), avg, node_count, model_count,
collide_model_count, texture_count, sound_count,
mem_usage.c_str());
fflush(stress_test_stats_file_);
}
last_stress_test_update_time_ = t;
}
}
}
} // namespace ballistica

148
src/ballistica/app/app.h Normal file
View File

@ -0,0 +1,148 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_APP_APP_H_
#define BALLISTICA_APP_APP_H_
#include <map>
#include <mutex>
#include <string>
#include "ballistica/core/module.h"
namespace ballistica {
/// Our high level app interface module.
/// It runs in the main thread and is what platform wrappers
/// should primarily interact with.
class App : public Module {
public:
explicit App(Thread* thread);
~App() override;
/// This gets run after the constructor completes.
/// Any setup that may trigger a virtual method/etc. should go here.
void PostInit();
/// Return whether this class runs its own event loop.
/// If true, BallisticaMain() will continuously ask the app for events
/// until the app is quit, at which point BallisticaMain() returns.
/// If false, BallisticaMain returns immediately and it is assumed
/// that the OS handles the app lifecycle and pushes events to the app
/// via callbacks/etc.
auto UsesEventLoop() const -> bool;
/// Called for non-event-loop apps to give them an opportunity to
/// ensure they are self-sustaining. For instance, an app relying on
/// frame-draws for its main thread event processing may need to
/// manually pump events until frame rendering begins.
virtual void PrimeEventPump();
/// Handle any pending OS events.
/// On normal graphical builds this is triggered by RunRenderUpkeepCycle();
/// timer intervals for headless builds, etc.
/// Should process any pending OS events, etc.
virtual void RunEvents();
// These should be called by the window, view-controller, sdl,
// or whatever is driving the app. They must be called from the main thread.
/// Should be called on mobile when the app is backgrounded.
/// Pauses threads, closes network sockets, etc.
void PauseApp();
auto paused() const -> bool { return actually_paused_; }
/// Should be called on mobile when the app is foregrounded.
/// Spins threads back up, re-opens network sockets, etc.
void ResumeApp();
/// The last time the app was resumed (uses GetRealTime() value).
auto last_app_resume_time() const -> millisecs_t {
return last_app_resume_time_;
}
/// Should be called when the window/screen resolution changes.
void SetScreenResolution(float width, float height);
/// Should be called if the platform detects the GL context was lost.
void RebuildLostGLContext();
/// Attempt to draw a frame.
void DrawFrame(bool during_resize = false);
/// Used on platforms where our main thread event processing is driven by
/// frame-draw commands given to us. This should be called after drawing
/// a frame in order to bring game state up to date and process OS events.
void RunRenderUpkeepCycle();
/// Called by the graphics-server when drawing completes for a frame.
virtual void DidFinishRenderingFrame(FrameDef* frame);
/// Return the price of an IAP product as a human-readable string,
/// or an empty string if not found.
/// FIXME: move this to platform.
auto GetProductPrice(const std::string& product) -> std::string;
void SetProductPrice(const std::string& product, const std::string& price);
auto done() const -> bool { return done_; }
/// Whether we're running under ballisticacore_server.py
/// (affects some app behavior).
auto server_wrapper_managed() const -> bool {
return server_wrapper_managed_;
}
virtual void OnBootstrapComplete();
// Deferred calls that can be made from other threads.
void PushCursorUpdate(bool vis);
void PushShowOnlineScoreUICall(const std::string& show,
const std::string& game,
const std::string& game_version);
void PushGetFriendScoresCall(const std::string& game,
const std::string& game_version, void* data);
void PushSubmitScoreCall(const std::string& game,
const std::string& game_version, int64_t score);
void PushAchievementReportCall(const std::string& achievement);
void PushGetScoresToBeatCall(const std::string& level,
const std::string& config, void* py_callback);
void PushOpenURLCall(const std::string& url);
void PushStringEditCall(const std::string& name, const std::string& value,
int max_chars);
void PushSetStressTestingCall(bool enable, int player_count);
void PushPurchaseCall(const std::string& item);
void PushRestorePurchasesCall();
void PushResetAchievementsCall();
void PushPurchaseAckCall(const std::string& purchase,
const std::string& order_id);
void PushNetworkSetupCall(int port, int telnet_port, bool enable_telnet,
const std::string& telnet_password);
void PushShutdownCompleteCall();
void PushInterruptSignalSetupCall();
private:
void UpdateStressTesting();
void UpdatePauseResume();
void OnPause();
void OnResume();
void ShutdownComplete();
bool done_{};
bool server_wrapper_managed_{};
bool sys_paused_app_{};
bool user_paused_app_{};
bool actually_paused_{};
millisecs_t last_resize_draw_event_time_{};
millisecs_t last_app_resume_time_{};
std::map<std::string, std::string> product_prices_;
std::mutex product_prices_mutex_;
FILE* stress_test_stats_file_{};
millisecs_t last_stress_test_update_time_{};
int last_total_frames_rendered_{};
bool stress_testing_{};
int stress_test_player_count_{8};
};
} // namespace ballistica
#endif // BALLISTICA_APP_APP_H_

View File

@ -0,0 +1,245 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/app/app_config.h"
#include <utility>
#include "ballistica/ballistica.h"
#include "ballistica/platform/platform.h"
#include "ballistica/python/python.h"
namespace ballistica {
void AppConfig::Init() { new AppConfig(); }
auto AppConfig::Entry::FloatValue() const -> float {
throw Exception("not a float entry");
}
auto AppConfig::Entry::StringValue() const -> std::string {
throw Exception("not a string entry");
}
auto AppConfig::Entry::IntValue() const -> int {
throw Exception("not an int entry");
}
auto AppConfig::Entry::BoolValue() const -> bool {
throw Exception("not a bool entry");
}
auto AppConfig::Entry::DefaultFloatValue() const -> float {
throw Exception("not a float entry");
}
auto AppConfig::Entry::DefaultStringValue() const -> std::string {
throw Exception("not a string entry");
}
auto AppConfig::Entry::DefaultIntValue() const -> int {
throw Exception("not an int entry");
}
auto AppConfig::Entry::DefaultBoolValue() const -> bool {
throw Exception("not a bool entry");
}
class AppConfig::StringEntry : public AppConfig::Entry {
public:
StringEntry() = default;
StringEntry(const char* name, std::string default_value)
: Entry(name), default_value_(std::move(default_value)) {}
auto GetType() const -> Type override { return Type::kString; }
auto Resolve() const -> std::string {
return g_python->GetRawConfigValue(name().c_str(), default_value_.c_str());
}
auto StringValue() const -> std::string override { return Resolve(); }
auto DefaultStringValue() const -> std::string override {
return default_value_;
}
private:
std::string default_value_;
};
class AppConfig::FloatEntry : public AppConfig::Entry {
public:
FloatEntry() = default;
FloatEntry(const char* name, float default_value)
: Entry(name), default_value_(default_value) {}
auto GetType() const -> Type override { return Type::kFloat; }
auto Resolve() const -> float {
return g_python->GetRawConfigValue(name().c_str(), default_value_);
}
auto FloatValue() const -> float override { return Resolve(); }
auto DefaultFloatValue() const -> float override { return default_value_; }
private:
float default_value_{};
};
class AppConfig::IntEntry : public AppConfig::Entry {
public:
IntEntry() = default;
IntEntry(const char* name, int default_value)
: Entry(name), default_value_(default_value) {}
auto GetType() const -> Type override { return Type::kInt; }
auto Resolve() const -> int {
return g_python->GetRawConfigValue(name().c_str(), default_value_);
}
auto IntValue() const -> int override { return Resolve(); }
auto DefaultIntValue() const -> int override { return default_value_; }
private:
int default_value_{};
};
class AppConfig::BoolEntry : public AppConfig::Entry {
public:
BoolEntry() = default;
BoolEntry(const char* name, bool default_value)
: Entry(name), default_value_(default_value) {}
auto GetType() const -> Type override { return Type::kBool; }
auto Resolve() const -> bool {
return g_python->GetRawConfigValue(name().c_str(), default_value_);
}
auto BoolValue() const -> bool override { return Resolve(); }
auto DefaultBoolValue() const -> bool override { return default_value_; }
private:
bool default_value_{};
};
AppConfig::AppConfig() {
// (We're a singleton).
assert(g_app_config == nullptr);
g_app_config = this;
SetupEntries();
}
template <typename T>
void AppConfig::CompleteMap(const T& entry_map) {
for (auto&& i : entry_map) {
assert(entries_by_name_.find(i.second.name()) == entries_by_name_.end());
assert(i.first < decltype(i.first)::kLast);
entries_by_name_[i.second.name()] = &i.second;
}
// Make sure all values have entries.
#if BA_DEBUG_BUILD
int last = static_cast<int>(decltype(entry_map.begin()->first)::kLast); // ew
for (int j = 0; j < last; ++j) {
auto i2 =
entry_map.find(static_cast<decltype(entry_map.begin()->first)>(j));
if (i2 == entry_map.end()) {
throw Exception("Missing appconfig entry " + std::to_string(j));
}
}
#endif
}
void AppConfig::SetupEntries() {
// Register all our typed entries.
float_entries_[FloatID::kScreenGamma] = FloatEntry("Screen Gamma", 1.0F);
float_entries_[FloatID::kScreenPixelScale] =
FloatEntry("Screen Pixel Scale", 1.0F);
float_entries_[FloatID::kTouchControlsScale] =
FloatEntry("Touch Controls Scale", 1.0F);
float_entries_[FloatID::kTouchControlsScaleMovement] =
FloatEntry("Touch Controls Scale Movement", 1.0F);
float_entries_[FloatID::kTouchControlsScaleActions] =
FloatEntry("Touch Controls Scale Actions", 1.0F);
float_entries_[FloatID::kSoundVolume] = FloatEntry("Sound Volume", 1.0F);
float_entries_[FloatID::kMusicVolume] = FloatEntry("Music Volume", 1.0F);
// Note: keep this synced with the defaults in MainActivity.java.
float gvrrts_default = g_platform->IsRunningOnDaydream() ? 1.0F : 0.5F;
float_entries_[FloatID::kGoogleVRRenderTargetScale] =
FloatEntry("GVR Render Target Scale", gvrrts_default);
string_entries_[StringID::kResolutionAndroid] =
StringEntry("Resolution (Android)", "Auto");
string_entries_[StringID::kTouchActionControlType] =
StringEntry("Touch Action Control Type", "buttons");
string_entries_[StringID::kTouchMovementControlType] =
StringEntry("Touch Movement Control Type", "swipe");
string_entries_[StringID::kGraphicsQuality] =
StringEntry("Graphics Quality", "Auto");
string_entries_[StringID::kTextureQuality] =
StringEntry("Texture Quality", "Auto");
string_entries_[StringID::kVerticalSync] =
StringEntry("Vertical Sync", "Auto");
string_entries_[StringID::kVRHeadRelativeAudio] =
StringEntry("VR Head Relative Audio", "Auto");
string_entries_[StringID::kMacControllerSubsystem] =
StringEntry("Mac Controller Subsystem", "Classic");
string_entries_[StringID::kTelnetPassword] =
StringEntry("Telnet Password", "changeme");
int_entries_[IntID::kPort] = IntEntry("Port", kDefaultPort);
int_entries_[IntID::kTelnetPort] =
IntEntry("Telnet Port", kDefaultTelnetPort);
bool_entries_[BoolID::kTouchControlsSwipeHidden] =
BoolEntry("Touch Controls Swipe Hidden", false);
bool_entries_[BoolID::kFullscreen] = BoolEntry("Fullscreen", false);
bool_entries_[BoolID::kKickIdlePlayers] =
BoolEntry("Kick Idle Players", false);
bool_entries_[BoolID::kAlwaysUseInternalKeyboard] =
BoolEntry("Always Use Internal Keyboard", false);
bool_entries_[BoolID::kShowFPS] = BoolEntry("Show FPS", false);
bool_entries_[BoolID::kTVBorder] =
BoolEntry("TV Border", g_platform->IsRunningOnTV());
bool_entries_[BoolID::kKeyboardP2Enabled] =
BoolEntry("Keyboard P2 Enabled", false);
bool_entries_[BoolID::kEnablePackageMods] =
BoolEntry("Enable Package Mods", false);
bool_entries_[BoolID::kChatMuted] = BoolEntry("Chat Muted", false);
bool_entries_[BoolID::kEnableRemoteApp] =
BoolEntry("Enable Remote App", true);
bool_entries_[BoolID::kEnableTelnet] = BoolEntry("Enable Telnet", true);
bool_entries_[BoolID::kDisableCameraShake] =
BoolEntry("Disable Camera Shake", false);
bool_entries_[BoolID::kDisableCameraGyro] =
BoolEntry("Disable Camera Gyro", false);
// Now add everything to our name map and make sure all is kosher.
CompleteMap(float_entries_);
CompleteMap(int_entries_);
CompleteMap(string_entries_);
CompleteMap(bool_entries_);
}
auto AppConfig::Resolve(FloatID id) -> float {
auto i = float_entries_.find(id);
if (i == float_entries_.end()) {
throw Exception("Invalid config entry");
}
return i->second.Resolve();
}
auto AppConfig::Resolve(StringID id) -> std::string {
auto i = string_entries_.find(id);
if (i == string_entries_.end()) {
throw Exception("Invalid config entry");
}
return i->second.Resolve();
}
auto AppConfig::Resolve(BoolID id) -> bool {
auto i = bool_entries_.find(id);
if (i == bool_entries_.end()) {
throw Exception("Invalid config entry");
}
return i->second.Resolve();
}
auto AppConfig::Resolve(IntID id) -> int {
auto i = int_entries_.find(id);
if (i == int_entries_.end()) {
throw Exception("Invalid config entry");
}
return i->second.Resolve();
}
} // namespace ballistica

View File

@ -0,0 +1,130 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_APP_APP_CONFIG_H_
#define BALLISTICA_APP_APP_CONFIG_H_
#include <map>
#include <memory>
#include <string>
#include <vector>
namespace ballistica {
// This class wrangles user config values for the app.
// The underlying config data currently lives in the Python layer,
// so at the moment these calls are only usable from the game thread,
// but that may change in the future.
class AppConfig {
public:
// Our official config values:
enum class FloatID {
kScreenGamma,
kScreenPixelScale,
kTouchControlsScale,
kTouchControlsScaleMovement,
kTouchControlsScaleActions,
kSoundVolume,
kMusicVolume,
kGoogleVRRenderTargetScale,
kLast // Sentinel.
};
enum class StringID {
kResolutionAndroid,
kTouchActionControlType,
kTouchMovementControlType,
kGraphicsQuality,
kTextureQuality,
kVerticalSync,
kVRHeadRelativeAudio,
kMacControllerSubsystem,
kTelnetPassword,
kLast // Sentinel.
};
enum class IntID {
kPort,
kTelnetPort,
kLast // Sentinel.
};
enum class BoolID {
kTouchControlsSwipeHidden,
kFullscreen,
kKickIdlePlayers,
kAlwaysUseInternalKeyboard,
kShowFPS,
kTVBorder,
kKeyboardP2Enabled,
kEnablePackageMods,
kChatMuted,
kEnableRemoteApp,
kEnableTelnet,
kDisableCameraShake,
kDisableCameraGyro,
kLast // Sentinel.
};
class Entry {
public:
enum class Type { kString, kInt, kFloat, kBool };
Entry() = default;
explicit Entry(const char* name) : name_(name) {}
virtual auto GetType() const -> Type = 0;
auto name() const -> const std::string& { return name_; }
virtual auto FloatValue() const -> float;
virtual auto StringValue() const -> std::string;
virtual auto IntValue() const -> int;
virtual auto BoolValue() const -> bool;
virtual auto DefaultFloatValue() const -> float;
virtual auto DefaultStringValue() const -> std::string;
virtual auto DefaultIntValue() const -> int;
virtual auto DefaultBoolValue() const -> bool;
private:
std::string name_;
};
static void Init();
AppConfig();
// Given specific ids, returns resolved values (fastest access).
auto Resolve(FloatID id) -> float;
auto Resolve(StringID id) -> std::string;
auto Resolve(IntID id) -> int;
auto Resolve(BoolID id) -> bool;
// Given a name, returns an entry (or nullptr).
// You should check the entry's type and request
// the corresponding typed resolved value from it.
auto GetEntry(const std::string& name) -> const Entry* {
auto i = entries_by_name_.find(name);
if (i == entries_by_name_.end()) {
return nullptr;
}
return i->second;
}
auto entries_by_name() const -> const std::map<std::string, const Entry*>& {
return entries_by_name_;
}
private:
class StringEntry;
class FloatEntry;
class IntEntry;
class BoolEntry;
template <typename T>
void CompleteMap(const T& entry_map);
void SetupEntries();
std::map<std::string, const Entry*> entries_by_name_;
std::map<FloatID, FloatEntry> float_entries_;
std::map<IntID, IntEntry> int_entries_;
std::map<StringID, StringEntry> string_entries_;
std::map<BoolID, BoolEntry> bool_entries_;
};
} // namespace ballistica
#endif // BALLISTICA_APP_APP_CONFIG_H_

View File

@ -0,0 +1,12 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/app/app_globals.h"
namespace ballistica {
AppGlobals::AppGlobals(int argc_in, char** argv_in)
: argc{argc_in},
argv{argv_in},
main_thread_id{std::this_thread::get_id()} {}
} // namespace ballistica

View File

@ -0,0 +1,99 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_APP_APP_GLOBALS_H_
#define BALLISTICA_APP_APP_GLOBALS_H_
#include <map>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "ballistica/ballistica.h"
#include "ballistica/networking/master_server_config.h"
namespace ballistica {
// The first thing the engine does is allocate an instance of this as g_globals.
// As much as possible, previously static/global values should be moved to here,
// ideally as a temporary measure until they can be placed as non-static members
// in the proper classes.
// Any use of non-trivial global/static values such as class instances should be
// avoided since it can introduce ambiguities during init and teardown.
// For more explanation, see the 'Static and Global Variables' section in the
// Google C++ Style Guide.
class AppGlobals {
public:
AppGlobals(int argc, char** argv);
/// Program argument count (on applicable platforms).
int argc{};
/// Program argument values (on applicable platforms).
char** argv{};
std::map<std::string, NodeType*> node_types;
std::map<int, NodeType*> node_types_by_id;
std::map<std::string, NodeMessageType> node_message_types;
std::vector<std::string> node_message_formats;
std::string calced_blessing_hash;
bool have_mods{};
bool replay_open{};
std::vector<Thread*> pausable_threads;
TouchInput* touch_input{};
std::string console_startup_messages;
std::mutex log_mutex;
std::string log;
bool put_log{};
bool log_full{};
int master_server_source{1};
int session_count{};
bool shutting_down{};
bool have_incentivized_ad{true};
bool should_pause{};
TelnetServer* telnet_server{};
Console* console{};
bool reset_vr_orientation{};
bool user_ran_commands{};
UIScale ui_scale{UIScale::kLarge};
AccountType account_type{AccountType::kInvalid};
bool remote_server_accepting_connections{true};
std::string game_commands;
std::string user_agent_string{"BA_USER_AGENT_UNSET (" BA_PLATFORM_STRING ")"};
int return_value{};
bool is_stdin_a_terminal{true};
std::thread::id main_thread_id{};
bool is_bootstrapped{};
bool args_handled{};
std::string user_config_dir;
bool started_suicide{};
// Netplay testing.
int buffer_time{1000 / 30};
// How often we send dynamics sync packets.
int dynamics_sync_time{500};
// How many steps we sample for each bucket.
int delay_samples{20};
bool vr_mode{g_buildconfig.vr_build()};
// Temp dirty way to do some shutdown stuff (FIXME: move to an App method).
void (*temp_cleanup_callback)() = nullptr;
millisecs_t real_time{};
millisecs_t last_real_time_ticks{};
std::mutex real_time_mutex;
std::mutex thread_name_map_mutex;
std::map<std::thread::id, std::string> thread_name_map;
std::string master_server_addr{BA_MASTER_SERVER_DEFAULT_ADDR};
std::string master_server_fallback_addr{BA_MASTER_SERVER_FALLBACK_ADDR};
#if BA_DEBUG_BUILD
std::mutex object_list_mutex;
Object* object_list_first{};
int object_count{0};
#endif
};
} // namespace ballistica
#endif // BALLISTICA_APP_APP_GLOBALS_H_

View File

@ -0,0 +1,21 @@
// Copyright (c) 2011-2020 Eric Froemling
#if BA_HEADLESS_BUILD
#include "ballistica/app/headless_app.h"
#include "ballistica/ballistica.h"
namespace ballistica {
// We could technically use the vanilla App class here since we're not
// changing anything.
HeadlessApp::HeadlessApp(Thread* thread) : App(thread) {
// NewThreadTimer(10, true, NewLambdaRunnable([this] {
// assert(g_app);
// g_app->RunEvents();
// }));
}
} // namespace ballistica
#endif // BA_HEADLESS_BUILD

View File

@ -0,0 +1,20 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_APP_HEADLESS_APP_H_
#define BALLISTICA_APP_HEADLESS_APP_H_
#if BA_HEADLESS_BUILD
#include "ballistica/app/app.h"
#include "ballistica/core/thread.h"
namespace ballistica {
class HeadlessApp : public App {
public:
explicit HeadlessApp(Thread* thread);
};
} // namespace ballistica
#endif // BA_HEADLESS_BUILD
#endif // BALLISTICA_APP_HEADLESS_APP_H_

View File

@ -0,0 +1,101 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/app/stress_test.h"
#include "ballistica/ballistica.h"
#include "ballistica/graphics/graphics_server.h"
#include "ballistica/graphics/renderer.h"
#include "ballistica/input/input.h"
#include "ballistica/platform/platform.h"
namespace ballistica {
void StressTest::Update() {
assert(InMainThread());
// Handle a little misc stuff here.
// If we're currently running stress-tests, update that stuff.
if (stress_testing_ && g_input) {
// Update our fake inputs to make our dudes run around.
g_input->ProcessStressTesting(stress_test_player_count_);
// Every 10 seconds update our stress-test stats.
millisecs_t t = GetRealTime();
if (t - last_stress_test_update_time_ >= 10000) {
if (stress_test_stats_file_ == nullptr) {
assert(g_platform);
std::string f_name =
g_platform->GetUserPythonDirectory() + "/stress_test_stats.csv";
stress_test_stats_file_ = g_platform->FOpen(f_name.c_str(), "wb");
if (stress_test_stats_file_ != nullptr) {
fprintf(stress_test_stats_file_,
"time,averageFps,nodes,models,collide_models,textures,sounds,"
"pssMem,sharedDirtyMem,privateDirtyMem\n");
fflush(stress_test_stats_file_);
if (g_buildconfig.ostype_android()) {
// On android, let the OS know we've added or removed a file
// (limit to android or we'll get an unimplemented warning).
g_platform->AndroidRefreshFile(f_name);
}
}
}
if (stress_test_stats_file_ != nullptr) {
// See how many frames we've rendered this past interval.
int total_frames_rendered;
if (g_graphics_server && g_graphics_server->renderer()) {
total_frames_rendered =
g_graphics_server->renderer()->total_frames_rendered();
} else {
total_frames_rendered = last_total_frames_rendered_;
}
float avg =
static_cast<float>(total_frames_rendered
- last_total_frames_rendered_)
/ (static_cast<float>(t - last_stress_test_update_time_) / 1000.0f);
last_total_frames_rendered_ = total_frames_rendered;
uint32_t model_count = 0;
uint32_t collide_model_count = 0;
uint32_t texture_count = 0;
uint32_t sound_count = 0;
uint32_t node_count = 0;
if (g_media) {
model_count = g_media->total_model_count();
collide_model_count = g_media->total_collide_model_count();
texture_count = g_media->total_texture_count();
sound_count = g_media->total_sound_count();
}
assert(g_game);
std::string mem_usage = g_platform->GetMemUsageInfo();
fprintf(stress_test_stats_file_, "%d,%.1f,%d,%d,%d,%d,%d,%s\n",
static_cast_check_fit<int>(GetRealTime()), avg, node_count,
model_count, collide_model_count, texture_count, sound_count,
mem_usage.c_str());
fflush(stress_test_stats_file_);
}
last_stress_test_update_time_ = t;
}
}
}
void StressTest::Set(bool enable, int player_count) {
assert(InMainThread());
bool was_stress_testing = stress_testing_;
stress_testing_ = enable;
stress_test_player_count_ = player_count;
// If we're turning on, reset our intervals and things.
if (!was_stress_testing && stress_testing_) {
// So our first sample is 1 interval from now.
last_stress_test_update_time_ = GetRealTime();
// Reset our frames-rendered tally.
if (g_graphics_server && g_graphics_server->renderer()) {
last_total_frames_rendered_ =
g_graphics_server->renderer()->total_frames_rendered();
} else {
// Assume zero if there's no graphics yet.
last_total_frames_rendered_ = 0;
}
}
}
} // namespace ballistica

View File

@ -0,0 +1,29 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_APP_STRESS_TEST_H_
#define BALLISTICA_APP_STRESS_TEST_H_
#include "ballistica/ballistica.h"
namespace ballistica {
// FIXME: This is not wired up; I just moved things here from App.
class StressTest {
public:
// This used to be a SetStressTesting() call in App.
void Set(bool enable, int player_count);
// This used to get run from RunEvents() in App.
void Update();
private:
FILE* stress_test_stats_file_{};
millisecs_t last_stress_test_update_time_{};
bool stress_testing_{};
int stress_test_player_count_{8};
int last_total_frames_rendered_{};
};
} // namespace ballistica
#endif // BALLISTICA_APP_STRESS_TEST_H_

View File

@ -0,0 +1,110 @@
// Copyright (c) 2011-2020 Eric Froemling
#if BA_VR_BUILD
#include "ballistica/app/vr_app.h"
#include "ballistica/game/game.h"
#include "ballistica/graphics/graphics_server.h"
#include "ballistica/graphics/renderer.h"
namespace ballistica {
VRApp::VRApp(Thread* thread) : App(thread) {}
void VRApp::PushVRSimpleRemoteStateCall(const VRSimpleRemoteState& state) {
PushCall([this, state] {
// Convert this to a full hands state, adding in some simple elbow
// positioning of our own and left/right.
VRHandsState s;
s.l.tx = -0.2f;
s.l.ty = -0.2f;
s.l.tz = -0.3f;
// Hmm; for now lets always assign this as right hand even when its in
// left-handed mode to keep things simple on the back-end. Can change later
// if there's a downside to that.
s.r.type = VRHandType::kDaydreamRemote;
s.r.tx = 0.2f;
s.r.ty = -0.2f;
s.r.tz = -0.3f;
s.r.yaw = state.r0;
s.r.pitch = state.r1;
s.r.roll = state.r2;
VRSetHands(s);
});
}
void VRApp::VRSetDrawDimensions(int w, int h) {
g_graphics_server->VideoResize(w, h);
}
void VRApp::VRPreDraw() {
if (!g_graphics_server || !g_graphics_server->renderer()) {
return;
}
assert(InMainThread());
if (FrameDef* frame_def = g_graphics_server->GetRenderFrameDef()) {
// Note: this could be part of PreprocessRenderFrameDef but
// the non-vr path needs it to be separate since preprocess doesn't
// happen sometimes. Should probably clean that up.
g_graphics_server->RunFrameDefMeshUpdates(frame_def);
// store this for the duration of this frame
vr_render_frame_def_ = frame_def;
g_graphics_server->PreprocessRenderFrameDef(frame_def);
}
}
void VRApp::VRPostDraw() {
assert(InMainThread());
if (!g_graphics_server || !g_graphics_server->renderer()) {
return;
}
if (vr_render_frame_def_) {
g_graphics_server->FinishRenderFrameDef(vr_render_frame_def_);
vr_render_frame_def_ = nullptr;
}
RunRenderUpkeepCycle();
}
void VRApp::VRSetHead(float tx, float ty, float tz, float yaw, float pitch,
float roll) {
assert(InMainThread());
Renderer* renderer = g_graphics_server->renderer();
if (renderer == nullptr) return;
renderer->VRSetHead(tx, ty, tz, yaw, pitch, roll);
}
void VRApp::VRSetHands(const VRHandsState& state) {
assert(InMainThread());
// Pass this along to the renderer (in this same thread) for drawing
// (so hands can be drawn at their absolute most up-to-date positions, etc).
Renderer* renderer = g_graphics_server->renderer();
if (renderer == nullptr) return;
renderer->VRSetHands(state);
// ALSO ship it off to the game/ui thread to actually handle input from it.
g_game->PushVRHandsState(state);
}
void VRApp::VRDrawEye(int eye, float yaw, float pitch, float roll, float tan_l,
float tan_r, float tan_b, float tan_t, float eye_x,
float eye_y, float eye_z, int viewport_x,
int viewport_y) {
if (!g_graphics_server || !g_graphics_server->renderer()) {
return;
}
assert(InMainThread());
if (vr_render_frame_def_) {
// set up VR eye stuff...
Renderer* renderer = g_graphics_server->renderer();
renderer->VRSetEye(eye, yaw, pitch, roll, tan_l, tan_r, tan_b, tan_t, eye_x,
eye_y, eye_z, viewport_x, viewport_y);
g_graphics_server->DrawRenderFrameDef(vr_render_frame_def_);
}
}
} // namespace ballistica
#endif // BA_VR_BUILD

View File

@ -0,0 +1,48 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_APP_VR_APP_H_
#define BALLISTICA_APP_VR_APP_H_
#if BA_VR_BUILD
#include "ballistica/app/app.h"
namespace ballistica {
class VRApp : public App {
public:
/// For passing in state of Daydream remote (and maybe gear vr?..).
struct VRSimpleRemoteState {
bool right_handed = true;
float r0 = 0.0f;
float r1 = 0.0f;
float r2 = 0.0f;
};
/// Return g_app as a VRApp. (assumes it actually is one).
static VRApp* get() {
assert(g_app != nullptr);
assert(dynamic_cast<VRApp*>(g_app) == static_cast<VRApp*>(g_app));
return static_cast<VRApp*>(g_app);
}
explicit VRApp(Thread* thread);
void PushVRSimpleRemoteStateCall(const VRSimpleRemoteState& state);
void VRSetDrawDimensions(int w, int h);
void VRPreDraw();
void VRPostDraw();
void VRSetHead(float tx, float ty, float tz, float yaw, float pitch,
float roll);
void VRSetHands(const VRHandsState& state);
void VRDrawEye(int eye, float yaw, float pitch, float roll, float tan_l,
float tan_r, float tan_b, float tan_t, float eye_x,
float eye_y, float eye_z, int viewport_x, int viewport_y);
private:
FrameDef* vr_render_frame_def_{};
};
} // namespace ballistica
#endif // BA_VR_BUILD
#endif // BALLISTICA_APP_VR_APP_H_