Adding more C++ sources

This commit is contained in:
Eric Froemling 2020-10-01 13:52:02 -05:00
parent 0804d7fcbb
commit 87b6716d59
46 changed files with 9299 additions and 55 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/24/b7/f7a54a77a43a97670bd448dfd3cc",
"build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/ae/55/a35c61332f2b6d761f87f9ec2094",
"build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/4d/04/9b581b616ff015783ae7933dcd6b",
"build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/db/89/e20095265d6f9ba568d983cf7e1f",
"build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/88/e3/65bba5e8585d7fc3f181ad6a3ad4",
"build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/78/26/83c0879bee2364c7ebac1aa0fa0d",
"build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/14/89/b155bde4ec2b545f02dd95948a1d",
"build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/56/d7/34926c551b6af98f8cfc038eb772",
"build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/ed/4e/8c5687c5130c5e99e355ddca6e92",
"build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/8d/38/e075856c1a5bac98885c03c92c3e"
"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"
}

View File

@ -29,8 +29,8 @@
<w>achname</w>
<w>achs</w>
<w>acinstance</w>
<w>ack</w>
<w>ack'ed</w>
<w>ack</w>
<w>acked</w>
<w>acks</w>
<w>acnt</w>
@ -151,8 +151,8 @@
<w>bacommon</w>
<w>badguy</w>
<w>bafoundation</w>
<w>ballistica</w>
<w>ballistica's</w>
<w>ballistica</w>
<w>ballisticacore</w>
<w>ballisticacorecb</w>
<w>bamaster</w>
@ -793,8 +793,8 @@
<w>gamedata</w>
<w>gameinstance</w>
<w>gamemap</w>
<w>gamepad</w>
<w>gamepad's</w>
<w>gamepad</w>
<w>gamepadadvanced</w>
<w>gamepads</w>
<w>gamepadselect</w>
@ -1177,8 +1177,8 @@
<w>lsqlite</w>
<w>lssl</w>
<w>lstart</w>
<w>lstr</w>
<w>lstr's</w>
<w>lstr</w>
<w>lstrs</w>
<w>lsval</w>
<w>ltex</w>
@ -1362,6 +1362,9 @@
<w>nosub</w>
<w>nosyncdir</w>
<w>nosyncdirs</w>
<w>nosyncfile</w>
<w>nosyncfiles</w>
<w>nosynctool</w>
<w>nosynctools</w>
<w>notdir</w>
<w>npos</w>
@ -1800,8 +1803,8 @@
<w>sessionname</w>
<w>sessionplayer</w>
<w>sessionplayers</w>
<w>sessionteam</w>
<w>sessionteam's</w>
<w>sessionteam</w>
<w>sessionteams</w>
<w>sessiontype</w>
<w>setactivity</w>
@ -2131,8 +2134,8 @@
<w>txtw</w>
<w>typeargs</w>
<w>typecheck</w>
<w>typechecker</w>
<w>typechecker's</w>
<w>typechecker</w>
<w>typedval</w>
<w>typeshed</w>
<w>typestr</w>

View File

@ -2,6 +2,22 @@
"code_source_dirs": [
"src/ballistica"
],
"cpplint_blacklist": [
"src/ballistica/generic/json.cc",
"src/ballistica/generic/json.h",
"src/ballistica/generic/utf8.cc",
"src/ballistica/graphics/texture/dds.h",
"src/ballistica/graphics/texture/ktx.cc",
"src/ballistica/platform/android/android_gl3.h",
"src/ballistica/platform/apple/app_delegate.h",
"src/ballistica/platform/apple/scripting_bridge_music.h",
"src/ballistica/platform/android/utf8/checked.h",
"src/ballistica/platform/android/utf8/unchecked.h",
"src/ballistica/platform/android/utf8/core.h",
"src/ballistica/platform/apple/sdl_main_mac.h",
"src/ballistica/platform/oculus/main_rift.cc",
"src/ballistica/platform/android/android_gl3.c"
],
"name": "BallisticaCore",
"public": true,
"pylint_ignored_untracked_deps": [

View File

@ -0,0 +1,343 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/ballistica.h"
#include <map>
#include "ballistica/app/app.h"
#include "ballistica/app/app_config.h"
#include "ballistica/app/app_globals.h"
#include "ballistica/audio/audio.h"
#include "ballistica/audio/audio_server.h"
#include "ballistica/core/fatal_error.h"
#include "ballistica/core/logging.h"
#include "ballistica/core/thread.h"
#include "ballistica/dynamics/bg/bg_dynamics.h"
#include "ballistica/dynamics/bg/bg_dynamics_server.h"
#include "ballistica/game/account.h"
#include "ballistica/graphics/graphics.h"
#include "ballistica/graphics/graphics_server.h"
#include "ballistica/input/input.h"
#include "ballistica/media/media.h"
#include "ballistica/media/media_server.h"
#include "ballistica/networking/network_write_module.h"
#include "ballistica/networking/networking.h"
#include "ballistica/platform/platform.h"
#include "ballistica/python/python.h"
#include "ballistica/scene/scene.h"
namespace ballistica {
// These are set automatically via script; don't change here.
const int kAppBuildNumber = 20192;
const char* kAppVersion = "1.5.26";
const char* kBlessingHash = nullptr;
// Our standalone globals.
// These are separated out for easy access.
// Everything else should go into AppGlobals (or more ideally into a class).
int g_early_log_writes{10};
Thread* g_main_thread{};
AppGlobals* g_app_globals{};
AppConfig* g_app_config{};
App* g_app{};
Account* g_account{};
Game* g_game{};
BGDynamics* g_bg_dynamics{};
BGDynamicsServer* g_bg_dynamics_server{};
Platform* g_platform{};
Utils* g_utils{};
UI* g_ui{};
Graphics* g_graphics{};
Python* g_python{};
Input* g_input{};
GraphicsServer* g_graphics_server{};
Media* g_media{};
Audio* g_audio{};
MediaServer* g_media_server{};
AudioServer* g_audio_server{};
StdInputModule* g_std_input_module{};
NetworkReader* g_network_reader{};
Networking* g_networking{};
NetworkWriteModule* g_network_write_module{};
TextGraphics* g_text_graphics{};
// Basic overview of our bootstrapping process:
// 1: All threads and globals are created and provisioned. Everything above
// should exist at the end of this step (if it is going to exist).
// Threads should not be talking to each other yet at this point.
// 2: The system is set in motion. Game thread is told to load/apply the config.
// This kicks off an initial-screen-creation message sent to the
// graphics-server thread. Other systems are informed that bootstrapping
// is complete and they are free to talk to each other. Initial input-devices
// are added, media loads can begin (at least ones not dependent on the
// screen/renderer), etc.
// 3: The initial screen is created on the graphics-server thread in response
// to the message sent from the game thread. A completion notice is sent
// back to the game thread when done.
// 4: Back on the game thread, any renderer-dependent media-loads/etc. can begin
// and lastly the initial game session is kicked off.
auto BallisticaMain(int argc, char** argv) -> int {
try {
// Even at the absolute start of execution we should be able to
// phone home on errors. Set BA_CRASH_TEST=1 to test this.
if (const char* crashenv = getenv("BA_CRASH_TEST")) {
if (!strcmp(crashenv, "1")) {
FatalError("Fatal-Error-Test");
}
}
// -------------------------------------------------------------------------
// Phase 1: Create and provision all globals.
// -------------------------------------------------------------------------
g_app_globals = new AppGlobals(argc, argv);
g_platform = Platform::Create();
g_platform->PostInit();
g_account = new Account();
g_utils = new Utils();
Scene::Init();
// Create a Thread wrapper around the current (main) thread.
g_main_thread = new Thread(ThreadIdentifier::kMain, ThreadType::kMain);
// Spin up g_app.
g_platform->CreateApp();
// Spin up our other standard threads.
auto* media_thread = new Thread(ThreadIdentifier::kMedia);
g_app_globals->pausable_threads.push_back(media_thread);
auto* audio_thread = new Thread(ThreadIdentifier::kAudio);
g_app_globals->pausable_threads.push_back(audio_thread);
auto* game_thread = new Thread(ThreadIdentifier::kGame);
g_app_globals->pausable_threads.push_back(game_thread);
auto* network_write_thread = new Thread(ThreadIdentifier::kNetworkWrite);
g_app_globals->pausable_threads.push_back(network_write_thread);
// And add our other standard modules to them.
game_thread->AddModule<Game>();
network_write_thread->AddModule<NetworkWriteModule>();
media_thread->AddModule<MediaServer>();
g_main_thread->AddModule<GraphicsServer>();
audio_thread->AddModule<AudioServer>();
// Now let the platform spin up any other threads/modules it uses.
// (bg-dynamics in non-headless builds, stdin/stdout where applicable, etc.)
g_platform->CreateAuxiliaryModules();
// Ok at this point we can be considered up-and-running.
g_app_globals->is_bootstrapped = true;
// -------------------------------------------------------------------------
// Phase 2: Set things in motion.
// -------------------------------------------------------------------------
// Ok; now that we're bootstrapped, tell the game thread to read and apply
// the config which should kick off the real action.
g_game->PushApplyConfigCall();
// Let the app and platform do whatever else it wants here such as adding
// initial input devices/etc.
g_app->OnBootstrapComplete();
g_platform->OnBootstrapComplete();
// -------------------------------------------------------------------------
// Phase 3/4: Create a screen and/or kick off game (in other threads).
// -------------------------------------------------------------------------
if (g_app->UsesEventLoop()) {
// On our event-loop using platforms we now simply sit in our event loop
// until the app is quit.
g_main_thread->RunEventLoop(false);
} else {
// In this case we'll now simply return and let the OS feed us events
// until the app quits.
// However we may need to 'prime the pump' first. For instance,
// if the main thread event loop is driven by frame draws, it may need to
// manually pump events until drawing begins (otherwise it will never
// process the 'create-screen' event and wind up deadlocked).
g_app->PrimeEventPump();
}
} catch (const std::exception& exc) {
std::string error_msg =
std::string("Unhandled exception in BallisticaMain(): ") + exc.what();
FatalError::ReportFatalError(error_msg, true);
bool exit_cleanly = !IsUnmodifiedBlessedBuild();
bool handled = FatalError::HandleFatalError(exit_cleanly, true);
// Do the default thing if it's not been handled.
if (!handled) {
if (exit_cleanly) {
exit(1);
} else {
throw;
}
}
}
// printf("BLESSED? %d\n", static_cast<int>(IsUnmodifiedBlessedBuild()));
g_platform->WillExitMain(false);
return g_app_globals->return_value;
}
auto GetRealTime() -> millisecs_t {
millisecs_t t = g_platform->GetTicks();
// If we're at a different time than our last query, do our funky math.
if (t != g_app_globals->last_real_time_ticks) {
std::lock_guard<std::mutex> lock(g_app_globals->real_time_mutex);
millisecs_t passed = t - g_app_globals->last_real_time_ticks;
// GetTicks() is supposed to be monotonic but I've seen 'passed'
// equal -1 even when it is using std::chrono::steady_clock. Let's do
// our own filtering here to make 100% sure we don't go backwards.
if (passed < 0) {
passed = 0;
} else {
// Super big times-passed probably means we went to sleep or something;
// clamp to a reasonable value.
if (passed > 250) {
passed = 250;
}
}
g_app_globals->real_time += passed;
g_app_globals->last_real_time_ticks = t;
}
return g_app_globals->real_time;
}
auto FatalError(const std::string& message) -> void {
FatalError::ReportFatalError(message, false);
bool exit_cleanly = !IsUnmodifiedBlessedBuild();
bool handled = FatalError::HandleFatalError(exit_cleanly, false);
assert(handled);
}
auto GetUniqueSessionIdentifier() -> const std::string& {
static std::string session_id;
static bool have_session_id = false;
if (!have_session_id) {
srand(static_cast<unsigned int>(
Platform::GetCurrentMilliseconds())); // NOLINT
uint32_t tval = static_cast<uint32_t>(rand()); // NOLINT
assert(g_platform);
session_id = g_platform->GetUniqueDeviceIdentifier() + std::to_string(tval);
have_session_id = true;
if (session_id.size() >= 100) {
Log("WARNING: session id longer than it should be.");
}
}
return session_id;
}
auto InGameThread() -> bool {
return (g_game && g_game->thread()->IsCurrent());
}
auto InMainThread() -> bool {
return (g_app_globals
&& std::this_thread::get_id() == g_app_globals->main_thread_id);
}
auto InGraphicsThread() -> bool {
return (g_graphics_server && g_graphics_server->thread()->IsCurrent());
}
auto InAudioThread() -> bool {
return (g_audio_server && g_audio_server->thread()->IsCurrent());
}
auto InBGDynamicsThread() -> bool {
#if !BA_HEADLESS_BUILD
return (g_bg_dynamics_server && g_bg_dynamics_server->thread()->IsCurrent());
#else
return false;
#endif
}
auto InMediaThread() -> bool {
return (g_media_server && g_media_server->thread()->IsCurrent());
}
auto InNetworkWriteThread() -> bool {
return (g_network_write_module
&& g_network_write_module->thread()->IsCurrent());
}
auto GetInterfaceType() -> UIScale { return g_app_globals->ui_scale; }
void Log(const std::string& msg, bool to_stdout, bool to_server) {
Logging::Log(msg, to_stdout, to_server);
}
auto IsVRMode() -> bool { return g_app_globals->vr_mode; }
auto IsStdinATerminal() -> bool { return g_app_globals->is_stdin_a_terminal; }
void ScreenMessage(const std::string& s, const Vector3f& color) {
if (g_game) {
g_game->PushScreenMessage(s, color);
} else {
Log("ScreenMessage before g_game init (will be lost): '" + s + "'");
}
}
void ScreenMessage(const std::string& msg) {
ScreenMessage(msg, {1.0f, 1.0f, 1.0f});
}
auto GetCurrentThreadName() -> std::string {
return Thread::GetCurrentThreadName();
}
auto IsBootstrapped() -> bool { return g_app_globals->is_bootstrapped; }
// Used by our built in exception type.
void SetPythonException(PyExcType python_type, const char* description) {
Python::SetPythonException(python_type, description);
}
auto IsUnmodifiedBlessedBuild() -> bool {
// Assume debug builds are not blessed (we'll determine this after
// we finish calcing blessing hash, but this we don't get false positives
// up until that point)
if (g_buildconfig.debug_build()) {
return false;
}
// Return false if we're unblessed or it seems that the user is likely
// mucking around with stuff. If we just don't know yet
// (for instance if blessing has calc hasn't completed) we assume we're
// clean.
if (g_app_globals && g_app_globals->user_ran_commands) {
return false;
}
// If they're using custom app scripts, just consider it modified.
// Otherwise can can tend to get errors in early bootstrapping before
// we've been able to calc hashes to see if things are modified.
if (g_platform && g_platform->using_custom_app_python_dir()) {
return false;
}
// If we don't have an embedded blessing hash, we're not blessed. Duh.
if (kBlessingHash == nullptr) {
return false;
}
// If we have an embedded hash and we've calced ours
// and it doesn't match, consider ourself modified.
return !(g_app_globals && !g_app_globals->calced_blessing_hash.empty()
&& g_app_globals->calced_blessing_hash != kBlessingHash);
}
} // namespace ballistica
// If desired, define main() in the global namespace.
#if BA_DEFINE_MAIN
auto main(int argc, char** argv) -> int {
return ballistica::BallisticaMain(argc, argv);
}
#endif

226
src/ballistica/ballistica.h Normal file
View File

@ -0,0 +1,226 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_BALLISTICA_H_
#define BALLISTICA_BALLISTICA_H_
// Try to ensure they're providing proper config stuff.
#ifndef BA_HAVE_CONFIG
#error platform config has not been defined!
#endif
// FIXME: We need to update to C++17 to get unified std::abs().
// Until we do that, int types are defined in <cstdlib>
// and float/double in <cmath>, meaning its possible to call the wrong
// version if we aren't careful and only include one header.
// For now just including both here at the top level to hopefully
// minimize problems.
#ifdef __cplusplus
#include <cassert>
#include <cmath>
#include <cstdlib>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
#endif
#include "ballistica/core/exception.h"
#include "ballistica/core/inline.h"
#include "ballistica/core/macros.h"
#include "ballistica/core/types.h"
// BA 2.0 UI testing.
#define BA_TOOLBAR_TEST 0
#ifdef __cplusplus
namespace ballistica {
extern const int kAppBuildNumber;
extern const char* kAppVersion;
extern const char* kBlessingHash;
// Protocol version we host games with and write replays to.
// This should be incremented whenever there are changes made to the
// session-commands layer (new/removed/changed nodes, attrs, data files,
// behavior, etc.)
// Note that the packet/gamepacket/message layer can vary more organically based
// on build-numbers of connected clients/servers since none of that data is
// stored; this just needs to be observed for all the scene stuff that
// goes into replays since a single stream can get played/replayed on different
// builds (as long as they support that protocol version).
const int kProtocolVersion = 33;
// Oldest protocol version we can act as a client to.
// This can generally be left as-is as long as only
// new nodes/attrs/commands are added and existing
// stuff is unchanged.
const int kProtocolVersionMin = 24;
// FIXME: We should separate out connection protocol from scene protocol. We
// want to be able to watch really old replays if possible but being able to
// connect to old clients is much less important (and slows progress).
// Protocol additions:
// 25: added a few new achievement graphics and new node attrs for displaying
// stuff in front of the UI
// 26: added penguin
// 27: added templates for LOTS of characters
// 28: added cyborg and enabled fallback sounds and textures
// 29: added bunny and eggs
// 30: added support for resource-strings in text-nodes and screen-messages
// 31: added support for short-form resource-strings, time-display-node, and
// string-to-string attr connections
// 32: added json based player profiles message, added shield
// alwaysShowHealthBar attr
// 33: handshake/handshake-response now send json dicts instead of
// just player-specs
// 34: new image_node enums, data assets.
const int kDefaultPort = 43210;
const int kDefaultTelnetPort = 43250;
const float kTVBorder = 0.075f;
const float kVRBorder = 0.085f;
// Largest UDP packets we attempt to send.
// (is there a definitive answer on what this should be?)
const int kMaxPacketSize = 700;
// Extra bytes added to message packets.
const int kMessagePacketHeaderSize = 6;
// The screen, no matter what size/aspect, will always
// fit this virtual rectangle, so placing UI elements within
// these coords is always safe.
// (we currently match the screen ratio of an iPhone 5).
const int kBaseVirtualResX = 1207;
const int kBaseVirtualResY = 680;
// Magic numbers at the start of our file types.
const int kBobFileID = 45623;
const int kCobFileID = 13466;
const int kBrpFileID = 83749;
const float kPi = 3.1415926535897932384626433832795028841971693993751f;
const float kPiDeg = kPi / 180.0f;
const float kDegPi = 180.0f / kPi;
// Sim step size in milliseconds.
const int kGameStepMilliseconds = 8;
// Sim step size in seconds.
const float kGameStepSeconds =
(static_cast<float>(kGameStepMilliseconds) / 1000.0f);
// Globals.
extern int g_early_log_writes;
extern Account* g_account;
extern App* g_app;
extern AppConfig* g_app_config;
extern AppGlobals* g_app_globals;
extern Audio* g_audio;
extern AudioServer* g_audio_server;
extern BGDynamics* g_bg_dynamics;
extern BGDynamicsServer* g_bg_dynamics_server;
extern Context* g_context;
extern Game* g_game;
extern Graphics* g_graphics;
extern GraphicsServer* g_graphics_server;
extern Input* g_input;
extern Thread* g_main_thread;
extern Media* g_media;
extern MediaServer* g_media_server;
extern Networking* g_networking;
extern NetworkReader* g_network_reader;
extern NetworkWriteModule* g_network_write_module;
extern Platform* g_platform;
extern Python* g_python;
extern StdInputModule* g_std_input_module;
extern TextGraphics* g_text_graphics;
extern UI* g_ui;
extern Utils* g_utils;
/// Main ballistica entry point.
auto BallisticaMain(int argc, char** argv) -> int;
/// Return a string that should be universally unique to this device and
/// running instance of the app.
auto GetUniqueSessionIdentifier() -> const std::string&;
/// Have our main threads/modules all been inited yet?
auto IsBootstrapped() -> bool;
/// Does it appear that we are a blessed build with no known user-modifications?
auto IsUnmodifiedBlessedBuild() -> bool;
// The following is a smattering of convenience functions declared in our top
// level namespace. Functionality can be exposed here if it is used often
// enough that avoiding the extra class includes seems like an overall
// compile-time/convenience win.
// Print a momentary message on the screen.
auto ScreenMessage(const std::string& msg) -> void;
auto ScreenMessage(const std::string& msg, const Vector3f& color) -> void;
/// Log a fatal error and kill the app.
/// Can be called from any thread at any time.
/// message is a message to be shown to the user if possible.
/// This will attempt to ship all accumulated logs to the master-server
/// so the standard Log() call can be used before this to include extra
/// info not relevant to the end user.
auto FatalError(const std::string& message = "") -> void;
// Check current-threads.
auto InMainThread() -> bool; // (main and graphics are same currently)
auto InGraphicsThread() -> bool; // (main and graphics are same currently)
auto InGameThread() -> bool;
auto InAudioThread() -> bool;
auto InBGDynamicsThread() -> bool;
auto InMediaThread() -> bool;
auto InNetworkWriteThread() -> bool;
/// Return a human-readable name for the current thread.
auto GetCurrentThreadName() -> std::string;
/// Write a string to the log.
/// This will go to stdout, windows debug log, android log, etc.
/// A trailing newline will be added.
auto Log(const std::string& msg, bool to_stdout = true, bool to_server = true)
-> void;
auto GetInterfaceType() -> UIScale;
/// Return true if stdin seems to be coming from a terminal
/// (so we know to print prompts, etc).
auto IsStdinATerminal() -> bool;
/// Are we running in a VR environment?
auto IsVRMode() -> bool;
/// Are we running headless?
inline auto HeadlessMode() -> bool {
// (currently a build-time value but this could change later)
return g_buildconfig.headless_build();
}
/// Return a lightly-filtered 'real' time value in milliseconds.
/// The value returned here will never go backwards or skip ahead
/// by significant amounts (even if the app has been sleeping or whatnot).
auto GetRealTime() -> millisecs_t;
/// Return a random float value. Not guaranteed to be deterministic or
/// consistent across platforms.
inline auto RandomFloat() -> float {
// FIXME: should convert this to something thread-safe.
return static_cast<float>(
(static_cast<double>(rand()) / RAND_MAX)); // NOLINT
}
auto SetPythonException(PyExcType python_type, const char* description) -> void;
} // namespace ballistica
#endif // __cplusplus
#endif // BALLISTICA_BALLISTICA_H_

View File

@ -0,0 +1,142 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/core/context.h"
#include <string>
#include "ballistica/game/host_activity.h"
#include "ballistica/generic/runnable.h"
#include "ballistica/ui/ui.h"
namespace ballistica {
// Dynamically allocate this; don't want it torn down on quit.
Context* g_context = nullptr;
void Context::Init() {
assert(!g_context);
g_context = new Context(nullptr);
}
ContextTarget::ContextTarget() = default;
ContextTarget::~ContextTarget() = default;
auto ContextTarget::GetHostSession() -> HostSession* { return nullptr; }
auto ContextTarget::GetAsHostActivity() -> HostActivity* { return nullptr; }
auto ContextTarget::GetAsUIContext() -> UI* { return nullptr; }
auto ContextTarget::GetMutableScene() -> Scene* { return nullptr; }
Context::Context() : target(g_context->target) { assert(InGameThread()); }
auto Context::operator==(const Context& other) const -> bool {
return (target.get() == other.target.get());
}
Context::Context(ContextTarget* target_in) : target(target_in) {}
auto Context::GetHostSession() const -> HostSession* {
assert(InGameThread());
if (target.exists()) return target->GetHostSession();
return nullptr;
}
auto Context::GetHostActivity() const -> HostActivity* {
ContextTarget* c = target.get();
HostActivity* a = c ? c->GetAsHostActivity() : nullptr;
assert(a == dynamic_cast<HostActivity*>(c)); // This should always match.
return a;
}
auto Context::GetMutableScene() const -> Scene* {
ContextTarget* c = target.get();
Scene* sg = c ? c->GetMutableScene() : nullptr;
return sg;
}
auto Context::GetUIContext() const -> UI* {
ContextTarget* c = target.get();
UI* uiContext = c ? c->GetAsUIContext() : nullptr;
assert(uiContext == dynamic_cast<UI*>(c));
return uiContext;
}
ScopedSetContext::ScopedSetContext(const Object::Ref<ContextTarget>& target) {
assert(InGameThread());
assert(g_context);
context_prev_ = *g_context;
g_context->target = target;
}
ScopedSetContext::ScopedSetContext(ContextTarget* target) {
assert(InGameThread());
assert(g_context);
context_prev_ = *g_context;
g_context->target = target;
}
ScopedSetContext::ScopedSetContext(const Context& context) {
assert(InGameThread());
assert(g_context);
context_prev_ = *g_context;
*g_context = context;
}
ScopedSetContext::~ScopedSetContext() {
assert(InGameThread());
assert(g_context);
// Restore old.
*g_context = context_prev_;
}
auto ContextTarget::NewTimer(TimeType timetype, TimerMedium length, bool repeat,
const Object::Ref<Runnable>& runnable) -> int {
// Make sure the passed runnable has a ref-count already
// (don't want them to rely on us to create initial one).
assert(runnable.exists());
assert(runnable->is_valid_refcounted_object());
switch (timetype) {
case TimeType::kSim:
throw Exception("Can't create 'sim' type timers in this context");
case TimeType::kBase:
throw Exception("Can't create 'base' type timers in this context");
case TimeType::kReal:
throw Exception("Can't create 'real' type timers in this context");
default:
throw Exception("Can't create that type timer in this context");
}
}
void ContextTarget::DeleteTimer(TimeType timetype, int timer_id) {
// We throw on NewTimer; lets just ignore anything that comes
// through here to avoid messing up destructors.
Log("ContextTarget::DeleteTimer() called; unexpected.");
}
auto ContextTarget::GetTime(TimeType timetype) -> millisecs_t {
throw Exception("Unsupported time type for this context");
}
auto ContextTarget::GetTexture(const std::string& name)
-> Object::Ref<Texture> {
throw Exception("GetTexture() not supported in this context");
}
auto ContextTarget::GetSound(const std::string& name) -> Object::Ref<Sound> {
throw Exception("GetSound() not supported in this context");
}
auto ContextTarget::GetData(const std::string& name) -> Object::Ref<Data> {
throw Exception("GetData() not supported in this context");
}
auto ContextTarget::GetModel(const std::string& name) -> Object::Ref<Model> {
throw Exception("GetModel() not supported in this context");
}
auto ContextTarget::GetCollideModel(const std::string& name)
-> Object::Ref<CollideModel> {
throw Exception("GetCollideModel() not supported in this context");
}
} // namespace ballistica

View File

@ -0,0 +1,128 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_CORE_CONTEXT_H_
#define BALLISTICA_CORE_CONTEXT_H_
#include <string>
#include "ballistica/core/object.h"
namespace ballistica {
// Stores important environmental state such as the recipient of commands.
// Callbacks and other mechanisms should save/restore the context so that their
// effects properly apply to the place they came from.
class Context {
public:
static void Init();
static auto current() -> const Context& {
assert(g_context);
// Context can only be accessed from the game thread.
BA_PRECONDITION(InGameThread());
return *g_context;
}
static void set_current(const Context& context) {
// Context can only be accessed from the game thread.
BA_PRECONDITION(InGameThread());
*g_context = context;
}
// Return the current context target, raising an Exception if there is none.
static auto current_target() -> ContextTarget& {
ContextTarget* t = current().target.get();
if (t == nullptr) {
throw Exception("No context target set.");
}
return *t;
}
// Default constructor will capture a copy of the current global context.
Context();
explicit Context(ContextTarget* sgc);
auto operator==(const Context& other) const -> bool;
Object::WeakRef<ContextTarget> target;
// If the current Context is (or is part of) a HostSession, return it;
// otherwise return nullptr. be aware that this will return a session if the
// context is *either* a host-activity or a host-session
auto GetHostSession() const -> HostSession*;
// return the current context as an HostActivity if it is one; otherwise
// nullptr (faster than a dynamic_cast)
auto GetHostActivity() const -> HostActivity*;
// if the current context contains a scene that can be manipulated by
// standard commands, this returns it. This includes host-sessions,
// host-activities, and the UI context.
auto GetMutableScene() const -> Scene*;
// return the current context as a UIContext if it is one; otherwise nullptr
// (faster than a dynamic_cst)
auto GetUIContext() const -> UI*;
};
// An interface for interaction with the engine; loading and wrangling media,
// nodes, etc.
// Note: it would seem like in an ideal world this could just be a pure
// virtual interface.
// However various things use WeakRef<ContextTarget> so technically they do
// all need to inherit from Object anyway.
class ContextTarget : public Object {
public:
ContextTarget();
~ContextTarget() override;
// returns the HostSession associated with this context, (if there is one).
virtual auto GetHostSession() -> HostSession*;
// Utility functions for casting; faster than dynamic_cast.
virtual auto GetAsHostActivity() -> HostActivity*;
virtual auto GetAsUIContext() -> UI*;
virtual auto GetMutableScene() -> Scene*;
// Timer create/destroy functions.
// Times are specified in milliseconds.
// Exceptions should be thrown for unsupported timetypes in NewTimer.
// Default NewTimer implementation throws a descriptive error, so it can
// be useful to fall back on for unsupported cases.
// NOTE: make sure runnables passed in here already have non-zero
// ref-counts since a ref might not be grabbed here.
virtual auto NewTimer(TimeType timetype, TimerMedium length, bool repeat,
const Object::Ref<Runnable>& runnable) -> int;
virtual void DeleteTimer(TimeType timetype, int timer_id);
virtual auto GetTexture(const std::string& name) -> Object::Ref<Texture>;
virtual auto GetSound(const std::string& name) -> Object::Ref<Sound>;
virtual auto GetData(const std::string& name) -> Object::Ref<Data>;
virtual auto GetModel(const std::string& name) -> Object::Ref<Model>;
virtual auto GetCollideModel(const std::string& name)
-> Object::Ref<CollideModel>;
// Return the current time of a given type in milliseconds.
// Exceptions should be thrown for unsupported timetypes.
// Default implementation throws a descriptive error so can be
// useful to fall back on for unsupported cases
virtual auto GetTime(TimeType timetype) -> millisecs_t;
};
// Use this to push/pop a change to the current context
class ScopedSetContext {
public:
explicit ScopedSetContext(const Object::Ref<ContextTarget>& context);
explicit ScopedSetContext(ContextTarget* context);
explicit ScopedSetContext(const Context& context);
~ScopedSetContext();
private:
BA_DISALLOW_CLASS_COPIES(ScopedSetContext);
Context context_prev_;
};
} // namespace ballistica
#endif // BALLISTICA_CORE_CONTEXT_H_

View File

@ -0,0 +1,83 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/core/exception.h"
#include <utility>
#include "ballistica/ballistica.h"
#include "ballistica/platform/platform.h"
namespace ballistica {
auto GetShortExceptionDescription(const std::exception& exc) -> const char* {
if (auto b_exc = dynamic_cast<const Exception*>(&exc)) {
return b_exc->message();
}
return exc.what();
}
Exception::Exception(std::string message_in, PyExcType python_type)
: message_(std::move(message_in)), python_type_(python_type) {
thread_name_ = GetCurrentThreadName();
// Attempt to capture a stack-trace here we can print out later if desired.
if (g_platform != nullptr) {
stack_trace_ = g_platform->GetStackTrace();
}
}
Exception::Exception(PyExcType python_type) : python_type_(python_type) {
thread_name_ = GetCurrentThreadName();
// Attempt to capture a stack-trace here we can print out later if desired.
if (g_platform != nullptr) {
stack_trace_ = g_platform->GetStackTrace();
}
}
// Copy constructor.
Exception::Exception(const Exception& other) noexcept {
try {
thread_name_ = other.thread_name_;
message_ = other.message_;
full_description_ = other.full_description_;
python_type_ = other.python_type_;
if (other.stack_trace_) {
stack_trace_ = other.stack_trace_->copy();
}
} catch (const std::exception&) {
// Hmmm not sure what we should do if this happens;
// for now we'll just wind up with some parts of our
// shiny new exception copy potentially missing.
// Better than crashing I suppose.
}
}
Exception::~Exception() { delete stack_trace_; }
auto Exception::what() const noexcept -> const char* {
// Return a nice pretty stack trace and other relevant info.
try {
// This call is const so we're technically not supposed to modify ourself,
// but a one-time flattening of our description into an internal buffer
// should be fine.
if (full_description_.empty()) {
if (stack_trace_ != nullptr) {
const_cast<Exception*>(this)->full_description_ =
message_ + "\nThrown from " + thread_name_ + " thread:\n"
+ stack_trace_->GetDescription();
} else {
const_cast<Exception*>(this)->full_description_ = message_;
}
}
return full_description_.c_str();
} catch (const std::exception&) {
// Welp; we tried.
return "Error generating ballistica::Exception::what(); oh dear.";
}
}
void Exception::SetPyError() const noexcept {
SetPythonException(python_type_, GetShortExceptionDescription(*this));
}
} // namespace ballistica

View File

@ -0,0 +1,82 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_CORE_EXCEPTION_H_
#define BALLISTICA_CORE_EXCEPTION_H_
#ifdef __cplusplus
#include <stdexcept>
#include <string>
#include "ballistica/core/types.h"
namespace ballistica {
// Notes on our C++ exception handling:
//
// std::exception in broken into two subclass categories, logic_error
// and runtime_error. It is my understanding that logic_error should be used
// as a sort of non-fatal assert() for things that the program is doing
// incorrectly, while runtime_error applies to external things such as user
// input (a user entering a name containing invalid characters, etc).
//
// In practice, we currently handle both sides identically, so the distinction
// is not really important to us. We also translate C++ exceptions to and
// from Python exceptions as their respective stacks unwind, so the distinction
// tends to get lost anyway.
//
// So for the time being we have a simple single ballistica::Exception type
// inheriting directly from std::exception that we use for pretty much anything
// going wrong. It contains useful tidbits such as a stack trace to help
// diagnose issues. We can expand on this or branch off into more particular
// types if/when the need arises.
//
// Note that any sites *catching* exception should catch std::exception
// (unless they have a particular need to catch a more specific type). This
// preserves our freedom to add variants under std::logic_error or
// std::runtime_error at a later time and also catches exceptions coming from
// std itself.
class PlatformStackTrace;
/// Get a short description for an exception.
/// By default, our Exception classes provide what() values that may include
/// backtraces of the throw location or other extended info that can be useful
/// to have printed in crash reports/etc. In some cases this extended info is
/// not desired, however, such as when converting a C++ exception to a Python
/// one (which will have its own backtrace and other context). This function
/// will return the raw message only if passed one of our Exceptions, and
/// simply what() in other cases.
auto GetShortExceptionDescription(const std::exception& exc) -> const char*;
class Exception : public std::exception {
public:
// NOTE: When adding exception types here, add a corresponding
// handler in Python::SetPythonException.
explicit Exception(std::string message = "",
PyExcType python_type = PyExcType::kRuntime);
explicit Exception(PyExcType python_type);
Exception(const Exception& other) noexcept;
~Exception() override;
/// Return the full description for this exception which may include
/// backtraces/etc.
auto what() const noexcept -> const char* override;
/// Return only the raw message passed to this exception on creation.
auto message() const noexcept -> const char* { return message_.c_str(); }
void SetPyError() const noexcept;
private:
std::string thread_name_;
std::string message_;
std::string full_description_;
PyExcType python_type_;
PlatformStackTrace* stack_trace_{};
};
} // namespace ballistica
#endif // __cplusplus
#endif // BALLISTICA_CORE_EXCEPTION_H_

View File

@ -0,0 +1,169 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/core/fatal_error.h"
#include "ballistica/app/app.h"
#include "ballistica/core/logging.h"
#include "ballistica/platform/platform.h"
namespace ballistica {
auto FatalError::ReportFatalError(const std::string& message,
bool in_top_level_exception_handler) -> void {
// We want to report the first fatal error that happens; if further ones
// happen they are probably red herrings.
static bool ran = false;
if (ran) {
return;
}
ran = true;
// Our main goal here varies based off whether we are an unmodified
// blessed build. If we are, our main goal is to communicate as much info
// about the error to the master server, and communicating to the user is
// a stretch goal.
// If we are unblessed or modified, the main goals are communicating the error
// to the user and exiting the app cleanly (so we don't pollute our crash
// records with results of user tinkering).
// Try to avoid crash reports if we're not a clean blessed build.
// bool exit_cleanly = !IsUnmodifiedBlessedBuild();
// printf("BLESSED %d\n", static_cast<int>(IsUnmodifiedBlessedBuild()));
// Give the platform the opportunity to completely override our handling.
if (g_platform) {
auto handled =
g_platform->ReportFatalError(message, in_top_level_exception_handler);
if (handled) {
return;
}
}
std::string dialog_msg = message;
if (!dialog_msg.empty()) {
dialog_msg += "\n";
}
// (No longer adding this note; individual errors to which the log is
// relevant can do to themselves).
// dialog_msg += "See BallisticaCore log for details.";
auto starttime = time(nullptr);
// Launch a thread and give it a chance to directly send our logs to the
// master-server. The standard mechanism probably won't get the job done
// since it relies on the game thread loop and we're likely blocking that.
// But generally we want to stay in this function and call abort() or whatnot
// from here so that our stack trace makes it into platform logs.
int result{};
std::string logmsg =
std::string("FATAL ERROR:") + (!message.empty() ? " " : "") + message;
// Try to include a stack trace if we're being called from outside of a
// top-level exception handler. Otherwise the trace isn't really useful
// since we know where those are anyway.
if (!in_top_level_exception_handler) {
if (g_platform) {
PlatformStackTrace* trace{g_platform->GetStackTrace()};
if (trace) {
std::string tracestr = trace->GetDescription();
if (!tracestr.empty()) {
logmsg += ("\nSTACK-TRACE-BEGIN:\n" + tracestr + "\nSTACK-TRACE-END");
}
delete trace;
}
}
}
// Prevent the early-log insta-send mechanism from firing since we do
// basically the same thing ourself here (avoid sending the same logs twice).
g_early_log_writes = 0;
Logging::Log(logmsg);
std::string prefix = "FATAL-ERROR-LOG:";
std::string suffix;
// If we have no globals yet, include this message explicitly
// since it won't be part of the standard log.
if (g_app_globals == nullptr) {
suffix = logmsg;
}
Logging::DirectSendLogs(prefix, suffix, true, &result);
// If we're able to show a fatal-error dialog synchronously, do so.
if (g_platform && g_platform->CanShowBlockingFatalErrorDialog()) {
DoBlockingFatalErrorDialog(dialog_msg);
}
// Wait until the log submit has finished or a bit of time has passed..
while (time(nullptr) - starttime < 10) {
if (result != 0) {
break;
}
Platform::SleepMS(100);
}
}
auto FatalError::DoBlockingFatalErrorDialog(const std::string& message)
-> void {
// If we're in the main thread; just fire off the dialog directly.
// Otherwise tell the main thread to do it and wait around until it's done.
if (InMainThread()) {
g_platform->BlockingFatalErrorDialog(message);
} else {
bool started{};
bool finished{};
bool* startedptr{&started};
bool* finishedptr{&finished};
g_app->PushCall([message, startedptr, finishedptr] {
*startedptr = true;
g_platform->BlockingFatalErrorDialog(message);
*finishedptr = true;
});
// Wait a short amount of time for the main thread to take action.
// There's a chance that it can't (if threads are paused, if it is
// blocked on a synchronous call to another thread, etc.) so if we don't
// see something happening soon, just give up on showing a dialog.
auto starttime = Platform::GetCurrentMilliseconds();
while (!started) {
if (Platform::GetCurrentMilliseconds() - starttime > 1000) {
return;
}
Platform::SleepMS(10);
}
while (!finished) {
Platform::SleepMS(10);
}
}
}
auto FatalError::HandleFatalError(bool exit_cleanly,
bool in_top_level_exception_handler) -> bool {
// Give the platform the opportunity to completely override our handling.
if (g_platform) {
auto handled = g_platform->HandleFatalError(exit_cleanly,
in_top_level_exception_handler);
if (handled) {
return true;
}
}
// If we're not being called as part of a top-level exception handler,
// bring the app down ourself.
if (!in_top_level_exception_handler) {
if (exit_cleanly) {
Log("Calling exit(1)...");
exit(1);
} else {
Log("Calling abort()...");
abort();
}
}
// Otherwise its up to who called us
// (they might let the caught exception bubble up)
return false;
}
} // namespace ballistica

View File

@ -0,0 +1,33 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_CORE_FATAL_ERROR_H_
#define BALLISTICA_CORE_FATAL_ERROR_H_
#include <string>
namespace ballistica {
class FatalError {
public:
/// Report a fatal error to the master-server/user/etc. Note that reporting
/// only happens for the first invocation of this call; additional calls
/// are no-ops.
static auto ReportFatalError(const std::string& message,
bool in_top_level_exception_handler) -> void;
/// Handle a fatal error. This can involve calling exit(), abort(), setting
/// up an asynchronous quit, etc. Returns true if the fatal-error has been
/// handled; otherwise it is up to the caller (this should only be the case
/// when in_top_level_exception_handler is true).
/// Unlike ReportFatalError, the logic in this call can be invoked repeatedly
/// and should be prepared for that possibility in the case of recursive
/// fatal errors/etc.
static auto HandleFatalError(bool clean_exit,
bool in_top_level_exception_handler) -> bool;
private:
static auto DoBlockingFatalErrorDialog(const std::string& message) -> void;
};
} // namespace ballistica
#endif // BALLISTICA_CORE_FATAL_ERROR_H_

View File

@ -0,0 +1,11 @@
// Copyright (c) 2011-2020 Eric Froemling
#if 0 // Satisfy both CppLint and CLang..
#include "ballistica/core/inline.h"
#endif
namespace ballistica {
auto InlineDebugExplicitBool(bool val) -> bool { return val; }
} // namespace ballistica

View File

@ -0,0 +1,143 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_CORE_INLINE_H_
#define BALLISTICA_CORE_INLINE_H_
#ifdef __cplusplus
#include <cassert>
#include <string>
#include <type_traits>
// Bits of functionality that are useful enough to include fully as
// inlines/templates in our top level namespace.
namespace ballistica {
// Support functions we declare in our .cc file; not for public use.
// auto InlineDebugExplicitBool(bool val) -> bool;
/// Return the same bool value passed in, but obfuscated enough in debug mode
/// that no 'value is always true/false', 'code will never run', type warnings
/// should appear. In release builds it should optimize away to a no-op.
inline auto explicit_bool(bool val) -> bool {
#if BA_DEBUG_BUILD
return InlineDebugExplicitBool(val);
#else
return val;
#endif
}
/// Simply a static_cast, but in debug builds casts the results back to ensure
/// the value fits into the receiver unchanged. Handy as a sanity check when
/// stuffing a 32 bit value into a 16 bit container, etc.
template <typename OUT_TYPE, typename IN_TYPE>
auto static_cast_check_fit(IN_TYPE in) -> OUT_TYPE {
// Make sure we don't try to use this when casting to or from floats or
// doubles. We don't expect to always get the same value back
// on casting back in that case.
static_assert(!std::is_same<IN_TYPE, float>::value
&& !std::is_same<IN_TYPE, double>::value
&& !std::is_same<IN_TYPE, const float>::value
&& !std::is_same<IN_TYPE, const double>::value
&& !std::is_same<OUT_TYPE, float>::value
&& !std::is_same<OUT_TYPE, double>::value
&& !std::is_same<OUT_TYPE, const float>::value
&& !std::is_same<OUT_TYPE, const double>::value,
"static_cast_check_fit cannot be used with floats or doubles.");
#if BA_DEBUG_BUILD
assert(static_cast<IN_TYPE>(static_cast<OUT_TYPE>(in)) == in);
#endif
return static_cast<OUT_TYPE>(in);
}
/// Like static_cast_check_fit, but runs checks even in release builds and
/// throws an Exception on failure.
template <typename OUT_TYPE, typename IN_TYPE>
auto static_cast_check_fit_always(IN_TYPE in) -> OUT_TYPE {
// Make sure we don't try to use this when casting to or from floats or
// doubles. We don't expect to always get the same value back
// on casting back in that case.
static_assert(
!std::is_same<IN_TYPE, float>::value
&& !std::is_same<IN_TYPE, double>::value
&& !std::is_same<IN_TYPE, const float>::value
&& !std::is_same<IN_TYPE, const double>::value
&& !std::is_same<OUT_TYPE, float>::value
&& !std::is_same<OUT_TYPE, double>::value
&& !std::is_same<OUT_TYPE, const float>::value
&& !std::is_same<OUT_TYPE, const double>::value,
"static_cast_always_checked cannot be used with floats or doubles.");
auto out = static_cast<OUT_TYPE>(in);
if (static_cast<IN_TYPE>(out) != in) {
throw Exception("static_cast_check_fit_always failed for value "
+ std::to_string(in) + ".");
}
return static_cast<OUT_TYPE>(in);
}
/// Simply a static_cast, but in debug builds also runs a dynamic cast to
/// ensure the results would have been the same. Handy for keeping casts
/// lightweight when types are known while still having a sanity check.
template <typename OUT_TYPE, typename IN_TYPE>
auto static_cast_check_type(IN_TYPE in) -> OUT_TYPE {
auto out_static = static_cast<OUT_TYPE>(in);
#if BA_DEBUG_BUILD
auto out_dynamic = dynamic_cast<OUT_TYPE>(in);
assert(out_static == out_dynamic);
#endif
return out_static;
}
// This call hijacks compile-type pretty-function-printing functionality
// to give human-readable strings for arbitrary types. Note that these
// will not be consistent across platforms and should only be used for
// logging/debugging. Also note that this code is dependent on very specific
// compiler output which could change at any time; to watch out for this
// it is recommended to add static_assert()s somewhere to ensure that
// output for a few given types matches expected result(s).
template <typename T>
constexpr auto static_type_name_constexpr(bool debug_full = false)
-> std::string_view {
std::string_view name, prefix, suffix;
#ifdef __clang__
name = __PRETTY_FUNCTION__;
prefix =
"std::string_view ballistica::"
"static_type_name_constexpr(bool) [T = ";
suffix = "]";
#elif defined(__GNUC__)
name = __PRETTY_FUNCTION__;
prefix =
"constexpr std::string_view "
"ballistica::static_type_name_constexpr(bool) "
"[with T = ";
suffix = "; std::string_view = std::basic_string_view<char>]";
#elif defined(_MSC_VER)
name = __FUNCSIG__;
prefix =
"class std::basic_string_view<char,struct std::char_traits<char> > "
"__cdecl ballistica::static_type_name_constexpr<";
suffix = ">(bool)";
#else
#error unimplemented
#endif
if (debug_full) {
return name;
}
name.remove_prefix(prefix.size());
name.remove_suffix(suffix.size());
return name;
}
/// Return a human-readable string for the template type.
template <typename T>
static auto static_type_name(bool debug_full = false) -> std::string {
return std::string(static_type_name_constexpr<T>(debug_full));
}
} // namespace ballistica
#endif // __cplusplus
#endif // BALLISTICA_CORE_INLINE_H_

View File

@ -0,0 +1,214 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/core/logging.h"
#include <map>
#include "ballistica/app/app_globals.h"
#include "ballistica/ballistica.h"
#include "ballistica/game/game.h"
#include "ballistica/networking/networking.h"
#include "ballistica/networking/telnet_server.h"
#include "ballistica/platform/platform.h"
#include "ballistica/python/python.h"
namespace ballistica {
static void PrintCommon(const std::string& s) {
// Print to in-game console.
{
if (g_game != nullptr) {
g_game->PushConsolePrintCall(s);
} else {
if (g_platform != nullptr) {
g_platform->HandleLog(
"Warning: Log() called before game-thread setup; "
"will not appear on in-game console.\n");
}
}
}
// Print to any telnet clients.
if (g_app_globals && g_app_globals->telnet_server) {
g_app_globals->telnet_server->PushPrint(s);
}
}
void Logging::PrintStdout(const std::string& s, bool flush) {
fprintf(stdout, "%s", s.c_str());
if (flush) {
fflush(stdout);
}
PrintCommon(s);
}
void Logging::PrintStderr(const std::string& s, bool flush) {
fprintf(stderr, "%s", s.c_str());
if (flush) {
fflush(stderr);
}
PrintCommon(s);
}
void Logging::Log(const std::string& msg, bool to_stdout, bool to_server) {
if (to_stdout) {
PrintStdout(msg + "\n", true);
}
// Ship to the platform logging mechanism (android-log, stderr, etc.)
// if that's available yet.
if (g_platform != nullptr) {
g_platform->HandleLog(msg);
}
// Ship to master-server/etc.
if (to_server) {
// Route through platform-specific loggers if present.
// (things like Crashlytics crash-logging)
if (g_platform) {
Platform::DebugLog(msg);
}
// Add to our complete log.
if (g_app_globals != nullptr) {
std::lock_guard<std::mutex> lock(g_app_globals->log_mutex);
if (!g_app_globals->log_full) {
(g_app_globals->log) += (msg + "\n");
if ((g_app_globals->log).size() > 10000) {
// Allow some reasonable overflow for last statement.
if ((g_app_globals->log).size() > 100000) {
// FIXME: This could potentially chop up utf-8 chars.
(g_app_globals->log).resize(100000);
}
g_app_globals->log += "\n<max log size reached>\n";
g_app_globals->log_full = true;
}
}
}
// If the game is fully bootstrapped, let the Python layer handle logs.
// It will group log messages intelligently and ship them to the
// master server with various other context info included.
if (g_app_globals && g_app_globals->is_bootstrapped) {
assert(g_python != nullptr);
g_python->PushObjCall(Python::ObjID::kHandleLogCall);
} else {
// For log messages during bootstrapping we ship them immediately since
// we don't know if the Python layer is (or will be) able to.
if (g_early_log_writes > 0) {
g_early_log_writes -= 1;
std::string logprefix = "EARLY-LOG:";
std::string logsuffix;
// If we're an early enough error, our global log isn't even available,
// so include this specific message as a suffix instead.
if (g_app_globals == nullptr) {
logsuffix = msg;
}
DirectSendLogs(logprefix, logsuffix, false);
}
}
}
}
auto Logging::DirectSendLogs(const std::string& prefix,
const std::string& suffix, bool instant,
int* result) -> void {
// Use a rough mechanism to restrict log uploads to 1 send per second.
static time_t last_non_instant_send_time{-1};
if (!instant) {
auto curtime = Platform::GetCurrentSeconds();
if (curtime == last_non_instant_send_time) {
return;
}
last_non_instant_send_time = curtime;
}
std::thread t([prefix, suffix, instant, result]() {
// For non-instant sends, sleep for 2 seconds before sending logs;
// this should capture the just-added log as well as any more that
// got added in the subsequent second when we were not launching new
// send threads.
if (!instant) {
Platform::SleepMS(2000);
}
std::string log;
// Send our blessing hash only after we've calculated it; don't use our
// internal one. This means that we'll get false-negatives on whether
// direct-sent logs are blessed, but I think that's better than false
// positives.
std::string calced_blessing_hash;
if (g_app_globals) {
std::lock_guard<std::mutex> lock(g_app_globals->log_mutex);
log = g_app_globals->log;
calced_blessing_hash = g_app_globals->calced_blessing_hash;
} else {
log = "(g_app_globals not yet inited; no global log available)";
}
if (!prefix.empty()) {
log = prefix + "\n" + log;
}
if (!suffix.empty()) {
log = log + "\n" + suffix;
}
// Also send our blessing-calculation state; we may want to distinguish
// between blessing not being calced yet and being confirmed as un-blessed.
// FIXME: should probably do this in python layer log submits too.
std::string bless_calc_state;
if (kBlessingHash == nullptr) {
bless_calc_state = "nointhash";
} else if (g_app_globals == nullptr) {
bless_calc_state = "noglobs";
} else if (g_app_globals->calced_blessing_hash.empty()) {
// Mention we're calculating, but also mention if it is likely that
// the user is mucking with stuff.
if (g_app_globals->user_ran_commands
|| g_platform->using_custom_app_python_dir()) {
bless_calc_state = "calcing_likely_modded";
} else {
bless_calc_state = "calcing_not_modded";
}
} else {
bless_calc_state = "done";
}
std::string path{"/bsLog"};
std::map<std::string, std::string> params{
{"log", log},
{"time", "-1"},
{"userAgentString", g_app_globals ? g_app_globals->user_agent_string
: "(no g_app_globals)"},
{"newsShow", calced_blessing_hash.c_str()},
{"bcs", bless_calc_state.c_str()},
{"build", std::to_string(kAppBuildNumber)}};
try {
Networking::MasterServerPost(path, params);
if (result) {
*result = 1; // SUCCESS!
}
} catch (const std::exception&) {
// Try our fallback master-server address if that didn't work.
try {
params["log"] = prefix + "(FALLBACK-ADDR):\n" + log;
Networking::MasterServerPost(path, params, true);
if (result) {
*result = 1; // SUCCESS!
}
} catch (const std::exception& exc) {
// Well, we tried; make a note to platform log if available
// that we failed.
if (g_platform != nullptr) {
g_platform->HandleLog(std::string("Early log-to-server failed: ")
+ exc.what());
}
if (result) {
*result = -1; // FAIL!!
}
}
}
});
t.detach();
}
} // namespace ballistica

View File

@ -0,0 +1,35 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_CORE_LOGGING_H_
#define BALLISTICA_CORE_LOGGING_H_
#include <string>
namespace ballistica {
class Logging {
public:
/// Print a string directly to stdout as well as the in-game console
/// and any connected telnet consoles.
static auto PrintStdout(const std::string& s, bool flush = false) -> void;
/// Print a string directly to stderr as well as the in-game console
/// and any connected telnet consoles.
static auto PrintStderr(const std::string& s, bool flush = false) -> void;
/// Write a string to the debug log.
/// This will go to stdout, windows debug log, android log, etc. depending
/// on the platform.
static auto Log(const std::string& msg, bool to_stdout = true,
bool to_server = true) -> void;
/// Ship logs to the master-server in a bg thread. If result is passed,
/// it will be set to 1 on success and -1 on error.
static auto DirectSendLogs(const std::string& prefix,
const std::string& suffix, bool instant,
int* result = nullptr) -> void;
};
} // namespace ballistica
#endif // BALLISTICA_CORE_LOGGING_H_

View File

@ -0,0 +1,106 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/core/macros.h"
#include "ballistica/platform/platform.h"
#include "ballistica/python/python.h"
// Snippets of compiled functionality used by our evil macros.
namespace ballistica {
void MacroFunctionTimerEnd(millisecs_t starttime, millisecs_t time,
const char* funcname) {
// Currently disabling this for test builds; not really useful for
// the general public.
if (g_buildconfig.test_build()) {
return;
}
millisecs_t endtime = g_platform->GetTicks();
if (endtime - starttime > time) {
Log("Warning: " + std::to_string(endtime - starttime)
+ " milliseconds spent in " + funcname);
}
}
void MacroFunctionTimerEndThread(millisecs_t starttime, millisecs_t time,
const char* funcname) {
// Currently disabling this for test builds; not really useful for
// the general public.
if (g_buildconfig.test_build()) {
return;
}
millisecs_t endtime = g_platform->GetTicks();
if (endtime - starttime > time) {
Log("Warning: " + std::to_string(endtime - starttime)
+ " milliseconds spent by " + ballistica::GetCurrentThreadName()
+ " thread in " + funcname);
}
}
void MacroFunctionTimerEndEx(millisecs_t starttime, millisecs_t time,
const char* funcname, const std::string& what) {
// Currently disabling this for test builds; not really useful for
// the general public.
if (g_buildconfig.test_build()) {
return;
}
millisecs_t endtime = g_platform->GetTicks();
if (endtime - starttime > time) {
Log("Warning: " + std::to_string(endtime - starttime)
+ " milliseconds spent in " + funcname + " for " + what);
}
}
void MacroFunctionTimerEndThreadEx(millisecs_t starttime, millisecs_t time,
const char* funcname,
const std::string& what) {
// Currently disabling this for test builds; not really useful for
// the general public.
if (g_buildconfig.test_build()) {
return;
}
millisecs_t endtime = g_platform->GetTicks();
if (endtime - starttime > time) {
Log("Warning: " + std::to_string(endtime - starttime)
+ " milliseconds spent by " + ballistica::GetCurrentThreadName()
+ " thread in " + funcname + " for " + what);
}
}
void MacroTimeCheckEnd(millisecs_t starttime, millisecs_t time,
const char* name, const char* file, int line) {
// Currently disabling this for test builds; not really useful for
// the general public.
if (g_buildconfig.test_build()) {
return;
}
millisecs_t e = g_platform->GetTicks();
if (e - starttime > time) {
Log(std::string("Warning: ") + name + " took "
+ std::to_string(e - starttime) + " milliseconds; " + file + " line "
+ std::to_string(line));
}
}
void MacroLogErrorTrace(const std::string& msg, const char* fname, int line) {
char buffer[2048];
snprintf(buffer, sizeof(buffer), "%s:%d:", fname, line);
buffer[sizeof(buffer) - 1] = 0;
Python::PrintStackTrace();
Log(std::string(buffer) + " error: " + msg);
}
void MacroLogError(const std::string& msg, const char* fname, int line) {
char e_buffer[2048];
snprintf(e_buffer, sizeof(e_buffer), "%s:%d:", fname, line);
e_buffer[sizeof(e_buffer) - 1] = 0;
ballistica::Log(std::string(e_buffer) + " error: " + msg);
}
void MacroLogPythonTrace(const std::string& msg) {
Python::PrintStackTrace();
Log(msg);
}
} // namespace ballistica

View File

@ -0,0 +1,166 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_CORE_MACROS_H_
#define BALLISTICA_CORE_MACROS_H_
#ifdef __cplusplus
#include <cassert>
#include <string>
#endif
#include "ballistica/core/types.h"
// Various utility macros and related support calls.
// Trying to contain the evil in this one place.
// Trailing-semicolon note:
// Some macros contain a ((void*) at the end. This is so the macro can be
// followed by a semicolon without triggering an 'empty statement' warning.
// I find standalone function-style macro invocations without semicolons
// tends to confuse code formatters.
#define BA_STRINGIFY(x) #x
#define BA_BUILD_COMMAND_FILENAME \
"<string: " __FILE__ " line " BA_STRINGIFY(__LINE__) ">"
#define BA_BCFN BA_BUILD_COMMAND_FILENAME
#if BA_OSTYPE_WINDOWS
#define BA_DIRSLASH "\\"
#else
#define BA_DIRSLASH "/"
#endif
#if BA_DEBUG_BUILD
#define BA_IFDEBUG(a) a
#else
#define BA_IFDEBUG(a) ((void)0)
#endif
// Useful for finding hitches.
// Call begin, followed at some point by any of the end versions.
// FIXME: Turn these into C++ classes.
#if BA_DEBUG_BUILD
#define BA_DEBUG_FUNCTION_TIMER_BEGIN() \
millisecs_t _dfts = g_platform->GetTicks()
#define BA_DEBUG_FUNCTION_TIMER_END(time) \
ballistica::MacroFunctionTimerEnd(_dfts, time, __PRETTY_FUNCTION__)
#define BA_DEBUG_FUNCTION_TIMER_END_THREAD(time) \
ballistica::MacroFunctionTimerEndThread(_dfts, time, __PRETTY_FUNCTION__)
#define BA_DEBUG_FUNCTION_TIMER_END_EX(time, what) \
MacroFunctionTimerEndEx(_dfts, time, __PRETTY_FUNCTION__, what)
#define BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(time, what) \
ballistica::MacroFunctionTimerEndThreadEx(_dfts, time, __PRETTY_FUNCTION__, \
what)
#define BA_DEBUG_TIME_CHECK_BEGIN(name) \
millisecs_t name##_ts = g_platform->GetTicks()
#define BA_DEBUG_TIME_CHECK_END(name, time) \
ballistica::MacroTimeCheckEnd(name##_ts, time, #name, __FILE__, __LINE__)
#else
#define BA_DEBUG_FUNCTION_TIMER_BEGIN() ((void)0)
#define BA_DEBUG_FUNCTION_TIMER_END(time) ((void)0)
#define BA_DEBUG_FUNCTION_TIMER_END_THREAD(time) ((void)0)
#define BA_DEBUG_FUNCTION_TIMER_END_EX(time, what) ((void)0)
#define BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(time, what) ((void)0)
#define BA_DEBUG_TIME_CHECK_BEGIN(name) ((void)0)
#define BA_DEBUG_TIME_CHECK_END(name, time) ((void)0)
#endif
// Disallow copying for a class.
#define BA_DISALLOW_CLASS_COPIES(type) \
type(const type& foo) = delete; \
type& operator=(const type& src) = delete; /* NOLINT (macro parens) */
// Call this for errors which are non-fatal but should be noted so they can be
// fixed.
#define BA_LOG_ERROR_TRACE(msg) \
ballistica::MacroLogErrorTrace(msg, __FILE__, __LINE__)
#define BA_LOG_ERROR_TRACE_ONCE(msg) \
{ \
static bool did_log_error_trace_here = false; \
if (!did_log_error_trace_here) { \
ballistica::MacroLogErrorTrace(msg, __FILE__, __LINE__); \
did_log_error_trace_here = true; \
} \
} \
((void)0) // (see 'Trailing-semicolon note' at top)
#define BA_LOG_ONCE(msg) \
{ \
static bool did_log_here = false; \
if (!did_log_here) { \
ballistica::Log(msg); \
did_log_here = true; \
} \
} \
((void)0) // (see 'Trailing-semicolon note' at top)
#define BA_LOG_PYTHON_TRACE(msg) ballistica::MacroLogPythonTrace(msg)
#define BA_LOG_PYTHON_TRACE_ONCE(msg) \
{ \
static bool did_log_python_trace_here = false; \
if (!did_log_python_trace_here) { \
ballistica::MacroLogPythonTrace(msg); \
did_log_python_trace_here = true; \
} \
} \
((void)0) // (see 'Trailing-semicolon note' at top)
/// Test a condition and throw an exception if it fails (on both debug and
/// release builds)
#define BA_PRECONDITION(b) \
{ \
if (!(b)) { \
throw ballistica::Exception("Precondition failed: " #b); \
} \
} \
((void)0) // (see 'Trailing-semicolon note' at top)
/// Test a condition and simply print a log message if it fails (on both debug
/// and release builds)
#define BA_PRECONDITION_LOG(b) \
{ \
if (!(b)) { \
Log("Precondition failed: " #b); \
} \
} \
((void)0) // (see 'Trailing-semicolon note' at top)
/// Test a condition and abort the program if it fails (on both debug
/// and release builds)
#define BA_PRECONDITION_FATAL(b) \
{ \
if (!(b)) { \
FatalError("Precondition failed: " #b); \
} \
} \
((void)0) // (see 'Trailing-semicolon note' at top)
#ifdef __cplusplus
namespace ballistica {
// Support functions used by some of our macros; not intended to be used
// directly.
void MacroFunctionTimerEnd(millisecs_t starttime, millisecs_t time,
const char* funcname);
void MacroFunctionTimerEndThread(millisecs_t starttime, millisecs_t time,
const char* funcname);
void MacroFunctionTimerEndEx(millisecs_t starttime, millisecs_t time,
const char* funcname, const std::string& what);
void MacroFunctionTimerEndThreadEx(millisecs_t starttime, millisecs_t time,
const char* funcname,
const std::string& what);
void MacroTimeCheckEnd(millisecs_t starttime, millisecs_t time,
const char* name, const char* file, int line);
void MacroLogErrorTrace(const std::string& msg, const char* fname, int line);
void MacroLogError(const std::string& msg, const char* fname, int line);
void MacroLogPythonTrace(const std::string& msg);
} // namespace ballistica
#endif // __cplusplus
#endif // BALLISTICA_CORE_MACROS_H_

View File

@ -0,0 +1,50 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/core/module.h"
#include <utility>
#include "ballistica/core/thread.h"
namespace ballistica {
void Module::PushLocalRunnable(Runnable* runnable) {
assert(std::this_thread::get_id() == thread()->thread_id());
runnables_.push_back(runnable);
}
void Module::PushRunnable(Runnable* runnable) {
// If we're being called from the module's thread, just drop it in the list.
// otherwise send it as a message to the other thread.
if (std::this_thread::get_id() == thread()->thread_id()) {
PushLocalRunnable(runnable);
} else {
thread_->PushModuleRunnable(runnable, id_);
}
}
Module::Module(std::string name_in, Thread* thread_in)
: thread_(thread_in), name_(std::move(name_in)) {
id_ = thread_->RegisterModule(name_, this);
}
Module::~Module() = default;
auto Module::NewThreadTimer(millisecs_t length, bool repeat,
const Object::Ref<Runnable>& runnable) -> Timer* {
return thread_->NewTimer(length, repeat, runnable);
}
void Module::RunPendingRunnables() {
// Pull all runnables off the list first (its possible for one of these
// runnables to add more) and then process them.
assert(std::this_thread::get_id() == thread()->thread_id());
std::list<Runnable*> runnables;
runnables_.swap(runnables);
for (Runnable* i : runnables) {
i->Run();
delete i;
}
}
} // namespace ballistica

View File

@ -0,0 +1,70 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_CORE_MODULE_H_
#define BALLISTICA_CORE_MODULE_H_
#include <list>
#include <mutex>
#include <string>
#include <vector>
#include "ballistica/generic/lambda_runnable.h"
#include "ballistica/generic/runnable.h"
namespace ballistica {
/// A logical entity that can be added to a thread and make use of its
/// event loop.
class Module {
public:
/// Add a runnable to this module's queue.
/// Pass a Runnable that has been allocated with new().
/// There must be no existing strong refs to it.
/// It will be owned and disposed of by the module from this point.
void PushRunnable(Runnable* runnable);
/// Convenience function to push a lambda as a runnable.
template <typename F>
void PushCall(const F& lambda) {
PushRunnable(NewLambdaRunnableRaw(lambda));
}
/// Return the thread this module is running on.
auto thread() const -> Thread* { return thread_; }
virtual ~Module();
/// Push a runnable from the same thread as the module.
void PushLocalRunnable(Runnable* runnable);
/// Called for each module when its thread is about to be suspended
/// (on platforms such as mobile).
virtual void HandleThreadPause() {}
/// Called for each module when its thread is about to be resumed
/// (on platforms such as mobile).
virtual void HandleThreadResume() {}
/// Whether this module has pending runnables.
auto has_pending_runnables() const -> bool { return !runnables_.empty(); }
/// Used by the module's owner thread to let it do its thing.
void RunPendingRunnables();
auto name() const -> const std::string& { return name_; }
protected:
Module(std::string name, Thread* thread);
auto NewThreadTimer(millisecs_t length, bool repeat,
const Object::Ref<Runnable>& runnable) -> Timer*;
private:
std::string name_;
int id_{};
std::list<Runnable*> runnables_;
Thread* thread_{};
};
} // namespace ballistica
#endif // BALLISTICA_CORE_MODULE_H_

View File

@ -0,0 +1,233 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/core/object.h"
#include <algorithm>
#include <map>
#include <mutex>
#include <string>
#include <utility>
#include "ballistica/app/app_globals.h"
#include "ballistica/generic/utils.h"
#include "ballistica/platform/min_sdl.h"
#include "ballistica/platform/platform.h"
namespace ballistica {
void Object::PrintObjects() {
#if BA_DEBUG_BUILD
std::string s;
{
std::lock_guard<std::mutex> lock(g_app_globals->object_list_mutex);
s = std::to_string(g_app_globals->object_count) + " Objects at time "
+ std::to_string(GetRealTime()) + ";";
if (explicit_bool(true)) {
std::map<std::string, int> obj_map;
// Tally up counts for all types.
int count = 0;
for (Object* o = g_app_globals->object_list_first; o != nullptr;
o = o->object_next_) {
count++;
std::string obj_name = o->GetObjectTypeName();
auto i = obj_map.find(obj_name);
if (i == obj_map.end()) {
obj_map[obj_name] = 1;
} else {
// Getting complaints that 'second' is unused, but we sort and print
// using this value like 10 lines down. Hmmm.
#pragma clang diagnostic push
#pragma ide diagnostic ignored "UnusedValue"
i->second++;
#pragma clang diagnostic pop
}
}
// Now sort them by count and print.
std::vector<std::pair<int, std::string> > sorted;
sorted.reserve(obj_map.size());
for (auto&& i : obj_map) {
sorted.emplace_back(i.second, i.first);
}
std::sort(sorted.begin(), sorted.end());
for (auto&& i : sorted) {
s += "\n " + std::to_string(i.first) + ": " + i.second;
}
assert(count == g_app_globals->object_count);
}
}
Log(s);
#else
Log("PrintObjects() only functions in debug builds.");
#endif // BA_DEBUG_BUILD
}
Object::Object() {
#if BA_DEBUG_BUILD
// Mark when we were born.
object_birth_time_ = GetRealTime();
// Add ourself to the global object list.
std::lock_guard<std::mutex> lock(g_app_globals->object_list_mutex);
object_prev_ = nullptr;
object_next_ = g_app_globals->object_list_first;
g_app_globals->object_list_first = this;
if (object_next_) {
object_next_->object_prev_ = this;
}
g_app_globals->object_count++;
#endif // BA_DEBUG_BUILD
}
Object::~Object() {
#if BA_DEBUG_BUILD
// Pull ourself from the global obj list.
std::lock_guard<std::mutex> lock(g_app_globals->object_list_mutex);
if (object_next_) {
object_next_->object_prev_ = object_prev_;
}
if (object_prev_) {
object_prev_->object_next_ = object_next_;
} else {
g_app_globals->object_list_first = object_next_;
}
g_app_globals->object_count--;
// More sanity checks.
if (object_strong_ref_count_ != 0) {
// Avoiding Log for these low level errors; can lead to deadlock.
printf(
"Warning: Object is dying with non-zero ref-count; this is bad. "
"(this "
"might mean the object raised an exception in its constructor after "
"being strong-referenced first).\n");
}
#endif // BA_DEBUG_BUILD
// Invalidate all our weak refs.
// We could call Release() on each but we'd have to deactivate the
// thread-check since virtual functions won't work right in a destructor.
// Also we can take a few shortcuts here since we know we're deleting the
// entire list, not just one object.
while (object_weak_refs_) {
auto tmp = object_weak_refs_;
object_weak_refs_ = tmp->next_;
tmp->prev_ = nullptr;
tmp->next_ = nullptr;
tmp->obj_ = nullptr;
}
}
auto Object::GetObjectTypeName() const -> std::string {
// Default implementation just returns type name.
return g_platform->DemangleCXXSymbol(typeid(*this).name());
}
auto Object::GetObjectDescription() const -> std::string {
return "<" + GetObjectTypeName() + " object at " + Utils::PtrToString(this)
+ ">";
}
auto Object::GetThreadOwnership() const -> Object::ThreadOwnership {
#if BA_DEBUG_BUILD
return thread_ownership_;
#else
// Not used in release build so doesn't matter.
return ThreadOwnership::kAny;
#endif
}
auto Object::GetDefaultOwnerThread() const -> ThreadIdentifier {
return ThreadIdentifier::kGame;
}
#if BA_DEBUG_BUILD
static auto GetCurrentThreadIdentifier() -> ThreadIdentifier {
if (InMainThread()) {
return ThreadIdentifier::kMain;
} else if (InGameThread()) {
return ThreadIdentifier::kGame;
} else if (InAudioThread()) {
return ThreadIdentifier::kAudio;
} else if (InNetworkWriteThread()) {
return ThreadIdentifier::kNetworkWrite;
} else if (InMediaThread()) {
return ThreadIdentifier::kMedia;
} else if (InBGDynamicsThread()) {
return ThreadIdentifier::kBGDynamics;
} else {
throw Exception(std::string("unrecognized thread: ")
+ GetCurrentThreadName());
}
}
void Object::ObjectThreadCheck() {
if (!thread_checks_enabled_) {
return;
}
ThreadOwnership thread_ownership = GetThreadOwnership();
if (thread_ownership == ThreadOwnership::kAny) {
return;
}
// If we're set to use the next-referencing thread
// and haven't set that yet, do so.
if (thread_ownership == ThreadOwnership::kNextReferencing
&& owner_thread_ == ThreadIdentifier::kInvalid) {
owner_thread_ = GetCurrentThreadIdentifier();
}
ThreadIdentifier t;
if (thread_ownership == ThreadOwnership::kClassDefault) {
t = GetDefaultOwnerThread();
} else {
t = owner_thread_;
}
#define DO_FAIL(THREADNAME) \
throw Exception("ObjectThreadCheck failed for " + GetObjectDescription() \
+ "; expected " THREADNAME " thread; got " \
+ GetCurrentThreadName())
switch (t) {
case ThreadIdentifier::kMain:
if (!InMainThread()) {
DO_FAIL("Main");
}
break;
case ThreadIdentifier::kGame:
if (!InGameThread()) {
DO_FAIL("Game");
}
break;
case ThreadIdentifier::kAudio:
if (!InAudioThread()) {
DO_FAIL("Audio");
}
break;
case ThreadIdentifier::kNetworkWrite:
if (!InNetworkWriteThread()) {
DO_FAIL("NetworkWrite");
}
break;
case ThreadIdentifier::kMedia:
if (!InMediaThread()) {
DO_FAIL("Media");
}
break;
case ThreadIdentifier::kBGDynamics:
if (!InBGDynamicsThread()) {
DO_FAIL("BGDynamics");
}
break;
default:
throw Exception();
}
#undef DO_FAIL
}
#endif // BA_DEBUG_BUILD
} // namespace ballistica

View File

@ -0,0 +1,671 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_CORE_OBJECT_H_
#define BALLISTICA_CORE_OBJECT_H_
#include <string>
#include <utility>
#include <vector>
#include "ballistica/ballistica.h"
namespace ballistica {
/// Objects supporting strong and weak referencing and thread enforcement.
/// A rule or two for for Objects:
/// Don't throw exceptions out of object destructors;
/// This will break references to that object and lead to crashes if/when they
/// are used.
class Object {
public:
Object();
virtual ~Object();
/// Prints a tally of object types and counts (debug build only).
static void PrintObjects();
// Object classes can provide descriptive names for themselves;
// these are used for debugging and other purposes.
// The default is to use the C++ symbol name, demangling it when possible.
// IMPORTANT: Do not rely on this being consistent across builds/platforms.
virtual auto GetObjectTypeName() const -> std::string;
// Provide a brief description of this particular object; by default returns
// type-name plus address.
virtual auto GetObjectDescription() const -> std::string;
// This is called when adding or removing a reference to an Object;
// it can perform sanity-tests to make sure references are not being
// added at incorrect times or from incorrect threads.
// The default implementation uses the per-object
// ThreadOwnership/ThreadIdentifier values accessible below. NOTE: this
// check runs only in the debug build so don't add any logical side-effects!
#if BA_DEBUG_BUILD
virtual void ObjectThreadCheck();
#endif
enum class ThreadOwnership {
kClassDefault, // Uses class' GetDefaultOwnerThread() call.
kNextReferencing, // Uses whichever thread next acquires/accesses a ref.
kCustom, // Always use a specific thread.
kAny // Any thread is fine.
};
/// Called by the default ObjectThreadCheck() to determine ThreadOwnership
/// for an Object. The default uses the object's individual value
/// (which defaults to ThreadOwnership::kClassDefault and can be set via
/// SetThreadOwnership())
virtual auto GetThreadOwnership() const -> ThreadOwnership;
/// Return the exact thread to check for with ThreadOwnership::kClassDefault
/// (in the default ObjectThreadCheck implementation at least).
/// Default returns ThreadIdentifier::kGame
virtual auto GetDefaultOwnerThread() const -> ThreadIdentifier;
/// Set thread ownership values for an individual object.
/// Note that these values may be ignored if ObjectThreadCheck() is
/// overridden, and thread_identifier is only relevant when ownership is
/// ThreadOwnership::kCustom.
/// UPDATE: turning off per-object controls; gonna see if we can get by
/// with just set_thread_checks_enabled() for temp special cases...
void SetThreadOwnership(
ThreadOwnership ownership,
ThreadIdentifier thread_identifier = ThreadIdentifier::kGame) {
#if BA_DEBUG_BUILD
thread_ownership_ = ownership;
if (thread_ownership_ == ThreadOwnership::kNextReferencing) {
owner_thread_ = ThreadIdentifier::kInvalid;
} else {
owner_thread_ = thread_identifier;
}
#endif
}
// Return true if the object is ref-counted and has at least 1 strong ref.
// This is generally a good thing for calls accepting object ptrs to check.
// Note that this can return false positives in release builds so should
// mainly be used as a debug sanity check (erroring if false)
auto is_valid_refcounted_object() const -> bool {
#if BA_DEBUG_BUILD
if (object_is_dead_) {
return false;
}
#endif
return (object_strong_ref_count_ > 0);
}
auto object_strong_ref_count() const -> int {
return object_strong_ref_count_;
}
template <typename T = Object>
class Ref;
template <typename T = Object>
class WeakRef;
class WeakRefBase {
public:
WeakRefBase() = default;
~WeakRefBase() { Release(); }
void Release() {
if (obj_) {
#if BA_DEBUG_BUILD
obj_->ObjectThreadCheck();
#endif
if (next_) {
next_->prev_ = prev_;
}
if (prev_) {
prev_->next_ = next_;
} else {
obj_->object_weak_refs_ = next_;
}
obj_ = nullptr;
next_ = prev_ = nullptr;
} else {
assert(next_ == nullptr && prev_ == nullptr);
}
}
private:
Object* obj_ = nullptr;
WeakRefBase* prev_ = nullptr;
WeakRefBase* next_ = nullptr;
friend class Object;
}; // WeakRefBase
/// Weak-reference to an instance of a specific Object subclass.
template <typename T>
class WeakRef : public WeakRefBase {
public:
auto exists() const -> bool { return (obj_ != nullptr); }
void Clear() { Release(); }
// Return a pointer or nullptr.
auto get() const -> T* {
// Yes, reinterpret_cast is evil, but we make sure
// we only operate on cases where this is valid
// (see Acquire()).
return reinterpret_cast<T*>(obj_);
}
// These operators throw exceptions if the object is dead.
auto operator*() const -> T& {
if (!obj_) {
throw Exception("Invalid dereference of " + static_type_name<T>());
}
// Yes, reinterpret_cast is evil, but we make sure
// we only operate on cases where this is valid
// (see Acquire()).
return *reinterpret_cast<T*>(obj_);
}
auto operator->() const -> T* {
if (!obj_) {
throw Exception("Invalid dereference of " + static_type_name<T>());
}
// Yes, reinterpret_cast is evil, but we make sure
// we only operate on cases where this is valid
// (see Acquire()).
return reinterpret_cast<T*>(obj_);
}
// Assign/compare with any compatible pointer.
template <typename U>
auto operator=(U* ptr) -> WeakRef<T>& {
Release();
// Go through our template type instead of assigning directly
// to our Object* so we catch invalid assigns at compile-time.
T* tmp = ptr;
if (tmp) Acquire(tmp);
// More debug sanity checks.
assert(reinterpret_cast<T*>(obj_) == ptr);
assert(static_cast<T*>(obj_) == ptr);
assert(dynamic_cast<T*>(obj_) == ptr);
return *this;
}
template <typename U>
auto operator==(U* ptr) -> bool {
return (get() == ptr);
}
template <typename U>
auto operator!=(U* ptr) -> bool {
return (get() != ptr);
}
// Assign/compare with same type ref (apparently the template below doesn't
// cover this case?).
auto operator=(const WeakRef<T>& ref) -> WeakRef<T>& {
*this = ref.get();
return *this;
}
auto operator==(const WeakRef<T>& ref) -> bool {
return (get() == ref.get());
}
auto operator!=(const WeakRef<T>& ref) -> bool {
return (get() != ref.get());
}
// Assign/compare with any compatible strong-ref.
template <typename U>
auto operator=(const Ref<U>& ref) -> WeakRef<T>& {
*this = ref.get();
return *this;
}
template <typename U>
auto operator==(const Ref<U>& ref) -> bool {
return (get() == ref.get());
}
template <typename U>
auto operator!=(const Ref<U>& ref) -> bool {
return (get() != ref.get());
}
// Assign/compare with any compatible weak-ref.
template <typename U>
auto operator=(const WeakRef<U>& ref) -> WeakRef<T>& {
*this = ref.get();
return *this;
}
template <typename U>
auto operator==(const WeakRef<U>& ref) -> bool {
return (get() == ref.get());
}
template <typename U>
auto operator!=(const WeakRef<U>& ref) -> bool {
return (get() != ref.get());
}
// Various constructors:
// Empty.
WeakRef() = default;
// From our type pointer.
explicit WeakRef(T* obj) { *this = obj; }
// Copy constructor (only non-explicit one).
WeakRef(const WeakRef<T>& ref) { *this = ref.get(); }
// From a compatible pointer.
template <typename U>
explicit WeakRef(U* ptr) {
*this = ptr;
}
// From a compatible strong ref.
template <typename U>
explicit WeakRef(const Ref<U>& ref) {
*this = ref;
}
// From a compatible weak ref.
template <typename U>
explicit WeakRef(const WeakRef<U>& ref) {
*this = ref;
}
private:
void Acquire(T* obj) {
if (obj == nullptr) {
throw Exception("Acquiring invalid ptr of " + static_type_name<T>());
}
#if BA_DEBUG_BUILD
// Seems like it'd be a good idea to prevent creation of weak-refs to
// objects in their destructors, but it turns out we're currently
// doing this (session points contexts at itself as it dies, etc.)
// Perhaps later can untangle this and change the behavior.
obj->ObjectThreadCheck();
assert(obj_ == nullptr && next_ == nullptr && prev_ == nullptr);
#endif
if (obj->object_weak_refs_) {
obj->object_weak_refs_->prev_ = this;
next_ = obj->object_weak_refs_;
}
obj->object_weak_refs_ = this;
// Sanity checking: We make the assumption that static-casting our pointer
// to/from Object gives the same results as reinterpret-casting it; let's
// be certain that's the case. In some cases involving multiple
// inheritance this might not be true, but we avoid those cases in our
// object hierarchy. (the one type of multiple inheritance we allow is
// pure virtual 'interfaces' which should not affect pointer offsets)
assert(static_cast<Object*>(obj) == reinterpret_cast<Object*>(obj));
// More random sanity checking.
assert(dynamic_cast<T*>(reinterpret_cast<Object*>(obj)) == obj);
obj_ = obj;
}
}; // WeakRef
// Strong-ref.
template <typename T>
class Ref {
public:
~Ref() { Release(); }
auto get() const -> T* { return obj_; }
// These operators throw an Exception if the object is dead.
auto operator*() const -> T& {
if (!obj_) {
throw Exception("Invalid dereference of " + static_type_name<T>());
}
return *obj_;
}
auto operator->() const -> T* {
if (!obj_) {
throw Exception("Invalid dereference of " + static_type_name<T>());
}
return obj_;
}
auto exists() const -> bool { return (obj_ != nullptr); }
void Clear() { Release(); }
// Assign/compare with any compatible pointer.
template <typename U>
auto operator=(U* ptr) -> Ref<T>& {
Release();
if (ptr) {
Acquire(ptr);
}
return *this;
}
template <typename U>
auto operator==(U* ptr) -> bool {
return (get() == ptr);
}
template <typename U>
auto operator!=(U* ptr) -> bool {
return (get() != ptr);
}
auto operator==(const Ref<T>& ref) -> bool { return (get() == ref.get()); }
auto operator!=(const Ref<T>& ref) -> bool { return (get() != ref.get()); }
// Assign/compare with same type ref (apparently the generic template below
// doesn't cover that case?..)
// DANGER: Seems to still compile if we comment this out, but crashes.
// Should get to the bottom of that.
auto operator=(const Ref<T>& ref) -> Ref<T>& {
assert(this != &ref); // Shouldn't be self-assigning.
*this = ref.get();
return *this;
}
// Assign/compare with any compatible strong-ref.
template <typename U>
auto operator=(const Ref<U>& ref) -> Ref<T>& {
*this = ref.get();
return *this;
}
template <typename U>
auto operator==(const Ref<U>& ref) -> bool {
return (get() == ref.get());
}
template <typename U>
auto operator!=(const Ref<U>& ref) -> bool {
return (get() != ref.get());
}
// Assign/compare from any compatible weak-ref.
template <typename U>
auto operator=(const WeakRef<U>& ref) -> Ref<T>& {
*this = ref.get();
return *this;
}
template <typename U>
auto operator==(const WeakRef<U>& ref) -> bool {
return (get() == ref.get());
}
template <typename U>
auto operator!=(const WeakRef<U>& ref) -> bool {
return (get() != ref.get());
}
// Various constructors:
// Empty.
Ref() = default;
// From our type pointer.
explicit Ref(T* obj) { *this = obj; }
// Copy constructor (only non-explicit one).
Ref(const Ref<T>& ref) { *this = ref.get(); }
// From a compatible pointer.
template <typename U>
explicit Ref(U* ptr) {
*this = ptr;
}
// From a compatible strong ref.
template <typename U>
explicit Ref(const Ref<U>& ref) {
*this = ref;
}
// From a compatible weak ref.
template <typename U>
explicit Ref(const WeakRef<U>& ref) {
*this = ref;
}
private:
void Acquire(T* obj) {
if (obj == nullptr) {
throw Exception("Acquiring invalid ptr of " + static_type_name<T>());
}
#if BA_DEBUG_BUILD
obj->ObjectThreadCheck();
// Obvs shouldn't be referencing dead stuff.
assert(!obj->object_is_dead_);
// Complain if creating an initial strong-ref to something
// not marked as ref-counted.
// (should make this an error once we know these are out of the system)
if (!obj->object_has_strong_ref_
&& !obj->object_creating_strong_reffed_) {
// Log only to system log for these low-level errors;
// console or server can cause deadlock due to recursive
// ref-list locks.
printf(
"Incorrectly creating initial strong-ref to %s; use "
"New() or MakeRefCounted()\n",
obj->GetObjectDescription().c_str());
}
obj->object_has_strong_ref_ = true;
#endif // BA_DEBUG_BUILD
obj->object_strong_ref_count_++;
obj_ = obj;
}
void Release() {
if (obj_ != nullptr) {
#if BA_DEBUG_BUILD
obj_->ObjectThreadCheck();
#endif
assert(obj_->object_strong_ref_count_ > 0);
obj_->object_strong_ref_count_--;
T* tmp = obj_;
// Invalidate ref *before* delete to avoid potential double-release.
obj_ = nullptr;
if (tmp->object_strong_ref_count_ == 0) {
#if BA_DEBUG_BUILD
tmp->object_is_dead_ = true;
#endif
delete tmp;
}
}
}
T* obj_ = nullptr;
};
/// Object::New<Type>(): The preferred way to create ref-counted Objects.
/// Allocates a new Object with the provided args and returns a strong
/// reference to it.
/// Generally you pass a single type to be instantiated and returned,
/// but you can optionally specify the two separately.
/// (for instance you may want to create a Button but return
/// a Ref to a Widget)
template <typename TRETURN, typename TALLOC = TRETURN, typename... ARGS>
[[nodiscard]] static auto New(ARGS&&... args) -> Object::Ref<TRETURN> {
auto* ptr = new TALLOC(std::forward<ARGS>(args)...);
#if BA_DEBUG_BUILD
if (ptr->object_creating_strong_reffed_) {
// Avoiding Log for these low level errors; can lead to deadlock.
printf("Object already set up as reffed in New: %s\n",
ptr->GetObjectDescription().c_str());
}
if (ptr->object_strong_ref_count_ > 0) {
// TODO(ericf): make this an error once its cleared out
printf("Obj strong-ref in constructor: %s\n",
ptr->GetObjectDescription().c_str());
}
ptr->object_in_constructor_ = false;
ptr->object_creating_strong_reffed_ = true;
#endif // BA_DEBUG_BUILD
return Object::Ref<TRETURN>(ptr);
}
/// In some cases it may be handy to allocate an object for ref-counting
/// but not actually create references yet. (Such as when creating an object
/// in one thread to be passed to another which will own said object)
/// For such cases, allocate using NewDeferred() and then use MakeRefCounted()
/// on the raw Object* to create its initial reference.
/// Note that in debug builds this will run checks to make sure the object
/// wound up being ref-counted. To allocate an object for manual
/// deallocation, use NewUnmanaged()
template <typename T, typename... ARGS>
[[nodiscard]] static auto NewDeferred(ARGS&&... args) -> T* {
T* ptr = new T(std::forward<ARGS>(args)...);
#if BA_DEBUG_BUILD
if (ptr->object_strong_ref_count_ > 0) {
printf("Obj strong-ref in constructor: %s\n",
ptr->GetObjectDescription().c_str());
}
ptr->object_in_constructor_ = false;
#endif
return ptr;
}
template <typename T>
static auto MakeRefCounted(T* ptr) -> Object::Ref<T> {
#if BA_DEBUG_BUILD
// Make sure we're operating on a fresh object.
assert(ptr->object_strong_ref_count_ == 0);
if (ptr->object_creating_strong_reffed_) {
// Avoiding Log for these low level errors; can lead to deadlock.
printf("Object already set up as reffed in MakeRefCounted: %s\n",
ptr->GetObjectDescription().c_str());
}
ptr->object_creating_strong_reffed_ = true;
#endif
return Object::Ref<T>(ptr);
}
/// Allocate an Object with no ref-counting; for use when an object
/// will be manually managed/deleted.
/// In debug builds, these objects will complain if attempts are made to
/// create strong references to them.
template <typename T, typename... ARGS>
[[nodiscard]] static auto NewUnmanaged(ARGS&&... args) -> T* {
T* ptr = new T(std::forward<ARGS>(args)...);
#if BA_DEBUG_BUILD
ptr->object_in_constructor_ = false;
#endif
return ptr;
}
private:
// Making operator new private here to help ensure all of our dynamic
// allocation/deallocation goes through our special functions (New(),
// NewDeferred(), etc.). However, sticking with original new for release
// builds since it may handle corner cases this does not.
#if BA_DEBUG_BUILD
auto operator new(size_t size) -> void* { return new char[size]; }
#endif
#if BA_DEBUG_BUILD
bool object_has_strong_ref_{};
bool object_creating_strong_reffed_{};
bool object_is_dead_{};
bool object_in_constructor_{true};
Object* object_next_{};
Object* object_prev_{};
ThreadOwnership thread_ownership_{ThreadOwnership::kClassDefault};
ThreadIdentifier owner_thread_{ThreadIdentifier::kInvalid};
bool thread_checks_enabled_{true};
millisecs_t object_birth_time_{};
bool object_printed_warning_{};
#endif
int object_strong_ref_count_{};
WeakRefBase* object_weak_refs_{};
BA_DISALLOW_CLASS_COPIES(Object);
}; // Object
/// Convert a vector of ptrs into a vector of refs.
template <typename T>
auto PointersToRefs(const std::vector<T*>& ptrs)
-> std::vector<Object::Ref<T> > {
std::vector<Object::Ref<T> > refs;
refs.reserve(ptrs.size());
for (typename std::vector<T*>::const_iterator i = ptrs.begin();
i != ptrs.end(); i++) {
refs.push_back(Object::Ref<T>(*i));
}
return refs;
}
/// Convert a vector of ptrs into a vector of refs.
template <typename T>
auto PointersToWeakRefs(const std::vector<T*>& ptrs)
-> std::vector<Object::WeakRef<T> > {
std::vector<Object::WeakRef<T> > refs;
refs.reserve(ptrs.size());
for (typename std::vector<T*>::const_iterator i = ptrs.begin();
i != ptrs.end(); i++) {
refs.push_back(Object::WeakRef<T>(*i));
}
return refs;
}
/// Convert a vector of refs to a vector of ptrs.
template <typename T>
auto RefsToPointers(const std::vector<Object::Ref<T> >& refs)
-> std::vector<T*> {
std::vector<T*> ptrs;
auto refs_size = refs.size();
if (refs_size > 0) {
ptrs.resize(refs_size);
// Let's just access the memory directly; potentially faster?
T** p = &(ptrs[0]);
for (size_t i = 0; i < refs_size; i++) {
p[i] = refs[i].get();
}
}
return ptrs;
}
/// Prune dead refs out of a vector/list.
template <typename T>
void PruneDeadRefs(T* list) {
for (typename T::iterator i = list->begin(); i != list->end();) {
if (!i->exists()) {
i = list->erase(i);
} else {
i++;
}
}
}
/// Prune dead refs out of a map/etc.
template <typename T>
void PruneDeadMapRefs(T* map) {
for (typename T::iterator i = map->begin(); i != map->end();) {
if (!i->second.exists()) {
typename T::iterator i_next = i;
i_next++;
map->erase(i);
i = i_next;
} else {
i++;
}
}
}
/// Print an Object (handles nullptr too).
inline auto ObjToString(Object* obj) -> std::string {
return obj ? obj->GetObjectDescription() : "<nullptr>";
}
// A handy utility which creates a weak-ref in debug mode
// and a simple pointer in release mode.
// This can be used when a pointer *should* always be valid
// but its nice to be sure when the cpu cycles don't matter
#if BA_DEBUG_BUILD
#define BA_DEBUG_PTR(TYPE) Object::WeakRef<TYPE>
#else
#define BA_DEBUG_PTR(TYPE) TYPE*
#endif
} // namespace ballistica
#endif // BALLISTICA_CORE_OBJECT_H_

View File

@ -0,0 +1,630 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/core/thread.h"
#include <map>
#include "ballistica/app/app.h"
#include "ballistica/core/fatal_error.h"
#include "ballistica/core/module.h"
#include "ballistica/platform/platform.h"
#include "ballistica/python/python.h"
namespace ballistica {
bool Thread::threads_paused_ = false;
void Thread::AddCurrentThreadName(const std::string& name) {
std::lock_guard<std::mutex> lock(g_app_globals->thread_name_map_mutex);
std::thread::id thread_id = std::this_thread::get_id();
auto i = g_app_globals->thread_name_map.find(thread_id);
std::string s;
if (i != g_app_globals->thread_name_map.end()) {
s = i->second;
}
if (!strstr(s.c_str(), name.c_str())) {
if (s.empty()) {
s = name;
} else {
s = s + "+" + name;
}
}
g_app_globals->thread_name_map[std::this_thread::get_id()] = s;
}
void Thread::ClearCurrentThreadName() {
std::lock_guard<std::mutex> lock(g_app_globals->thread_name_map_mutex);
auto i = g_app_globals->thread_name_map.find(std::this_thread::get_id());
if (i != g_app_globals->thread_name_map.end()) {
g_app_globals->thread_name_map.erase(i);
}
}
void Thread::UpdateMainThreadID() {
auto current_id = std::this_thread::get_id();
// This gets called a lot and it may happen before we are spun up,
// so just ignore it in that case..
if (g_app_globals) {
g_app_globals->main_thread_id = current_id;
}
if (g_app) {
g_app->thread()->set_thread_id(current_id);
}
}
void Thread::KillModule(const Module& module) {
for (auto i = modules_.begin(); i != modules_.end(); i++) {
if (*i == &module) {
delete *i;
modules_.erase(i);
return;
}
}
throw Exception("Module not found on this thread");
}
void Thread::KillModules() {
for (auto i : modules_) {
delete i;
}
modules_.clear();
}
// These are all exactly the same, but by running different ones for
// different thread groups makes its easy to see which thread is which
// in profilers, backtraces, etc.
auto Thread::RunGameThread(void* data) -> int {
return static_cast<Thread*>(data)->ThreadMain();
}
auto Thread::RunAudioThread(void* data) -> int {
return static_cast<Thread*>(data)->ThreadMain();
}
auto Thread::RunBGDynamicThread(void* data) -> int {
return static_cast<Thread*>(data)->ThreadMain();
}
auto Thread::RunNetworkWriteThread(void* data) -> int {
return static_cast<Thread*>(data)->ThreadMain();
}
auto Thread::RunStdInputThread(void* data) -> int {
return static_cast<Thread*>(data)->ThreadMain();
}
auto Thread::RunMediaThread(void* data) -> int {
return static_cast<Thread*>(data)->ThreadMain();
}
void Thread::SetPaused(bool paused) {
// Can be toggled from the main thread only.
assert(std::this_thread::get_id() == g_app_globals->main_thread_id);
PushThreadMessage(ThreadMessage(paused ? ThreadMessage::Type::kPause
: ThreadMessage::Type::kResume));
}
void Thread::WaitForNextEvent(bool single_cycle) {
// If we're running a single cycle we never stop to wait.
if (single_cycle) {
return;
}
// We also never wait if any of our modules have pending runnables.
// (we run all existing runnables in each loop cycle, but one of those
// may have enqueued more).
for (auto&& i : modules_) {
if (i->has_pending_runnables()) {
return;
}
}
// While we're waiting, allow other python threads to run.
if (owns_python_) {
g_python->ReleaseGIL();
}
// If we've got active timers, wait for messages with a timeout so we can
// run the next timer payload.
if ((!paused_) && timers_.active_timer_count() > 0) {
millisecs_t real_time = GetRealTime();
millisecs_t wait_time = timers_.GetTimeToNextExpire(real_time);
if (wait_time > 0) {
std::unique_lock<std::mutex> lock(thread_message_mutex_);
if (thread_message_count_ == 0) {
thread_message_cv_.wait_for(lock, std::chrono::milliseconds(wait_time),
[this] {
// Go back to sleep on spurious wakeups
// if we didn't wind up with any new
// messages.
return (thread_message_count_ > 0);
});
}
}
} else {
// Not running timers; just wait indefinitely for the next message.
std::unique_lock<std::mutex> lock(thread_message_mutex_);
if (thread_message_count_ == 0) {
thread_message_cv_.wait(lock, [this] {
// Go back to sleep on spurious wakeups
// (if we didn't wind up with any new messages).
return (thread_message_count_ > 0);
});
}
}
if (owns_python_) {
g_python->AcquireGIL();
}
}
void Thread::LoopUpkeep(bool single_cycle) {
// Keep our autorelease pool clean on mac/ios
// FIXME: Should define a Platform::ThreadHelper or something
// so we don't have platform-specific code here.
#if BA_XCODE_BUILD
// Let's not do autorelease pools when being called ad-hoc,
// since in that case we're part of another run loop
// (and its crashing on drain for some reason)
if (!single_cycle) {
if (auto_release_pool_) {
g_platform->DrainAutoReleasePool(auto_release_pool_);
auto_release_pool_ = nullptr;
}
auto_release_pool_ = g_platform->NewAutoReleasePool();
}
#endif
}
auto Thread::RunEventLoop(bool single_cycle) -> int {
while (true) {
LoopUpkeep(single_cycle);
WaitForNextEvent(single_cycle);
// Process all queued thread messages.
std::list<ThreadMessage> thread_messages;
GetThreadMessages(&thread_messages);
for (auto& thread_message : thread_messages) {
switch (thread_message.type) {
case ThreadMessage::Type::kNewModule: {
// Launch a new module and unlock.
ModuleLauncher* tl;
tl = static_cast<ModuleLauncher*>(thread_message.pval);
tl->Launch(this);
auto cmd =
static_cast<uint32_t>(ThreadMessage::Type::kNewModuleConfirm);
WriteToOwner(&cmd, sizeof(cmd));
break;
}
case ThreadMessage::Type::kRunnable: {
auto module_id = thread_message.ival;
Module* t = GetModule(module_id);
assert(t);
auto e = static_cast<Runnable*>(thread_message.pval);
// Add the event to our list.
t->PushLocalRunnable(e);
RunnablesWhilePausedSanityCheck(e);
break;
}
case ThreadMessage::Type::kShutdown: {
// Shutdown; die!
done_ = true;
break;
}
case ThreadMessage::Type::kResume: {
assert(paused_);
// Let all modules do pause-related stuff.
for (auto&& i : modules_) {
i->HandleThreadResume();
}
paused_ = false;
break;
}
case ThreadMessage::Type::kPause: {
assert(!paused_);
// Let all modules do pause-related stuff.
for (auto&& i : modules_) {
i->HandleThreadPause();
}
paused_ = true;
last_pause_time_ = GetRealTime();
messages_since_paused_ = 0;
break;
}
default: {
throw Exception();
}
}
// If the thread is going down.
if (done_) {
break;
}
}
// Run timers && queued module runnables unless we're paused.
if (!paused_) {
// Run timers.
timers_.Run(GetRealTime());
// Run module-messages.
for (auto& module_entry : modules_) {
module_entry->RunPendingRunnables();
}
}
if (done_ || single_cycle) {
break;
}
}
return 0;
}
void Thread::RunnablesWhilePausedSanityCheck(Runnable* e) {
// We generally shouldn't be getting messages while paused..
// (check both our pause-state and the global one; wanna ignore things
// that might slip through if some just-unlocked thread msgs us but we
// haven't been unlocked yet)
// UPDATE - we are migrating away from distinct message classes and towards
// LambdaRunnables for everything, which means that we can't easily
// see details of what is coming through. Disabling this check for now.
}
void Thread::GetThreadMessages(std::list<ThreadMessage>* messages) {
assert(messages);
assert(std::this_thread::get_id() == thread_id());
// Make sure they passed an empty one in.
assert(messages->empty());
if (thread_message_count_ > 0) {
std::unique_lock<std::mutex> lock(thread_message_mutex_);
assert(thread_messages_.size() == thread_message_count_);
messages->swap(thread_messages_);
thread_message_count_ = 0;
}
}
void Thread::WriteToOwner(const void* data, uint32_t size) {
assert(std::this_thread::get_id() == thread_id());
{
std::unique_lock<std::mutex> lock(data_to_client_mutex_);
data_to_client_.emplace_back(size);
memcpy(&(data_to_client_.back()[0]), data, size);
}
data_to_client_cv_.notify_all();
}
Thread::Thread(ThreadIdentifier identifier_in, ThreadType type_in)
: type_(type_in), identifier_(identifier_in) {
switch (type_) {
case ThreadType::kStandard: {
// Lock down until the thread is up and running. It'll unlock us when
// it's ready to go.
int (*func)(void*);
switch (identifier_) {
case ThreadIdentifier::kGame:
func = RunGameThread;
break;
case ThreadIdentifier::kMedia:
func = RunMediaThread;
break;
case ThreadIdentifier::kMain:
// Shouldn't happen; this thread gets wrapped; not launched.
throw Exception();
case ThreadIdentifier::kAudio:
func = RunAudioThread;
break;
case ThreadIdentifier::kBGDynamics:
func = RunBGDynamicThread;
break;
case ThreadIdentifier::kNetworkWrite:
func = RunNetworkWriteThread;
break;
case ThreadIdentifier::kStdin:
func = RunStdInputThread;
break;
default:
throw Exception();
}
// Let 'er rip.
thread_ = new std::thread(func, this);
// The thread lets us know when its up and running.
std::unique_lock<std::mutex> lock(data_to_client_mutex_);
uint32_t cmd;
ReadFromThread(&lock, &cmd, sizeof(cmd));
assert(static_cast<ThreadMessage::Type>(cmd)
== ThreadMessage::Type::kNewThreadConfirm);
break;
}
case ThreadType::kMain: {
// We've got no thread of our own to launch
// so we run our setup stuff right here instead of off in some.
assert(std::this_thread::get_id() == g_app_globals->main_thread_id);
thread_id_ = std::this_thread::get_id();
// Hmmm we might want to set our thread name here,
// as we do for other threads?
// However on linux that winds up being what we see in top/etc
// so maybe shouldn't.
break;
}
}
}
auto Thread::ThreadMain() -> int {
try {
assert(type_ == ThreadType::kStandard);
thread_id_ = std::this_thread::get_id();
const char* id_string;
switch (identifier_) {
case ThreadIdentifier::kGame:
id_string = "ballistica game";
break;
case ThreadIdentifier::kStdin:
id_string = "ballistica stdin";
break;
case ThreadIdentifier::kMedia:
id_string = "ballistica media";
break;
case ThreadIdentifier::kFileOut:
id_string = "ballistica file-out";
break;
case ThreadIdentifier::kMain:
id_string = "ballistica main";
break;
case ThreadIdentifier::kAudio:
id_string = "ballistica audio";
break;
case ThreadIdentifier::kBGDynamics:
id_string = "ballistica bg-dynamics";
break;
case ThreadIdentifier::kNetworkWrite:
id_string = "ballistica network writing";
break;
default:
throw Exception();
}
g_platform->SetCurrentThreadName(id_string);
// Send our owner a confirmation that we're alive.
auto cmd = static_cast<uint32_t>(ThreadMessage::Type::kNewThreadConfirm);
WriteToOwner(&cmd, sizeof(cmd));
// Now just run our loop until we die.
int result = RunEventLoop();
KillModules();
ClearCurrentThreadName();
return result;
} catch (const std::exception& e) {
auto error_msg = std::string("Unhandled exception in ")
+ GetCurrentThreadName() + " thread:\n" + e.what();
FatalError::ReportFatalError(error_msg, true);
bool exit_cleanly = !IsUnmodifiedBlessedBuild();
bool handled = FatalError::HandleFatalError(exit_cleanly, true);
// Do the default thing if platform didn't handle it.
if (!handled) {
if (exit_cleanly) {
exit(1);
} else {
throw;
}
}
return 0;
}
}
void Thread::SetOwnsPython() {
owns_python_ = true;
g_python->AcquireGIL();
}
// Explicitly kill the main thread.
void Thread::Quit() {
assert(type_ == ThreadType::kMain);
if (type_ == ThreadType::kMain) {
done_ = true;
}
}
Thread::~Thread() = default;
void Thread::LogThreadMessageTally() {
// Prevent recursion.
if (!writing_tally_) {
writing_tally_ = true;
std::map<std::string, int> tally;
Log("Thread message tally (" + std::to_string(thread_messages_.size())
+ " in list):");
for (auto&& m : thread_messages_) {
std::string s;
switch (m.type) {
case ThreadMessage::Type::kShutdown:
s += "kShutdown";
break;
case ThreadMessage::Type::kRunnable:
s += "kRunnable";
break;
case ThreadMessage::Type::kNewModule:
s += "kNewModule";
break;
case ThreadMessage::Type::kNewModuleConfirm:
s += "kNewModuleConfirm";
break;
case ThreadMessage::Type::kNewThreadConfirm:
s += "kNewThreadConfirm";
break;
case ThreadMessage::Type::kPause:
s += "kPause";
break;
case ThreadMessage::Type::kResume:
s += "kResume";
break;
default:
s += "UNKNOWN(" + std::to_string(static_cast<int>(m.type)) + ")";
break;
}
if (m.type == ThreadMessage::Type::kRunnable) {
// Runnable* e;
// e = static_cast<Runnable*>(m.pval);
{
std::string m_name = g_platform->DemangleCXXSymbol(
typeid(*(static_cast<Runnable*>(m.pval))).name());
s += std::string(": ") + m_name;
}
}
auto j = tally.find(s);
if (j == tally.end()) {
tally[s] = 1;
} else {
tally[s]++;
}
}
int entry = 1;
for (auto&& i : tally) {
Log(" #" + std::to_string(entry++) + " (" + std::to_string(i.second)
+ "x): " + i.first);
}
writing_tally_ = false;
}
}
void Thread::PushThreadMessage(const ThreadMessage& t) {
{
std::unique_lock<std::mutex> lock(thread_message_mutex_);
// Plop the data on to the list; we're assuming the mutex is locked.
thread_messages_.push_back(t);
// Keep our own count; apparently size() on an stl list involves
// iterating.
// FIXME: Actually I don't think this is the case anymore; should check.
thread_message_count_++;
assert(thread_message_count_ == thread_messages_.size());
// Show message count states.
if (explicit_bool(false)) {
static int one_off = 0;
static int foo = 0;
foo++;
one_off++;
// Show momemtary spikes.
if (thread_message_count_ > 100 && one_off > 100) {
one_off = 0;
foo = 999;
}
// Show count periodically.
if ((std::this_thread::get_id() == g_app_globals->main_thread_id)
&& foo > 100) {
foo = 0;
Log("MSG COUNT " + std::to_string(thread_message_count_));
}
}
if (thread_message_count_ > 1000) {
static bool sent_error = false;
if (!sent_error) {
sent_error = true;
Log("Error: ThreadMessage list > 1000 in thread: "
+ GetCurrentThreadName());
LogThreadMessageTally();
}
}
// Prevent runaway mem usage if the list gets out of control.
if (thread_message_count_ > 10000) {
throw Exception("KILLING APP: ThreadMessage list > 10000 in thread: "
+ GetCurrentThreadName());
}
// Unlock thread-message list and inform thread that there's something
// available.
}
thread_message_cv_.notify_all();
}
void Thread::ReadFromThread(std::unique_lock<std::mutex>* lock, void* buffer,
uint32_t size) {
// Threads cant read from themselves.. could load to lock-deadlock.
assert(std::this_thread::get_id() != thread_id());
data_to_client_cv_.wait(*lock, [this] {
// Go back to sleep on spurious wakeups
// (if we didn't wind up with any new messages)
return (!data_to_client_.empty());
});
// Read the oldest thing on our in-data list.
assert(!data_to_client_.empty());
assert(data_to_client_.front().size() == size);
memcpy(buffer, &(data_to_client_.front()[0]), size);
data_to_client_.pop_front();
}
void Thread::SetThreadsPaused(bool paused) {
threads_paused_ = paused;
for (auto&& i : g_app_globals->pausable_threads) {
i->SetPaused(paused);
}
}
auto Thread::AreThreadsPaused() -> bool { return threads_paused_; }
auto Thread::RegisterModule(const std::string& name, Module* module) -> int {
AddCurrentThreadName(name);
// This should assure we were properly launched.
// (the ModuleLauncher will set the index to what ours will be)
int index = static_cast<int>(modules_.size());
// module_entries_.emplace_back(module, name, index);
modules_.push_back(module);
return index;
}
auto Thread::NewTimer(millisecs_t length, bool repeat,
const Object::Ref<Runnable>& runnable) -> Timer* {
assert(IsCurrent());
assert(runnable.exists());
return timers_.NewTimer(GetRealTime(), length, 0, repeat ? -1 : 0, runnable);
}
auto Thread::GetCurrentThreadName() -> std::string {
if (g_app_globals == nullptr) {
return "unknown(not-yet-inited)";
}
{
std::lock_guard<std::mutex> lock(g_app_globals->thread_name_map_mutex);
auto i = g_app_globals->thread_name_map.find(std::this_thread::get_id());
if (i != g_app_globals->thread_name_map.end()) {
return i->second;
}
}
#if BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS || BA_OSTYPE_LINUX
std::string name = "unknown (sys-name=";
char buffer[256];
int result = pthread_getname_np(pthread_self(), buffer, sizeof(buffer));
if (result == 0) {
name += std::string("\"") + buffer + "\")";
} else {
name += "<error " + std::to_string(result) + ">";
}
return name;
#else
return "unknown";
#endif
}
} // namespace ballistica

View File

@ -0,0 +1,249 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_CORE_THREAD_H_
#define BALLISTICA_CORE_THREAD_H_
#include <condition_variable>
#include <list>
#include <mutex>
#include <string>
#include <utility>
#include <vector>
#include "ballistica/app/app_globals.h"
#include "ballistica/ballistica.h"
#include "ballistica/generic/timer_list.h"
#include "ballistica/platform/min_sdl.h"
namespace ballistica {
// A thread with a built-in event loop.
class Thread {
public:
explicit Thread(ThreadIdentifier id,
ThreadType type_in = ThreadType::kStandard);
virtual ~Thread();
/// Register a name for the current thread (should generally describe its
/// purpose). If called multiple times, names will be combined with a '+'. ie:
/// "graphics+animation+audio".
void AddCurrentThreadName(const std::string& name);
void ClearCurrentThreadName();
static auto GetCurrentThreadName() -> std::string;
/// Call this if the main thread changes.
static void UpdateMainThreadID();
static void SetThreadsPaused(bool enable);
static auto AreThreadsPaused() -> bool;
auto IsCurrent() const -> bool {
return std::this_thread::get_id() == thread_id();
}
// Used to quit the main thread.
void Quit();
struct ModuleLauncher {
virtual void Launch(Thread* g) = 0;
virtual ~ModuleLauncher() = default;
};
template <class MODULETYPE>
struct ModuleLauncherTemplate : public ModuleLauncher {
void Launch(Thread* g) override { new MODULETYPE(g); }
};
template <class MODULETYPE, class ARGTYPE>
struct ModuleLauncherArgTemplate : public ModuleLauncher {
explicit ModuleLauncherArgTemplate(ARGTYPE arg_in) : arg(arg_in) {}
ARGTYPE arg;
void Launch(Thread* g) override { new MODULETYPE(g, arg); }
};
void SetOwnsPython();
// Add a new module to a thread. This doesn't return anything. If you need
// a pointer to the module, have it store itself somewhere in its constructor
// or whatnot. Returning a pointer made it too easy to introduce race
// conditions with the thread trying to access itself via this pointer
// before it was set up.
template <class THREADTYPE>
void AddModule() {
switch (type_) {
case ThreadType::kStandard: {
// Launching a module in the current thread: do it immediately.
if (IsCurrent()) {
ModuleLauncherTemplate<THREADTYPE> launcher;
launcher.Launch(this);
} else {
// Launching a module in another thread;
// send a module-launcher and wait for the confirmation.
ModuleLauncherTemplate<THREADTYPE> launcher;
ModuleLauncher* tl = &launcher;
PushThreadMessage(
ThreadMessage(ThreadMessage::Type::kNewModule, 0, tl));
std::unique_lock<std::mutex> lock(data_to_client_mutex_);
uint32_t cmd;
ReadFromThread(&lock, &cmd, sizeof(cmd));
assert(static_cast<ThreadMessage::Type>(cmd)
== ThreadMessage::Type::kNewModuleConfirm);
}
break;
}
case ThreadType::kMain: {
assert(std::this_thread::get_id() == g_app_globals->main_thread_id);
new THREADTYPE(this);
break;
}
default: {
throw Exception();
}
}
}
// An alternate version of AddModule that passes an argument along
// to the thread's constructor.
template <class THREADTYPE, class ARGTYPE>
void AddModule(ARGTYPE arg) {
switch (type_) {
case ThreadType::kStandard: {
// Launching a module in the current thread: do it immediately.
if (IsCurrent()) {
ModuleLauncherArgTemplate<THREADTYPE, ARGTYPE> launcher(arg);
launcher.Launch(this);
} else {
// Launching a module in another thread;
// send a module-launcher and wait for the confirmation.
ModuleLauncherArgTemplate<THREADTYPE, ARGTYPE> launcher(arg);
ModuleLauncher* tl = &launcher;
PushThreadMessage(
ThreadMessage(ThreadMessage::Type::kNewModule, 0, tl));
std::unique_lock<std::mutex> lock(data_to_client_mutex_);
uint32_t cmd;
ReadFromThread(&lock, &cmd, sizeof(cmd));
assert(static_cast<ThreadMessage::Type>(cmd)
== ThreadMessage::Type::kNewModuleConfirm);
}
break;
}
case ThreadType::kMain: {
assert(std::this_thread::get_id() == g_app_globals->main_thread_id);
new THREADTYPE(this, arg);
break;
}
default: {
throw Exception();
}
}
}
void KillModule(const Module& module);
void SetPaused(bool paused);
auto thread_id() const -> std::thread::id { return thread_id_; }
// Needed in rare cases where we jump physical threads.
// (Our 'main' thread on Android can switch under us as
// rendering contexts are recreated in new threads/etc.)
void set_thread_id(std::thread::id id) { thread_id_ = id; }
auto RunEventLoop(bool single_cycle = false) -> int;
auto identifier() const -> ThreadIdentifier { return identifier_; }
// For use by modules.
auto RegisterModule(const std::string& name, Module* module) -> int;
void PushModuleRunnable(Runnable* runnable, int module_index) {
PushThreadMessage(Thread::ThreadMessage(
Thread::ThreadMessage::Type::kRunnable, module_index, runnable));
}
// Register a timer to run on the thread.
auto NewTimer(millisecs_t length, bool repeat,
const Object::Ref<Runnable>& runnable) -> Timer*;
private:
struct ThreadMessage {
enum class Type {
kShutdown = 999,
kRunnable,
kNewModule,
kNewModuleConfirm,
kNewThreadConfirm,
kPause,
kResume
};
Type type;
void* pval;
int ival;
explicit ThreadMessage(Type type_in, int ival_in = 0,
void* pval_in = nullptr)
: type(type_in), ival(ival_in), pval(pval_in) {}
};
static void RunnablesWhilePausedSanityCheck(Runnable* r);
void WaitForNextEvent(bool single_cycle);
void LoopUpkeep(bool once);
void LogThreadMessageTally();
void ReadFromThread(std::unique_lock<std::mutex>* lock, void* buffer,
uint32_t size);
void WriteToOwner(const void* data, uint32_t size);
bool writing_tally_ = false;
bool paused_ = false;
millisecs_t last_pause_time_ = 0;
int messages_since_paused_ = 0;
millisecs_t last_paused_message_report_time_ = 0;
bool done_ = false;
ThreadType type_;
int listen_sd_ = 0;
std::thread::id thread_id_{};
ThreadIdentifier identifier_ = ThreadIdentifier::kInvalid;
millisecs_t last_complaint_time_ = 0;
bool owns_python_ = false;
// FIXME: Should generalize this to some sort of PlatformThreadData class.
#if BA_XCODE_BUILD
void* auto_release_pool_ = nullptr;
#endif
void KillModules();
// These are all exactly the same, but by running different ones for
// different thread groups makes its easy to see which thread is which
// in profilers, backtraces, etc.
static auto RunGameThread(void* data) -> int;
static auto RunAudioThread(void* data) -> int;
static auto RunBGDynamicThread(void* data) -> int;
static auto RunNetworkWriteThread(void* data) -> int;
static auto RunStdInputThread(void* data) -> int;
static auto RunMediaThread(void* data) -> int;
auto ThreadMain() -> int;
std::thread* thread_;
void GetThreadMessages(std::list<ThreadMessage>* messages);
void PushThreadMessage(const ThreadMessage& t);
std::condition_variable thread_message_cv_;
std::mutex thread_message_mutex_;
std::list<ThreadMessage> thread_messages_;
int thread_message_count_ = 0;
std::condition_variable data_to_client_cv_;
std::mutex data_to_client_mutex_;
std::list<std::vector<char> > data_to_client_;
std::vector<Module*> modules_;
auto GetModule(int id) -> Module* {
assert(id >= 0 && id < static_cast<int>(modules_.size()));
return modules_[id];
}
// Complete list of all timers created by this group's modules.
TimerList timers_;
static bool threads_paused_;
};
} // namespace ballistica
#endif // BALLISTICA_CORE_THREAD_H_

1068
src/ballistica/core/types.h Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,161 @@
// Copyright (c) 2011-2020 Eric Froemling
// Derived from code licensed as follows:
/*
base64.cpp and base64.h
Copyright (C) 2004-2008 René Nyffenegger
This source code is provided 'as-is', without any express or implied
warranty. In no event will the author be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this source code must not be misrepresented; you must not
claim that you wrote the original source code. If you use this source code
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original source code.
3. This notice may not be removed or altered from any source distribution.
René Nyffenegger rene.nyffenegger@adp-gmbh.ch
*/
#include "ballistica/generic/base64.h"
#include <cctype>
namespace ballistica {
// NOLINTNEXTLINE(cert-err58-cpp)
static const std::string* base64_chars_non_urlsafe = new std::string(
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/");
// NOLINTNEXTLINE(cert-err58-cpp)
static const std::string* base64_chars_urlsafe = new std::string(
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/");
static inline auto is_base64(unsigned char c, bool urlsafe) -> bool {
if (urlsafe) {
return (isalnum(c) || (c == '-') || (c == '_'));
} else {
return (isalnum(c) || (c == '+') || (c == '/'));
}
}
auto base64_encode(const unsigned char* bytes_to_encode, unsigned int in_len,
bool urlsafe) -> std::string {
std::string ret;
int i = 0;
// int j = 0;
unsigned char char_array_3[3];
unsigned char char_array_4[4];
static const std::string& base64_chars =
urlsafe ? *base64_chars_urlsafe : *base64_chars_non_urlsafe;
while (in_len--) {
char_array_3[i++] = *(bytes_to_encode++);
if (i == 3) {
char_array_4[0] =
static_cast<unsigned char>((char_array_3[0] & 0xfcu) >> 2u);
char_array_4[1] =
static_cast<unsigned char>(((char_array_3[0] & 0x03u) << 4u)
+ ((char_array_3[1] & 0xf0u) >> 4u));
char_array_4[2] =
static_cast<unsigned char>(((char_array_3[1] & 0x0fu) << 2u)
+ ((char_array_3[2] & 0xc0u) >> 6u));
char_array_4[3] = static_cast<unsigned char>(char_array_3[2] & 0x3fu);
for (i = 0; (i < 4); i++) ret += base64_chars[char_array_4[i]];
i = 0;
}
}
if (i) {
for (int j = i; j < 3; j++) {
char_array_3[j] = '\0';
}
char_array_4[0] =
static_cast<unsigned char>((char_array_3[0] & 0xfcu) >> 2u);
char_array_4[1] = static_cast<unsigned char>(
((char_array_3[0] & 0x03u) << 4u) + ((char_array_3[1] & 0xf0u) >> 4u));
char_array_4[2] = static_cast<unsigned char>(
((char_array_3[1] & 0x0fu) << 2u) + ((char_array_3[2] & 0xc0u) >> 6u));
char_array_4[3] = static_cast<unsigned char>(char_array_3[2u] & 0x3fu);
for (int j = 0; (j < i + 1); j++) {
ret += base64_chars[char_array_4[j]];
}
while ((i++ < 3)) {
ret += '=';
}
}
return ret;
}
auto base64_decode(const std::string& encoded_string, bool urlsafe)
-> std::string {
int in_len = static_cast<int>(encoded_string.size());
int i = 0;
// int j = 0;
int in_ = 0;
unsigned char char_array_4[4], char_array_3[3];
std::string ret;
static const std::string& base64_chars =
urlsafe ? *base64_chars_urlsafe : *base64_chars_non_urlsafe;
while (in_len-- && (encoded_string[in_] != '=')
&& is_base64((unsigned char)encoded_string[in_], urlsafe)) {
char_array_4[i++] = (unsigned char)encoded_string[in_];
in_++;
if (i == 4) {
for (i = 0; i < 4; i++) {
char_array_4[i] =
static_cast<unsigned char>(base64_chars.find(char_array_4[i]));
}
char_array_3[0] = static_cast<unsigned char>(
(char_array_4[0] << 2u) + ((char_array_4[1] & 0x30u) >> 4u));
char_array_3[1] = static_cast<unsigned char>(
((char_array_4[1] & 0xfu) << 4u) + ((char_array_4[2] & 0x3cu) >> 2u));
char_array_3[2] = static_cast<unsigned char>(
((char_array_4[2] & 0x3u) << 6u) + char_array_4[3]);
for (i = 0; (i < 3); i++) ret += char_array_3[i];
i = 0;
}
}
if (i) {
for (int j = i; j < 4; j++) {
char_array_4[j] = 0;
}
for (int j = 0; j < 4; j++) { // NOLINT(modernize-loop-convert)
char_array_4[j] =
static_cast<unsigned char>(base64_chars.find(char_array_4[j]));
}
char_array_3[0] = static_cast<unsigned char>(
(char_array_4[0] << 2u) + ((char_array_4[1] & 0x30u) >> 4u));
char_array_3[1] = static_cast<unsigned char>(
((char_array_4[1] & 0xfu) << 4u) + ((char_array_4[2] & 0x3cu) >> 2u));
char_array_3[2] = static_cast<unsigned char>(
((char_array_4[2] & 0x3u) << 6u) + char_array_4[3]);
for (int j = 0; (j < i - 1); j++) {
ret += char_array_3[j];
}
}
return ret;
}
} // namespace ballistica

View File

@ -0,0 +1,16 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_GENERIC_BASE64_H_
#define BALLISTICA_GENERIC_BASE64_H_
#include <string>
namespace ballistica {
auto base64_encode(const unsigned char*, unsigned int len, bool urlsafe = false)
-> std::string;
auto base64_decode(const std::string& s, bool urlsafe = false) -> std::string;
} // namespace ballistica
#endif // BALLISTICA_GENERIC_BASE64_H_

View File

@ -0,0 +1,95 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_GENERIC_BUFFER_H_
#define BALLISTICA_GENERIC_BUFFER_H_
#include <cstdlib>
#include "ballistica/generic/utils.h"
namespace ballistica {
// Simple data-holding buffer class.
// (FIXME: should kill this and just use std::vector for this purpose)
template <class T>
class Buffer {
public:
Buffer(const Buffer& b) : data_(nullptr), size_(0) {
Resize(b.size());
if (b.size() > 0) {
memcpy(data_, b.data_, b.size() * sizeof(T));
}
}
~Buffer() {
if (data_) {
free(data_);
}
}
auto operator=(const Buffer& src) -> Buffer& {
assert(this != &src); // Shouldn't be self-assigning.
Resize(src.size());
if (size_ > 0) {
memcpy(data_, src.data_, size_ * sizeof(T));
}
return *this;
}
explicit Buffer(size_t size_in = 0) : data_(nullptr), size_(size_in) {
if (size_ > 0) {
Resize(size_);
}
}
Buffer(const T* data_in, size_t length) : data_(nullptr), size_(0) {
if (length > 0) {
Resize(length);
memcpy(data_, data_in, length * sizeof(T));
}
}
/// Get the amount of space needed to embed this buffer
auto GetFlattenedSize() -> size_t { return 4 + size_ * sizeof(T); }
/// Embed this buffer into a flat memory buffer.
void embed(char** b) {
// Embed our size (in items not bytes).
Utils::EmbedInt32NBO(b, static_cast<int32_t>(size_));
memcpy(*b, data_, size_ * sizeof(T));
*b += size_ * sizeof(T);
}
/// Extract this buffer for a flat memory buffer.
void Extract(const char** b) {
Resize(static_cast_check_fit<size_t>(Utils::ExtractInt32NBO(b)));
memcpy(data_, *b, size_ * sizeof(T));
*b += size_ * sizeof(T);
}
void Resize(size_t new_size) {
if (data_) {
free(data_);
}
if (new_size > 0) {
data_ = static_cast<T*>(malloc(new_size * sizeof(T)));
BA_PRECONDITION(data_);
} else {
data_ = nullptr;
}
size_ = new_size;
}
// gets the length in the buffer's units (not bytes)
auto size() const -> size_t { return size_; }
auto data() const -> T* { return data_; }
private:
T* data_;
size_t size_;
};
} // namespace ballistica
#endif // BALLISTICA_GENERIC_BUFFER_H_

View File

@ -0,0 +1,590 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/generic/huffman.h"
#include <string>
#include <vector>
#include "ballistica/networking/networking.h"
namespace ballistica {
// Yes, I should clean this up to use unsigned vals, but it seems to work
// fine for now so I don't want to touch it.
#pragma clang diagnostic push
#pragma ide diagnostic ignored "hicpp-signed-bitwise"
// how much data we read in training mode before spitting out results
#if HUFFMAN_TRAINING_MODE
const int kTrainingLength = 200000;
#endif
// we currently just have a static table of char frequencies - this can be
// generated by setting "training mode" on.
static int g_freqs[] = {
101342, 9667, 3497, 1072, 0, 3793, 0, 0, 2815, 5235, 0, 0, 0, 3570, 0, 0,
0, 1383, 0, 0, 0, 2970, 0, 0, 2857, 0, 0, 0, 0, 0, 0, 0,
0, 1199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1494,
1974, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1351, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1475,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
static void DoWriteBits(char** ptr, int* bit, int val, int val_bits) {
int src_bit = 0;
while (src_bit < val_bits) {
**ptr |= ((val >> src_bit) & 0x01) << (*bit); // NOLINT
if ((*bit) == 7) (*ptr)++;
(*bit) = ((*bit) + 1) % 8;
src_bit++;
}
}
Huffman::Huffman() : built(false) {
static_assert(sizeof(g_freqs) == sizeof(int) * 256);
build();
}
Huffman::~Huffman() = default;
auto Huffman::compress(const std::vector<uint8_t>& src)
-> std::vector<uint8_t> {
#if BA_HUFFMAN_NET_COMPRESSION
auto length = static_cast<uint32_t>(src.size());
const char* data = (const char*)src.data();
// IMPORTANT:
// our uncompressed packets have a type byte at the beginning
// (which should just be a few bits)
// and the compressed ones have a remainder byte (4 bits of which are used)
// ...so the first few bits should always be unused.
// we hijack the highest bit to denote whether we're sending
// a compressed or uncompressed packet (1 for compressed, 0 for uncompressed)
BA_PRECONDITION(data[0] >> 7 == 0);
// see how many bits we'll need
uint32_t bit_count = 0;
for (uint32_t i = 0; i < length; i++) {
bit_count += nodes_[static_cast<uint8_t>(data[i])].bits;
}
// round up to next byte and add our one-byte header
uint32_t length_out = bit_count / 8 + 1;
if (bit_count % 8) {
length_out++;
}
bit_count %= 8;
// if compressed is bigger than uncompressed, go with uncompressed - just
// return the data they provided
if ((length_out >= length)) {
return src;
} else {
std::vector<uint8_t> out(length_out, 0);
// first byte gives our number of empty trailing bits
char* ptr = reinterpret_cast<char*>(out.data());
int bit = 0;
*ptr = static_cast<char>(8 - bit_count % 8);
if (*ptr == 8) {
*ptr = 0;
}
ptr++;
for (uint32_t i = 0; i < length; i++) {
DoWriteBits(&ptr, &bit, nodes_[static_cast<uint8_t>(data[i])].val,
nodes_[static_cast<uint8_t>(data[i])].bits);
}
// make sure we're either at the end of our allotted buffer or we're one
// from the end and the bitcount takes care of the rest
assert(ptr - reinterpret_cast<char*>(out.data()) == length_out
|| (ptr - reinterpret_cast<char*>(out.data()) == length_out - 1
&& bit_count != 0));
assert(bit == bit_count % 8);
// mark it as compressed
out[0] |= (0x01 << 7);
return out;
}
#else
#if HUFFMAN_TRAINING_MODE
train(data, length);
#endif
data_out = data;
length_out = length;
#endif
}
// hmmm - I saw a crash logged in this function; need to make sure this is
// bulletproof since untrusted data is coming through here..
auto Huffman::decompress(const std::vector<uint8_t>& src)
-> std::vector<uint8_t> {
#if BA_HUFFMAN_NET_COMPRESSION
auto length = static_cast<uint32_t>(src.size());
BA_PRECONDITION(length > 0);
const char* data = (const char*)src.data();
auto remainder = static_cast<uint8_t>(*data & 0x0F);
bool compressed = *data >> 7;
if (compressed) {
std::vector<uint8_t> out;
out.reserve(src.size() * 2); // hopefully minimize reallocations..
uint32_t bit_length = ((length - 1) * 8);
if (remainder > bit_length) throw Exception("invalid huffman data");
bit_length -= remainder;
uint32_t bit = 0;
const char* ptr = data + 1;
// navigate bit by bit through our nodes to build values from binary codes
while (bit < bit_length) {
bool bitval = static_cast<bool>((ptr[bit / 8] >> (bit % 8)) & 0x01);
bit++;
// 1 in first bit denotes huffman compressed
if (bitval) {
int val;
int n = 510;
BA_PRECONDITION(nodes_[n].parent == 0);
while (true) {
BA_PRECONDITION(n <= 510);
bitval = static_cast<bool>((ptr[bit / 8] >> (bit % 8)) & 0x01);
// 1 for right, 0 for left
if (bitval == 0) {
if (nodes_[n].left_child == -1) {
val = n;
break;
} else {
n = nodes_[n].left_child;
bit++;
}
} else {
if (nodes_[n].right_child == -1) {
val = n;
break;
} else {
n = nodes_[n].right_child;
bit++;
}
}
// ERICF FIX - if both new children are dead-ends, stop reading
// bits; otherwise we might read past the end of the buffer.
// (I assume the original code didn't have child nodes with dual -1s
// so this case probably never came up)
if (nodes_[n].left_child == -1 && nodes_[n].right_child == -1) {
val = n;
break;
}
if (bit > bit_length) {
throw Exception("huffman decompress got bit > bitlength");
}
}
out.push_back(static_cast<unsigned char&&>(val));
} else {
// just read next 8 bits as value
uint8_t val;
if (bit % 8 == 0) {
BA_PRECONDITION((bit / 8) < (length - 1));
val = static_cast<uint8_t>(ptr[bit / 8]);
} else {
BA_PRECONDITION((bit / 8 + 1) < (length - 1));
val = (static_cast<uint8_t>(ptr[bit / 8]) >> bit % 8)
| (static_cast<uint8_t>(ptr[bit / 8 + 1]) << (8 - bit % 8));
}
out.push_back(val);
bit += 8;
if (bit > bit_length) {
throw Exception("huffman decompress got bit > bitlength b");
}
}
}
BA_PRECONDITION(bit == bit_length);
return out;
} else {
// uncompressed - just provide it as is
return src;
}
#else
data_out = data;
length_out = length;
#endif
}
// old janky version..
#if 0
void Huffman::compress(const char* data, uint32_t length, const char*& data_out,
uint32_t& length_out) {
if (length > kMaxPacketSize) {
throw Exception("packet too large for huffman compressor: "
+ std::to_string(length) + " (packet "
+ std::to_string(static_cast<int>(data[0])) + ")");
}
#if BA_HUFFMAN_NET_COMPRESSION
// all our packets have a type bit at the beginning
// we hijack the highest bit to denote whether we're sending
// a compressed or uncompressed packet (1 for compressed, 0 for uncompressed)
BA_PRECONDITION(data[0] >> 7 == 0);
// length_out = length;
// if (buffer.size() < length_out)
// buffer.resize(length_out);
// memcpy(buffer.data,data,length_out);
// see how many bits we'll need
uint32_t bit_count = 0;
for (uint32_t i = 0; i < length; i++) {
bit_count += nodes[static_cast<uint8_t>(data[i])].bits;
}
// round up to next byte and add our one-byte header
length_out = bit_count / 8 + 1;
if (bit_count % 8) length_out++;
bit_count %= 8;
// if compressed is bigger than uncompressed, go with uncompressed -
// just return the data they provided
// lets always do huffman in debug builds; make sure we aren't making
// any incorrect assumptions about
// where stuff is compressed vs uncompressed.
#if BA_DEBUG_BUILD
bool force = false;
#else
bool force = false;
#endif
if ((length_out >= length) && !force) {
// throw Exception();
data_out = data;
length_out = length;
} else {
if (buffer.size() < length_out) {
buffer.resize(length_out);
}
// first byte gives our number of empty trailing bits
memset(buffer.data, 0, buffer.size());
char* ptr = buffer.data;
int bit = 0;
*ptr = (8 - bit_count % 8);
if (*ptr == 8) *ptr = 0;
ptr++;
for (uint32_t i = 0; i < length; i++) {
DoWriteBits(ptr, bit, nodes[static_cast<uint8_t>(data[i])].val,
nodes[static_cast<uint8_t>(data[i])].bits);
}
// make sure we're either at the end of our alloted buffer or we're one
// from the end and the bitcount takes care of the rest
assert(ptr - buffer.data == length_out
|| (ptr - buffer.data == length_out - 1 && bit_count != 0));
assert(bit == bit_count % 8);
// for (int i = 0; i < length_out;i++)
// buffer.data[i]--;
data_out = buffer.data;
// mark it as compressed
buffer.data[0] |= (0x01 << 7);
}
#else
#if HUFFMAN_TRAINING_MODE
train(data, length);
#endif
data_out = data;
length_out = length;
#endif
}
#endif
#if 0
void Huffman::decompress(const char* data, uint32_t length,
const char*& data_out, uint32_t& length_out) {
#if BA_HUFFMAN_NET_COMPRESSION
uint8_t remainder = *data & 0x0F;
bool compressed = *data >> 7;
if (compressed) {
uint32_t bit_length = ((length - 1) * 8) - remainder;
uint32_t bit = 0;
const char* ptr = data + 1;
uint32_t bytes = 0;
// navigate bit by bit through our nodes to build values from binary codes
while (bit < bit_length) {
bool bitval = (ptr[bit / 8] >> (bit % 8)) & 0x01;
bit++;
// 1 in first bit denotes huffman compressed
if (bitval) {
int val;
int n = 510;
assert(nodes[n].parent == 0);
while (true) {
bitval = (ptr[bit / 8] >> (bit % 8)) & 0x01;
// 1 for right, 0 for left
if (bitval == 0) {
if (nodes[n].left_child == -1) {
val = n;
break;
} else {
n = nodes[n].left_child;
bit++;
}
} else {
if (nodes[n].right_child == -1) {
val = n;
break;
} else {
n = nodes[n].right_child;
bit++;
}
}
}
buffer.data[bytes] = val;
bytes++;
} else {
// just read next 8 bits as value
// unsigned int val = (((ptr[(bit+0)/8] >> ((bit+0)%8)) & 0x01)
// << 0)
// | (((ptr[(bit+1)/8] >> ((bit+1)%8)) & 0x01) << 1)
// | (((ptr[(bit+2)/8] >> ((bit+2)%8)) & 0x01) << 2)
// | (((ptr[(bit+3)/8] >> ((bit+3)%8)) & 0x01) << 3)
// | (((ptr[(bit+4)/8] >> ((bit+4)%8)) & 0x01) << 4)
// | (((ptr[(bit+5)/8] >> ((bit+5)%8)) & 0x01) << 5)
// | (((ptr[(bit+6)/8] >> ((bit+6)%8)) & 0x01) << 6)
// | (((ptr[(bit+7)/8] >> ((bit+7)%8)) & 0x01) << 7);
uint8_t val;
if (bit % 8 == 0)
val = static_cast<uint8_t>(ptr[bit / 8]);
else
val = (static_cast<uint8_t>(ptr[bit / 8]) >> bit % 8)
| (static_cast<uint8_t>(ptr[bit / 8 + 1]) << (8 - bit % 8));
// uint8_t val2 = (((ptr[(bit+0)/8] >> ((bit+0)%8)) & 0x01) <<
// 0)
// | (((ptr[(bit+1)/8] >> ((bit+1)%8)) & 0x01) << 1)
// | (((ptr[(bit+2)/8] >> ((bit+2)%8)) & 0x01) << 2)
// | (((ptr[(bit+3)/8] >> ((bit+3)%8)) & 0x01) << 3)
// | (((ptr[(bit+4)/8] >> ((bit+4)%8)) & 0x01) << 4)
// | (((ptr[(bit+5)/8] >> ((bit+5)%8)) & 0x01) << 5)
// | (((ptr[(bit+6)/8] >> ((bit+6)%8)) & 0x01) << 6)
// | (((ptr[(bit+7)/8] >> ((bit+7)%8)) & 0x01) << 7);
// assert(val2 == val);
buffer.data[bytes] = val;
bytes++;
bit += 8;
// throw Exception();
}
}
assert(bit == bit_length);
// fixme??
if (bytes > kMaxPacketSize) {
Log("HUFFMAN DECOMPRESSING TO TOO LARGE: " + std::to_string(bytes));
}
assert(bytes <= kMaxPacketSize);
// throw Exception();
// length_out = length;
// if (buffer.size() < length_out)
// buffer.resize(length_out);
// memcpy(buffer.data,data,length_out);
// data_out = buffer.data;
// for (int i = 0; i < length_out;i++)
// buffer.data[i]++;
data_out = buffer.data;
length_out = bytes;
} else {
// uncompressed - just provide it as is
data_out = data;
length_out = length;
}
#else
data_out = data;
length_out = length;
#endif
}
#endif // 0
#if HUFFMAN_TRAINING_MODE
void Huffman::train(const char* buffer, int len) {
if (built) {
test_bytes += len;
for (int i = 0; i < len; i++) {
test_bits_compressed += nodes[static_cast<uint8_t>(buffer[i])].bits;
}
static int poo = 0;
poo++;
if (poo > 100) {
poo = 0;
test_bytes = 0;
test_bits_compressed = 0;
}
return;
}
total_length += len;
while (len > 0) {
nodes[static_cast<uint8_t>(*buffer)].frequency++;
total_count++;
buffer++;
len--;
}
if (total_length > kTrainingLength) {
Log("HUFFMAN TRAINING COMPLETE:");
build();
// spit the C array to stdout for insertion into our code
string s = "{";
for (int i = 0; i < 256; i++) {
s += std::to_string(nodes[i].frequency);
if (i < 255) s += ",";
}
s += "}";
Log("FINAL: " + s);
}
}
#endif // HUFFMAN_TRAINING_MODE
void Huffman::build() {
assert(!built);
// if we're not in training mode, use our hard-coded values
#if 1
for (int i = 0; i < 256; i++) {
nodes_[i].frequency = g_freqs[i];
}
#else
// go through and set all but the top 15 or so to zero
// this is because all smaller values will be provided in full binary
// form and thus don't need to be influencing the graph
for (int i = 0; i < 256; i++) {
int bigger = 0;
for (int j = 0; j < 256; j++) {
if (nodes[j].frequency > nodes[i].frequency) {
bigger++;
if (bigger > 15) {
nodes[i].frequency = 0;
break;
}
}
}
}
#endif
// first 256 nodes are leaves
int node_count = 256;
// now loop through existing nodes finding the two smallest values without
// parents and creating a new parent node for them with their sum as its
// frequency value once there's only 1 node without a parent we're done
// (that's the root node)
int smallest1;
int smallest2;
while (node_count < 511) {
int i = 0;
// find first two non-parented nodes
while (nodes_[i].parent != 0) i++;
smallest1 = i;
i++;
while (nodes_[i].parent != 0) i++;
smallest2 = i;
i++;
while (i < node_count) {
if (nodes_[i].parent == 0) {
// compare each node to the larger of the two existing to try and knock
// it off
if (nodes_[smallest1].frequency > nodes_[smallest2].frequency) {
if (nodes_[i].frequency < nodes_[smallest1].frequency) smallest1 = i;
} else {
if (nodes_[i].frequency < nodes_[smallest2].frequency) smallest2 = i;
}
}
i++;
}
nodes_[node_count].frequency =
nodes_[smallest1].frequency + nodes_[smallest2].frequency;
nodes_[smallest1].parent = static_cast<uint8_t>(node_count - 255);
nodes_[smallest2].parent = static_cast<uint8_t>(node_count - 255);
nodes_[node_count].right_child = static_cast<int16_t>(smallest1);
nodes_[node_count].left_child = static_cast<int16_t>(smallest2);
node_count++;
}
assert(nodes_[509].parent != 0);
assert(nodes_[510].parent == 0);
// now store binary values for each base value (0-255)
for (int i = 0; i < 256; i++) {
// uint32_t val = 0;
nodes_[i].val = 0;
nodes_[i].bits = 0;
int index = i;
while (nodes_[index].parent != 0) {
// 0 if we're left child, 1 if we're right
if (nodes_[nodes_[index].parent + 255].right_child == index) {
nodes_[i].val = static_cast<uint16_t>(nodes_[i].val << 1 | 0x01);
} else {
assert(nodes_[nodes_[index].parent + 255].left_child == index);
nodes_[i].val = nodes_[i].val << 1;
}
nodes_[i].bits++;
index = nodes_[index].parent + 255;
}
// we're slightly different than normal huffman in that
// our first bit denotes whether the following values are the huffman bits
// or the full 8 bit value.
if (nodes_[i].bits >= 8) {
nodes_[i].bits = 8;
// nodes[i].val = nodes[i].val << 1;
nodes_[i].val = static_cast<uint16_t>(i << 1);
} else {
nodes_[i].val = static_cast<uint16_t>(
nodes_[i].val << 1
| 0x01); // 1 in first bit denotes huffman compressed
}
// nodes[i].val = 0;
nodes_[i].bits += 1;
}
built = true;
}
#pragma clang diagnostic pop
} // namespace ballistica

View File

@ -0,0 +1,61 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_GENERIC_HUFFMAN_H_
#define BALLISTICA_GENERIC_HUFFMAN_H_
#include <vector>
#include "ballistica/core/object.h"
namespace ballistica {
class Huffman {
public:
Huffman();
~Huffman();
#if HUFFMAN_TRAINING_MODE
void train(const char* buffer, int len);
#endif
void build();
// NOTE: this assumes the topmost bit of the first byte is unused
// (see details in implementation).
auto compress(const std::vector<uint8_t>& src) -> std::vector<uint8_t>;
auto decompress(const std::vector<uint8_t>& src) -> std::vector<uint8_t>;
auto get_built() const -> bool { return built; }
private:
bool built;
#if HUFFMAN_TRAINING_MODE
uint32_t test_bytes = 0;
uint32_t test_bits_compressed = 0;
int total_count = 0;
int total_length = 0;
#endif
class Node {
public:
Node() = default;
// Left child index in node array (-1 for none).
int16_t left_child = -1;
// Right child index in node array (-1 for none).
int16_t right_child = -1;
// Parent index in node array (0 for none - add 255 to this to get actual
// index).
uint8_t parent = 0;
uint8_t bits = 0;
uint16_t val = 0;
int frequency = 0;
};
Node nodes_[511];
};
} // namespace ballistica
#endif // BALLISTICA_GENERIC_HUFFMAN_H_

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,239 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_GENERIC_JSON_H_
#define BALLISTICA_GENERIC_JSON_H_
/*
Copyright (c) 2009 Dave Gamble
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.
*/
#include "ballistica/ballistica.h"
namespace ballistica {
#pragma clang diagnostic push
#pragma ide diagnostic ignored "OCUnusedMacroInspection"
// #ifdef __cplusplus
// extern "C" {
// #endif
/* cJSON Types: */
#define cJSON_False 0u
#define cJSON_True 1u
#define cJSON_NULL 2u
#define cJSON_Number 3u
#define cJSON_String 4u
#define cJSON_Array 5u
#define cJSON_Object 6u
#define cJSON_IsReference 256u
/* The cJSON structure: */
typedef struct cJSON {
struct cJSON *next,
*prev; /* next/prev allow you to walk array/object chains. Alternatively,
use GetArraySize/GetArrayItem/GetObjectItem */
struct cJSON*
child; /* An array or object item will have a child pointer pointing to a
chain of the items in the array/object. */
uint32_t type; /* The type of the item, as above. */
char* valuestring; /* The item's string, if type==cJSON_String */
int valueint; /* The item's number, if type==cJSON_Number */
double valuedouble; /* The item's number, if type==cJSON_Number */
char* string; /* The item's name string, if this item is the child of, or is
in the list of subitems of an object. */
} cJSON;
typedef struct cJSON_Hooks {
void* (*malloc_fn)(size_t sz);
void (*free_fn)(void* ptr);
} cJSON_Hooks;
/* Supply malloc, realloc and free functions to cJSON */
extern void cJSON_InitHooks(cJSON_Hooks* hooks);
/* Supply a block of JSON, and this returns a cJSON object you can interrogate.
* Call cJSON_Delete when finished. */
extern auto cJSON_Parse(const char* value) -> cJSON*;
/* Render a cJSON entity to text for transfer/storage. Free the char* when
* finished. */
extern auto cJSON_Print(cJSON* item) -> char*;
/* Render a cJSON entity to text for transfer/storage without any formatting.
* Free the char* when finished. */
extern auto cJSON_PrintUnformatted(cJSON* item) -> char*;
/* Delete a cJSON entity and all subentities. */
extern void cJSON_Delete(cJSON* c);
/* Returns the number of items in an array (or object). */
extern auto cJSON_GetArraySize(cJSON* array) -> int;
/* Retrieve item number "item" from array "array". Returns NULL if unsuccessful.
*/
extern auto cJSON_GetArrayItem(cJSON* array, int item) -> cJSON*;
/* Get item "string" from object. Case insensitive. */
extern auto cJSON_GetObjectItem(cJSON* object, const char* string) -> cJSON*;
/* For analysing failed parses. This returns a pointer to the parse error.
* You'll probably need to look a few chars back to make sense of it. Defined
* when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
extern auto cJSON_GetErrorPtr() -> const char*;
/* These calls create a cJSON item of the appropriate type. */
extern auto cJSON_CreateNull() -> cJSON*;
extern auto cJSON_CreateTrue() -> cJSON*;
extern auto cJSON_CreateFalse() -> cJSON*;
extern auto cJSON_CreateBool(int b) -> cJSON*;
extern auto cJSON_CreateNumber(double num) -> cJSON*;
extern auto cJSON_CreateString(const char* string) -> cJSON*;
extern auto cJSON_CreateArray() -> cJSON*;
extern auto cJSON_CreateObject() -> cJSON*;
/* These utilities create an Array of count items. */
extern auto cJSON_CreateIntArray(const int* numbers, int count) -> cJSON*;
extern auto cJSON_CreateFloatArray(const float* numbers, int count) -> cJSON*;
extern auto cJSON_CreateDoubleArray(const double* numbers, int count) -> cJSON*;
extern auto cJSON_CreateStringArray(const char** strings, int count) -> cJSON*;
/* Append item to the specified array/object. */
extern void cJSON_AddItemToArray(cJSON* array, cJSON* item);
extern void cJSON_AddItemToObject(cJSON* object, const char* string,
cJSON* item);
/* Append reference to item to the specified array/object. Use this when you
* want to add an existing cJSON to a new cJSON, but don't want to corrupt your
* existing cJSON. */
extern void cJSON_AddItemReferenceToArray(cJSON* array, cJSON* item);
extern void cJSON_AddItemReferenceToObject(cJSON* object, const char* string,
cJSON* item);
/* Remove/Detach items from Arrays/Objects. */
extern auto cJSON_DetachItemFromArray(cJSON* array, int which) -> cJSON*;
extern void cJSON_DeleteItemFromArray(cJSON* array, int which);
extern auto cJSON_DetachItemFromObject(cJSON* object, const char* string)
-> cJSON*;
extern void cJSON_DeleteItemFromObject(cJSON* object, const char* string);
/* Update array items. */
extern void cJSON_ReplaceItemInArray(cJSON* array, int which, cJSON* newitem);
extern void cJSON_ReplaceItemInObject(cJSON* object, const char* string,
cJSON* newitem);
/* Duplicate a cJSON item */
extern auto cJSON_Duplicate(cJSON* item, int recurse) -> cJSON*;
/* Duplicate will create a new, identical cJSON item to the one you pass, in new
memory that will need to be released. With recurse!=0, it will duplicate any
children connected to the item. The item->next and ->prev pointers are always
zero on return from Duplicate. */
/* ParseWithOpts allows you to require (and check) that the JSON is null
* terminated, and to retrieve the pointer to the final byte parsed. */
extern auto cJSON_ParseWithOpts(const char* value,
const char** return_parse_end,
int require_null_terminated) -> cJSON*;
extern void cJSON_Minify(char* json);
/* Macros for creating things quickly. */
#define cJSON_AddNullToObject(object, name) \
cJSON_AddItemToObject(object, name, cJSON_CreateNull())
#define cJSON_AddTrueToObject(object, name) \
cJSON_AddItemToObject(object, name, cJSON_CreateTrue())
#define cJSON_AddFalseToObject(object, name) \
cJSON_AddItemToObject(object, name, cJSON_CreateFalse())
#define cJSON_AddBoolToObject(object, name, b) \
cJSON_AddItemToObject(object, name, cJSON_CreateBool(b))
#define cJSON_AddNumberToObject(object, name, n) \
cJSON_AddItemToObject(object, name, cJSON_CreateNumber(n))
#define cJSON_AddStringToObject(object, name, s) \
cJSON_AddItemToObject(object, name, cJSON_CreateString(s))
/* When assigning an integer value, it needs to be propagated to valuedouble
* too. */
#define cJSON_SetIntValue(object, val) \
((object) ? (object)->valueint = (object)->valuedouble = (val) : (val))
// ericf addition: c++ wrapper for this stuff.
// NOTE: once added to a dict/list/etc, the underlying cJSON's
// lifecycle is dependent on its parent, not this object.
// ..So be sure to keep the root JsonObject alive as long as child
// objects are being accessed.
class JsonObject {
public:
~JsonObject() {
if (obj_ && root_) {
cJSON_Delete(obj_);
}
}
auto root() const -> bool { return root_; }
auto obj() const -> cJSON* { return obj_; }
// Root objects will clean themselves up.
// turn this off when adding to a dict/list/etc.
// that will take responsibility for that instead.
void set_root(bool val) { root_ = val; }
protected:
JsonObject() = default;
// Used by subclasses to fill value.
void set_obj(cJSON* val) {
assert(obj_ == nullptr);
obj_ = val;
}
private:
cJSON* obj_ = nullptr;
bool root_ = true;
};
class JsonDict : public JsonObject {
public:
JsonDict() { set_obj(cJSON_CreateObject()); }
void AddNumber(const std::string& name, double val) {
cJSON_AddItemToObject(obj(), name.c_str(), cJSON_CreateNumber(val));
}
void AddString(const std::string& name, const std::string& val) {
cJSON_AddItemToObject(obj(), name.c_str(), cJSON_CreateString(val.c_str()));
}
auto PrintUnformatted() -> std::string {
return cJSON_PrintUnformatted(obj());
}
};
// class JsonNumber : public JsonObject {
// public:
// JsonNumber(double val) { set_obj(cJSON_CreateNumber(val)); }
// };
// class JsonString : public JsonObject {
// public:
// JsonString(const std::string& s) { set_obj(cJSON_CreateString(s.c_str()));
// } JsonString(const char* s) { set_obj(cJSON_CreateString(s)); }
// };
#pragma clang diagnostic pop
} // namespace ballistica
#endif // BALLISTICA_GENERIC_JSON_H_

View File

@ -0,0 +1,38 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_GENERIC_LAMBDA_RUNNABLE_H_
#define BALLISTICA_GENERIC_LAMBDA_RUNNABLE_H_
#include "ballistica/generic/runnable.h"
namespace ballistica {
// (don't use this class directly; call NewLambdaRunnable below)
// from what I hear, heavy use of std::function can slow
// compiles down dramatically, so sticking to raw lambdas here
template <typename F>
class LambdaRunnable : public Runnable {
public:
explicit LambdaRunnable(F lambda) : lambda_(lambda) {}
void Run() override { lambda_(); }
private:
F lambda_;
};
// Call this to allocate and return a raw lambda runnable
template <typename F>
auto NewLambdaRunnable(const F& lambda) -> Object::Ref<Runnable> {
return Object::New<Runnable, LambdaRunnable<F>>(lambda);
}
// Same but returns the raw pointer instead of a ref;
// (used when passing across threads).
template <typename F>
auto NewLambdaRunnableRaw(const F& lambda) -> Runnable* {
return Object::NewDeferred<LambdaRunnable<F>>(lambda);
}
} // namespace ballistica
#endif // BALLISTICA_GENERIC_LAMBDA_RUNNABLE_H_

View File

@ -0,0 +1,49 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_GENERIC_REAL_TIMER_H_
#define BALLISTICA_GENERIC_REAL_TIMER_H_
#include "ballistica/ballistica.h"
#include "ballistica/core/object.h"
#include "ballistica/game/game.h"
#include "ballistica/generic/runnable.h"
namespace ballistica {
// Manages a timer which runs on real time and calls a
// 'HandleRealTimerExpired' method on the provided pointer.
template <typename T>
class RealTimer : public Object {
public:
RealTimer(millisecs_t length, bool repeat, T* delegate) {
assert(g_game);
assert(InGameThread());
timer_id_ = g_game->NewRealTimer(
length, repeat, Object::New<Runnable, Callback>(delegate, this));
}
void SetLength(uint32_t length) {
assert(InGameThread());
g_game->SetRealTimerLength(timer_id_, length);
}
~RealTimer() override {
assert(InGameThread());
g_game->DeleteRealTimer(timer_id_);
}
private:
class Callback : public Runnable {
public:
Callback(T* delegate, RealTimer<T>* timer)
: delegate_(delegate), timer_(timer) {}
void Run() override { delegate_->HandleRealTimerExpired(timer_); }
private:
RealTimer<T>* timer_;
T* delegate_;
};
int timer_id_;
};
} // namespace ballistica
#endif // BALLISTICA_GENERIC_REAL_TIMER_H_

View File

@ -0,0 +1,11 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/generic/runnable.h"
namespace ballistica {
auto Runnable::GetThreadOwnership() const -> Object::ThreadOwnership {
return ThreadOwnership::kNextReferencing;
}
} // namespace ballistica

View File

@ -0,0 +1,23 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_GENERIC_RUNNABLE_H_
#define BALLISTICA_GENERIC_RUNNABLE_H_
#include <string>
#include "ballistica/core/object.h"
namespace ballistica {
class Runnable : public Object {
public:
virtual void Run() = 0;
// these are used on lots of threads; lets
// lock to wherever we're first referenced
auto GetThreadOwnership() const -> ThreadOwnership override;
};
} // namespace ballistica
#endif // BALLISTICA_GENERIC_RUNNABLE_H_

View File

@ -0,0 +1,55 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/generic/timer.h"
#include "ballistica/generic/timer_list.h"
namespace ballistica {
Timer::Timer(TimerList* list_in, int id_in, TimerMedium current_time,
TimerMedium length_in, TimerMedium offset_in, int repeat_count_in)
: list_(list_in),
on_list_(false),
initial_(true),
dead_(false),
list_died_(false),
last_run_time_(current_time),
expire_time_(current_time + offset_in),
id_(id_in),
length_(length_in),
repeat_count_(repeat_count_in) {
list_->timer_count_total_++;
}
Timer::~Timer() {
// If the list is dead, dont touch the corpse.
if (!list_died_) {
if (on_list_) {
list_->PullTimer(id_);
} else {
// Should never be explicitly deleting the current client timer
// (it should just get marked as dead so the loop can kill it when
// re-submitted).
assert(list_->client_timer_ != this);
}
list_->timer_count_total_--;
}
}
void Timer::SetLength(TimerMedium l, bool set_start_time,
TimerMedium starttime) {
if (on_list_) {
assert(id_ != 0); // zero denotes "no-id"
Timer* t = list_->PullTimer(id_);
BA_PRECONDITION(t == this);
length_ = l;
if (set_start_time) last_run_time_ = starttime;
expire_time_ = last_run_time_ + length_;
list_->AddTimer(this);
} else {
length_ = l;
if (set_start_time) last_run_time_ = starttime;
}
}
} // namespace ballistica

View File

@ -0,0 +1,41 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_GENERIC_TIMER_H_
#define BALLISTICA_GENERIC_TIMER_H_
#include "ballistica/ballistica.h"
#include "ballistica/core/object.h"
#include "ballistica/generic/runnable.h"
namespace ballistica {
class Timer {
public:
auto id() const -> int { return id_; }
auto length() const -> TimerMedium { return length_; }
void SetLength(TimerMedium l, bool set_start_time = false,
TimerMedium starttime = 0);
private:
Timer(TimerList* list_in, int id_in, TimerMedium current_time,
TimerMedium length_in, TimerMedium offset_in, int repeat_count_in);
virtual ~Timer();
TimerList* list_{};
bool on_list_{};
Timer* next_{};
bool initial_{};
bool dead_{};
bool list_died_{};
TimerMedium last_run_time_{};
TimerMedium expire_time_{};
int id_{};
TimerMedium length_{};
int repeat_count_{};
Object::Ref<Runnable> runnable_;
// FIXME: Shouldn't have friend classes in different files.
friend class TimerList;
};
} // namespace ballistica
#endif // BALLISTICA_GENERIC_TIMER_H_

View File

@ -0,0 +1,271 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/generic/timer_list.h"
#include "ballistica/generic/runnable.h"
#include "ballistica/generic/timer.h"
namespace ballistica {
TimerList::TimerList() = default;
TimerList::~TimerList() {
Clear();
// Don't delete the client timer if one exists; just inform it that the list
// is dead.
if (client_timer_) {
client_timer_->list_died_ = true;
}
if (g_buildconfig.debug_build()) {
if (timer_count_active_ != 0) {
Log("Error: Invalid timerlist state on teardown.");
}
if (timer_count_inactive_ != 0) {
Log("Error: Invalid timerlist state on teardown.");
}
if (!((timer_count_total_ == 0)
|| (client_timer_ != nullptr && timer_count_total_ == 1))) {
Log("Error: Invalid timerlist state on teardown.");
}
}
}
void TimerList::Clear() {
assert(!are_clearing_);
are_clearing_ = true;
while (timers_) {
Timer* t = timers_;
t->on_list_ = false;
timer_count_active_--;
timers_ = t->next_;
delete t;
}
while (timers_inactive_) {
Timer* t = timers_inactive_;
t->on_list_ = false;
timer_count_inactive_--;
timers_inactive_ = t->next_;
delete t;
}
are_clearing_ = false;
}
// Pull a timer out of the list.
auto TimerList::PullTimer(int timer_id, bool remove) -> Timer* {
Timer* t = timers_;
Timer* p = nullptr;
while (t) {
if (t->id_ == timer_id) {
if (remove) {
if (p) {
p->next_ = t->next_;
} else {
timers_ = t->next_;
}
t->on_list_ = false;
timer_count_active_--;
}
return t;
}
p = t;
t = t->next_;
}
// Didn't find it. check the inactive list.
t = timers_inactive_;
p = nullptr;
while (t) {
if (t->id_ == timer_id) {
if (remove) {
if (p) {
p->next_ = t->next_;
} else {
timers_inactive_ = t->next_;
}
t->on_list_ = false;
timer_count_inactive_--;
}
return t;
}
p = t;
t = t->next_;
}
// Not on either list; only other possibility is the current client timer.
if (client_timer_ && client_timer_->id_ == timer_id) {
return client_timer_;
}
return nullptr;
}
void TimerList::Run(TimerMedium target_time) {
assert(!are_clearing_);
// Limit our runs to whats initially on the list so we don't spin all day if
// a timer resets itself to run immediately.
// FIXME - what if this timer kills one or more of the initially-expired ones
// ..that means it could potentially run more than once.. does it matter?
int expired_count = GetExpiredCount(target_time);
for (int timers_to_run = expired_count; timers_to_run > 0; timers_to_run--) {
Timer* t = GetExpiredTimer(target_time);
if (t) {
assert(!t->dead_);
try {
t->runnable_->Run();
} catch (const std::exception&) {
// If something went wrong, put our list back in order and propagate.
if (t->list_died_) {
delete t; // nothing is left but this timer
} else {
SubmitTimer(t);
}
throw;
}
// If this timer killed the list, stop; otherwise put it back and keep on
// trucking.
if (t->list_died_) {
delete t; // nothing is left but this timer
return;
} else {
SubmitTimer(t);
}
}
}
}
auto TimerList::GetExpiredCount(TimerMedium target_time) -> int {
assert(!are_clearing_);
Timer* t = timers_;
int count = 0;
while (t && t->expire_time_ <= target_time) {
count++;
t = t->next_;
}
return count;
}
// Returns the next expired timer. When done with the timer,
// return it to the list with Timer::submit()
// (this will either put it back in line or delete it)
auto TimerList::GetExpiredTimer(TimerMedium target_time) -> Timer* {
assert(!are_clearing_);
Timer* t;
if (timers_ != nullptr && timers_->expire_time_ <= target_time) {
t = timers_;
t->last_run_time_ = target_time;
timers_ = timers_->next_;
timer_count_active_--;
t->on_list_ = false;
// Exactly one timer at a time can be out in userland and not on
// any list - this is now that one.
assert(client_timer_ == nullptr);
client_timer_ = t;
return t;
}
return nullptr;
}
auto TimerList::NewTimer(TimerMedium current_time, TimerMedium length,
TimerMedium offset, int repeat_count,
const Object::Ref<Runnable>& runnable) -> Timer* {
assert(!are_clearing_);
auto* t = new Timer(this, next_timer_id_++, current_time, length, offset,
repeat_count);
t->runnable_ = runnable;
t = SubmitTimer(t);
return t;
}
auto TimerList::GetTimeToNextExpire(TimerMedium current_time) -> TimerMedium {
assert(!are_clearing_);
if (!timers_) {
return (TimerMedium)-1;
}
TimerMedium diff = timers_->expire_time_ - current_time;
return (diff < 0) ? 0 : diff;
}
auto TimerList::GetTimer(int id) -> Timer* {
assert(!are_clearing_);
assert(id != 0); // Zero denotes "no-id".
Timer* t = PullTimer(id, false);
return t->dead_ ? nullptr : t;
}
void TimerList::DeleteTimer(int timer_id) {
assert(timer_id != 0); // zero denotes "no-id"
Timer* t = PullTimer(timer_id);
if (t) {
// If its the client timer, just mark it as dead, so the client can still
// resubmit it without crashing.
if (client_timer_ == t) {
t->dead_ = true;
} else {
// Not in the client domain; kill it now.
delete t;
}
}
}
auto TimerList::SubmitTimer(Timer* t) -> Timer* {
assert(t->list_ == this);
assert(t->initial_ || t == client_timer_ || t->dead_);
// Aside from initial timer submissions, only the one client timer should be
// coming thru here.
if (!t->initial_) {
assert(client_timer_ == t);
client_timer_ = nullptr;
}
// If its a one-shot timer or is dead, kill it.
if ((t->repeat_count_ == 0 && !t->initial_) || t->dead_) {
delete t;
return nullptr;
} else {
// Its still alive. Shove it back in line and tell it to keep working.
if (!t->initial_ && t->repeat_count_ > 0) t->repeat_count_--;
t->initial_ = false;
// No drift.
if (explicit_bool(false)) {
t->expire_time_ = t->expire_time_ + t->length_;
} else {
// Drift.
t->expire_time_ = t->last_run_time_ + t->length_;
}
AddTimer(t);
return t;
}
}
void TimerList::AddTimer(Timer* t) {
assert(t && !t->on_list_);
// If its set to never go off, throw it on the inactive list.
if (t->length_ == -1) {
t->next_ = timers_inactive_;
timers_inactive_ = t;
timer_count_inactive_++;
} else {
Timer** list = &timers_;
// Go along till we find an expire time later than ourself.
while (*list != nullptr) {
if ((*list)->expire_time_ > t->expire_time_) break;
list = &((*list)->next_);
}
Timer* tmp = (*list);
(*list) = t;
t->next_ = tmp;
timer_count_active_++;
}
t->on_list_ = true;
}
} // namespace ballistica

View File

@ -0,0 +1,68 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_GENERIC_TIMER_LIST_H_
#define BALLISTICA_GENERIC_TIMER_LIST_H_
#include <cstdio>
#include <vector>
#include "ballistica/ballistica.h"
#include "ballistica/core/object.h"
namespace ballistica {
class TimerList {
public:
TimerList();
~TimerList();
// Run timers up to the provided target time.
void Run(TimerMedium target_time);
// Create a timer with provided runnable.
auto NewTimer(TimerMedium current_time, TimerMedium length,
TimerMedium offset, int repeat_count,
const Object::Ref<Runnable>& runnable) -> Timer*;
// Return a timer by its id, or nullptr if the timer no longer exists.
auto GetTimer(int id) -> Timer*;
// Delete a currently-queued timer via its id.
void DeleteTimer(int timer_id);
// Return the time until the next timer goes off.
// If no timers are present, -1 is returned.
auto GetTimeToNextExpire(TimerMedium current_time) -> TimerMedium;
// Return the active timer count. Note that this does not include the client
// timer (a timer returned via getExpiredTimer() but not yet re-submitted).
auto active_timer_count() const -> int { return timer_count_active_; }
auto empty() -> bool { return (timers_ == nullptr); }
void Clear();
private:
// Returns the next expired timer. When done with the timer,
// return it to the list with Timer::submit()
// (this will either put it back in line or delete it)
auto GetExpiredTimer(TimerMedium target_time) -> Timer*;
auto GetExpiredCount(TimerMedium target_time) -> int;
auto PullTimer(int timer_id, bool remove = true) -> Timer*;
auto SubmitTimer(Timer* t) -> Timer*;
void AddTimer(Timer* t);
int timer_count_active_ = 0;
int timer_count_inactive_ = 0;
int timer_count_total_ = 0;
Timer* client_timer_ = nullptr;
Timer* timers_ = nullptr;
Timer* timers_inactive_ = nullptr;
int next_timer_id_ = 1;
bool running_ = false;
bool are_clearing_ = false;
friend class Timer;
};
} // namespace ballistica
#endif // BALLISTICA_GENERIC_TIMER_LIST_H_

View File

@ -0,0 +1,449 @@
// Copyright (c) 2011-2020 Eric Froemling
// Derived from code licensed as follows:
/*
Basic UTF-8 manipulation routines
by Jeff Bezanson
placed in the public domain Fall 2005
This code is designed to provide the utilities you need to manipulate
UTF-8 as an internal string encoding. These functions do not perform the
error checking normally needed when handling UTF-8 data, so if you happen
to be from the Unicode Consortium you will want to flay me alive.
I do this because error checking can be performed at the boundaries (I/O),
with these routines reserved for higher performance on data known to be
valid.
*/
#include "ballistica/generic/utf8.h"
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#if _WIN32 || _WIN64
#include <malloc.h>
#else
#include <alloca.h>
#endif
namespace ballistica {
// Should tidy this up but don't want to risk breaking anything for now.
#pragma clang diagnostic push
#pragma ide diagnostic ignored "hicpp-signed-bitwise"
#pragma ide diagnostic ignored "bugprone-narrowing-conversions"
static const uint32_t offsetsFromUTF8[6] = {0x00000000UL, 0x00003080UL,
0x000E2080UL, 0x03C82080UL,
0xFA082080UL, 0x82082080UL};
static const char trailingBytesForUTF8[256] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5};
/* returns length of next utf-8 sequence */
auto u8_seqlen(const char* s) -> int {
return trailingBytesForUTF8[(unsigned int)(unsigned char)s[0]] + 1;
}
/* conversions without error checking
only works for valid UTF-8, i.e. no 5- or 6-byte sequences
srcsz = source size in bytes, or -1 if 0-terminated
sz = dest size in # of wide characters
returns # characters converted
dest will always be L'\0'-terminated, even if there isn't enough room
for all the characters.
if sz = srcsz+1 (i.e. 4*srcsz+4 bytes), there will always be enough space.
*/
auto u8_toucs(uint32_t* dest, int sz, const char* src, int srcsz) -> int {
uint32_t ch;
const char* src_end = src + srcsz;
int nb;
int i = 0;
while (i < sz - 1) {
nb = trailingBytesForUTF8[(unsigned char)*src]; // NOLINT(cert-str34-c)
if (srcsz == -1) {
if (*src == 0) goto done_toucs;
} else {
if (src + nb >= src_end) goto done_toucs;
}
ch = 0;
switch (nb) {
/* these fall through deliberately */
case 3: // NOLINT(bugprone-branch-clone)
ch += (unsigned char)*src++;
ch <<= 6;
case 2:
ch += (unsigned char)*src++;
ch <<= 6;
case 1:
ch += (unsigned char)*src++;
ch <<= 6;
case 0:
ch += (unsigned char)*src++;
default:
break;
}
ch -= offsetsFromUTF8[nb];
dest[i++] = ch;
}
done_toucs:
dest[i] = 0;
return i;
}
/* srcsz = number of source characters, or -1 if 0-terminated
sz = size of dest buffer in bytes
returns # characters converted
dest will only be '\0'-terminated if there is enough space. this is
for consistency; imagine there are 2 bytes of space left, but the next
character requires 3 bytes. in this case we could NUL-terminate, but in
general we can't when there's insufficient space. therefore this function
only NUL-terminates if all the characters fit, and there's space for
the NUL as well.
the destination string will never be bigger than the source string.
*/
auto u8_toutf8(char* dest, int sz, const uint32_t* src, int srcsz) -> int {
uint32_t ch;
int i = 0;
char* dest_end = dest + sz;
while (srcsz < 0 ? src[i] != 0 : i < srcsz) {
ch = src[i];
if (ch < 0x80) {
if (dest >= dest_end) return i;
*dest++ = (char)ch;
} else if (ch < 0x800) {
if (dest >= dest_end - 1) return i;
*dest++ = static_cast<char>((ch >> 6) | 0xC0);
*dest++ = static_cast<char>((ch & 0x3F) | 0x80);
} else if (ch < 0x10000) {
if (dest >= dest_end - 2) return i;
*dest++ = static_cast<char>((ch >> 12) | 0xE0);
*dest++ = static_cast<char>(((ch >> 6) & 0x3F) | 0x80);
*dest++ = static_cast<char>((ch & 0x3F) | 0x80);
} else if (ch < 0x110000) {
if (dest >= dest_end - 3) return i;
*dest++ = static_cast<char>((ch >> 18) | 0xF0);
*dest++ = static_cast<char>(((ch >> 12) & 0x3F) | 0x80);
*dest++ = static_cast<char>(((ch >> 6) & 0x3F) | 0x80);
*dest++ = static_cast<char>((ch & 0x3F) | 0x80);
}
i++;
}
if (dest < dest_end) *dest = '\0';
return i;
}
auto u8_wc_toutf8(char* dest, uint32_t ch) -> int {
if (ch < 0x80) {
dest[0] = (char)ch;
return 1;
}
if (ch < 0x800) {
dest[0] = static_cast<char>((ch >> 6) | 0xC0);
dest[1] = static_cast<char>((ch & 0x3F) | 0x80);
return 2;
}
if (ch < 0x10000) {
dest[0] = static_cast<char>((ch >> 12) | 0xE0);
dest[1] = static_cast<char>(((ch >> 6) & 0x3F) | 0x80);
dest[2] = static_cast<char>((ch & 0x3F) | 0x80);
return 3;
}
if (ch < 0x110000) {
dest[0] = static_cast<char>((ch >> 18) | 0xF0);
dest[1] = static_cast<char>(((ch >> 12) & 0x3F) | 0x80);
dest[2] = static_cast<char>(((ch >> 6) & 0x3F) | 0x80);
dest[3] = static_cast<char>((ch & 0x3F) | 0x80);
return 4;
}
return 0;
}
/* charnum => byte offset */
auto u8_offset(const char* str, int charnum) -> int {
int offs = 0;
while (charnum > 0 && str[offs]) {
(void)(isutf(str[++offs]) || isutf(str[++offs]) || isutf(str[++offs])
|| ++offs);
charnum--;
}
return offs;
}
/* byte offset => charnum */
auto u8_charnum(const char* s, int offset) -> int {
int charnum = 0, offs = 0;
while (offs < offset && s[offs]) {
(void)(isutf(s[++offs]) || isutf(s[++offs]) || isutf(s[++offs]) || ++offs);
charnum++;
}
return charnum;
}
/* number of characters */
auto u8_strlen(const char* s) -> int {
int count = 0;
int i = 0;
while (u8_nextchar(s, &i) != 0) {
count++;
}
return count;
}
auto u8_nextchar(const char* s, int* i) -> uint32_t {
uint32_t ch = 0;
size_t sz = 0;
do {
ch <<= 6;
ch += (unsigned char)s[(*i)];
sz++;
} while (s[*i] && (++(*i)) && !isutf(s[*i]));
ch -= offsetsFromUTF8[sz - 1];
return ch;
}
void u8_inc(const char* s, int* i) {
(void)(isutf(s[++(*i)]) || isutf(s[++(*i)]) || isutf(s[++(*i)]) || ++(*i));
}
void u8_dec(const char* s, int* i) {
(void)(isutf(s[--(*i)]) || isutf(s[--(*i)]) || isutf(s[--(*i)]) || --(*i));
}
auto octal_digit(char c) -> int { return (c >= '0' && c <= '7'); }
auto hex_digit(char c) -> int {
return ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')
|| (c >= 'a' && c <= 'f'));
}
/* assumes that src points to the character after a backslash
returns number of input characters processed */
auto u8_read_escape_sequence(char* str, uint32_t* dest) -> int {
uint32_t ch;
char digs[9] = "\0\0\0\0\0\0\0\0";
int dno = 0, i = 1;
ch = (uint32_t)str[0]; /* take literal character */ // NOLINT(cert-str34-c)
if (str[0] == 'n')
ch = L'\n';
else if (str[0] == 't')
ch = L'\t';
else if (str[0] == 'r')
ch = L'\r';
else if (str[0] == 'b')
ch = L'\b';
else if (str[0] == 'f')
ch = L'\f';
else if (str[0] == 'v')
ch = L'\v';
else if (str[0] == 'a')
ch = L'\a';
else if (octal_digit(str[0])) {
i = 0;
do {
digs[dno++] = str[i++];
} while (octal_digit(str[i]) && dno < 3);
ch = static_cast<uint32_t>(strtol(digs, nullptr, 8));
} else if (str[0] == 'x') {
while (hex_digit(str[i]) && dno < 2) {
digs[dno++] = str[i++];
}
if (dno > 0) ch = static_cast<uint32_t>(strtol(digs, nullptr, 16));
} else if (str[0] == 'u') {
while (hex_digit(str[i]) && dno < 4) {
digs[dno++] = str[i++];
}
if (dno > 0) ch = static_cast<uint32_t>(strtol(digs, nullptr, 16));
} else if (str[0] == 'U') {
while (hex_digit(str[i]) && dno < 8) {
digs[dno++] = str[i++];
}
if (dno > 0) ch = static_cast<uint32_t>(strtol(digs, nullptr, 16));
}
*dest = ch;
return i;
}
/* convert a string with literal \uxxxx or \Uxxxxxxxx characters to UTF-8
example: u8_unescape(mybuf, 256, "hello\\u220e")
note the double backslash is needed if called on a C string literal */
auto u8_unescape(char* buf, int sz, char* src) -> int {
int c = 0, amt;
uint32_t ch;
char temp[4];
while (*src && c < sz) {
if (*src == '\\') {
src++;
amt = u8_read_escape_sequence(src, &ch);
} else {
ch = (uint32_t)*src; // NOLINT(cert-str34-c)
amt = 1;
}
src += amt;
amt = u8_wc_toutf8(temp, ch);
if (amt > sz - c) break;
memcpy(&buf[c], temp, static_cast<size_t>(amt));
c += amt;
}
if (c < sz) buf[c] = '\0';
return c;
}
auto u8_escape_wchar(char* buf, int sz, uint32_t ch) -> int {
if (ch == L'\n')
return snprintf(buf, static_cast<size_t>(sz), "\\n");
else if (ch == L'\t')
return snprintf(buf, static_cast<size_t>(sz), "\\t");
else if (ch == L'\r')
return snprintf(buf, static_cast<size_t>(sz), "\\r");
else if (ch == L'\b')
return snprintf(buf, static_cast<size_t>(sz), "\\b");
else if (ch == L'\f')
return snprintf(buf, static_cast<size_t>(sz), "\\f");
else if (ch == L'\v')
return snprintf(buf, static_cast<size_t>(sz), "\\v");
else if (ch == L'\a')
return snprintf(buf, static_cast<size_t>(sz), "\\a");
else if (ch == L'\\')
return snprintf(buf, static_cast<size_t>(sz), "\\\\");
else if (ch < 32 || ch == 0x7f)
return snprintf(buf, static_cast<size_t>(sz), "\\x%hhX", (unsigned char)ch);
else if (ch > 0xFFFF)
return snprintf(buf, static_cast<size_t>(sz), "\\U%.8X", (uint32_t)ch);
else if (ch >= 0x80 && ch <= 0xFFFF)
return snprintf(buf, static_cast<size_t>(sz), "\\u%.4hX",
(unsigned short)ch);
return snprintf(buf, static_cast<size_t>(sz), "%c", (char)ch);
}
auto u8_escape(char* buf, int sz, char* src, int escape_quotes) -> int {
int c = 0, i = 0, amt;
while (src[i] && c < sz) {
if (escape_quotes && src[i] == '"') {
amt = snprintf(buf, static_cast<size_t>(sz - c), "\\\"");
i++;
} else {
amt = u8_escape_wchar(buf, sz - c, u8_nextchar(src, &i));
}
c += amt;
buf += amt;
}
if (c < sz) *buf = '\0';
return c;
}
auto u8_strchr(char* s, uint32_t ch, int* charn) -> char* {
int i = 0, lasti = 0;
uint32_t c;
*charn = 0;
while (s[i]) {
c = u8_nextchar(s, &i);
if (c == ch) {
return &s[lasti];
}
lasti = i;
(*charn)++;
}
return nullptr;
}
auto u8_memchr(char* s, uint32_t ch, size_t sz, int* charn) -> char* {
size_t i = 0, lasti = 0;
uint32_t c;
int csz;
*charn = 0;
while (i < sz) {
c = static_cast<uint32_t>(csz = 0);
do {
c <<= 6;
c += (unsigned char)s[i++];
csz++;
} while (i < sz && !isutf(s[i]));
c -= offsetsFromUTF8[csz - 1];
if (c == ch) {
return &s[lasti];
}
lasti = i;
(*charn)++;
}
return nullptr;
}
auto u8_is_locale_utf8(const char* locale) -> int {
/* this code based on libutf8 */
const char* cp = locale;
for (; *cp != '\0' && *cp != '@' && *cp != '+' && *cp != ','; cp++) {
if (*cp == '.') {
const char* encoding = ++cp;
for (; *cp != '\0' && *cp != '@' && *cp != '+' && *cp != ','; cp++)
;
if ((cp - encoding == 5 && !strncmp(encoding, "UTF-8", 5))
|| (cp - encoding == 4 && !strncmp(encoding, "utf8", 4)))
return 1; /* it's UTF-8 */
break;
}
}
return 0;
}
auto u8_vprintf(char* fmt, va_list ap) -> int {
char* buf;
uint32_t* wcs;
int sz{512};
buf = (char*)alloca(sz);
try_print:
int cnt = vsnprintf(buf, static_cast<size_t>(sz), fmt, ap);
if (cnt >= sz) {
buf = (char*)alloca(cnt - sz + 1);
sz = cnt + 1;
goto try_print;
}
wcs = (uint32_t*)alloca((cnt + 1) * sizeof(uint32_t));
cnt = u8_toucs(wcs, cnt + 1, buf, cnt);
printf("%ls", (wchar_t*)wcs);
return cnt;
}
auto u8_printf(char* fmt, ...) -> int {
int cnt;
va_list args;
va_start(args, fmt);
cnt = u8_vprintf(fmt, args);
va_end(args);
return cnt;
}
#pragma clang diagnostic pop
} // namespace ballistica

View File

@ -0,0 +1,85 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_GENERIC_UTF8_H_
#define BALLISTICA_GENERIC_UTF8_H_
#include <cstdarg>
#include "ballistica/ballistica.h"
// ericf note: i think this is cutef8?...
namespace ballistica {
/* is c the start of a utf8 sequence? */
#define isutf(c) (((c)&0xC0) != 0x80)
/* convert UTF-8 data to wide character */
auto u8_toucs(uint32_t* dest, int sz, const char* src, int srcsz) -> int;
/* the opposite conversion */
auto u8_toutf8(char* dest, int sz, const uint32_t* src, int srcsz) -> int;
/* single character to UTF-8 */
auto u8_wc_toutf8(char* dest, uint32_t ch) -> int;
/* character number to byte offset */
auto u8_offset(const char* str, int charnum) -> int;
/* byte offset to character number */
auto u8_charnum(const char* s, int offset) -> int;
/* return next character, updating an index variable */
auto u8_nextchar(const char* s, int* i) -> uint32_t;
/* move to next character */
void u8_inc(const char* s, int* i);
/* move to previous character */
void u8_dec(const char* s, int* i);
/* returns length of next utf-8 sequence */
auto u8_seqlen(const char* s) -> int;
/* assuming src points to the character after a backslash, read an
escape sequence, storing the result in dest and returning the number of
input characters processed */
auto u8_read_escape_sequence(char* src, uint32_t* dest) -> int;
/* given a wide character, convert it to an ASCII escape sequence stored in
buf, where buf is "sz" bytes. returns the number of characters output. */
auto u8_escape_wchar(char* buf, int sz, uint32_t ch) -> int;
/* convert a string "src" containing escape sequences to UTF-8 */
auto u8_unescape(char* buf, int sz, char* src) -> int;
/* convert UTF-8 "src" to ASCII with escape sequences.
if escape_quotes is nonzero, quote characters will be preceded by
backslashes as well. */
auto u8_escape(char* buf, int sz, char* src, int escape_quotes) -> int;
/* utility predicates used by the above */
auto octal_digit(char c) -> int;
auto hex_digit(char c) -> int;
/* return a pointer to the first occurrence of ch in s, or NULL if not
found. character index of found character returned in *charn. */
auto u8_strchr(char* s, uint32_t ch, int* charn) -> char*;
/* same as the above, but searches a buffer of a given size instead of
a NUL-terminated string. */
auto u8_memchr(char* s, uint32_t ch, size_t sz, int* charn) -> char*;
/* count the number of characters in a UTF-8 string */
auto u8_strlen(const char* s) -> int;
auto u8_is_locale_utf8(const char* locale) -> int;
/* printf where the format string and arguments may be in UTF-8.
you can avoid this function and just use ordinary printf() if the current
locale is UTF-8. */
auto u8_vprintf(char* fmt, va_list ap) -> int;
auto u8_printf(char* fmt, ...) -> int;
} // namespace ballistica
#endif // BALLISTICA_GENERIC_UTF8_H_

View File

@ -0,0 +1,639 @@
// Copyright (c) 2011-2020 Eric Froemling
#include "ballistica/generic/utils.h"
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <memory>
#include <sstream>
#include <string>
#include "ballistica/app/app_globals.h"
#include "ballistica/generic/base64.h"
#include "ballistica/generic/huffman.h"
#include "ballistica/generic/json.h"
#include "ballistica/generic/utf8.h"
#include "ballistica/math/vector3f.h"
#include "ballistica/platform/platform.h"
#include "ballistica/scene/scene.h"
// FIXME: Cleaner to add the lib to the project(s) instead?
#if BA_OSTYPE_WINDOWS
#pragma comment(lib, "Ws2_32.lib")
#endif
namespace ballistica {
#define USE_BAKED_RANDS 1
#if BA_OSTYPE_WINDOWS
#endif
#if USE_BAKED_RANDS
float Utils::precalc_rands_1[kPrecalcRandsCount] = {
0.00424972f, 0.0470216f, 0.545227f, 0.538243f, 0.214183f, 0.627205f,
0.194698f, 0.917583f, 0.468622f, 0.0779965f, 0.304211f, 0.773231f,
0.522742f, 0.378898f, 0.404598f, 0.468434f, 0.081512f, 0.408348f,
0.0808838f, 0.427364f, 0.226629f, 0.234887f, 0.516467f, 0.0457478f,
0.455418f, 0.194083f, 0.502244f, 0.0733989f, 0.458193f, 0.898715f,
0.624819f, 0.70762f, 0.759858f, 0.559276f, 0.956318f, 0.408562f,
0.206264f, 0.322909f, 0.293165f, 0.524073f, 0.407753f, 0.961242f,
0.278234f, 0.423968f, 0.631937f, 0.534858f, 0.842336f, 0.786993f,
0.934668f, 0.739984f, 0.968577f, 0.468159f, 0.804702f, 0.0686368f,
0.397594f, 0.60871f, 0.485322f, 0.907066f, 0.587516f, 0.364387f,
0.791611f, 0.899199f, 0.0186556f, 0.446891f, 0.0138f, 0.999024f,
0.556364f, 0.29821f, 0.23943f, 0.338024f, 0.157135f, 0.25299f,
0.791138f, 0.367175f, 0.584245f, 0.496136f, 0.358228f, 0.280143f,
0.538658f, 0.190721f, 0.656737f, 0.010905f, 0.520343f, 0.678249f,
0.930145f, 0.823978f, 0.457201f, 0.988418f, 0.854635f, 0.955912f,
0.0226999f, 0.183605f, 0.838141f, 0.210646f, 0.160344f, 0.111269f,
0.348488f, 0.648031f, 0.844362f, 0.65157f, 0.0598469f, 0.952439f,
0.265193f, 0.768256f, 0.773861f, 0.723251f, 0.53157f, 0.36183f,
0.485393f, 0.348683f, 0.551617f, 0.648207f, 0.656125f, 0.879799f,
0.0674501f, 0.000782927f, 0.607129f, 0.116035f, 0.67095f, 0.692934f,
0.276618f, 0.137535f, 0.771033f, 0.278625f, 0.686023f, 0.873823f,
0.254666f, 0.75378f};
float Utils::precalc_rands_2[kPrecalcRandsCount] = {
0.425019f, 0.29261f, 0.623541f, 0.241628f, 0.772656f, 0.434116f,
0.295335f, 0.814317f, 0.122326f, 0.887651f, 0.873536f, 0.692463f,
0.730894f, 0.142115f, 0.0722184f, 0.977652f, 0.971393f, 0.111517f,
0.41341f, 0.699999f, 0.955932f, 0.746667f, 0.267962f, 0.883952f,
0.202871f, 0.952115f, 0.221069f, 0.616162f, 0.842076f, 0.705628f,
0.332754f, 0.974675f, 0.940277f, 0.756059f, 0.831943f, 0.70631f,
0.674705f, 0.13903f, 0.22751f, 0.0875125f, 0.101364f, 0.593826f,
0.271567f, 0.63593f, 0.970994f, 0.359381f, 0.147583f, 0.987353f,
0.960315f, 0.904639f, 0.874661f, 0.352573f, 0.630782f, 0.578075f,
0.364932f, 0.588095f, 0.799978f, 0.0502811f, 0.379093f, 0.252171f,
0.598992f, 0.843808f, 0.544584f, 0.895444f, 0.935885f, 0.592526f,
0.810681f, 0.0200064f, 0.0986983f, 0.164623f, 0.975185f, 0.0102097f,
0.648763f, 0.114897f, 0.400273f, 0.549732f, 0.732205f, 0.363931f,
0.223837f, 0.4427f, 0.770981f, 0.280827f, 0.407232f, 0.323108f,
0.9429f, 0.594368f, 0.175995f, 0.34f, 0.857507f, 0.016013f,
0.516969f, 0.847756f, 0.638805f, 0.324338f, 0.897038f, 0.0950314f,
0.0460401f, 0.449791f, 0.189096f, 0.931966f, 0.846644f, 0.64728f,
0.096389f, 0.075902f, 0.27798f, 0.673576f, 0.102553f, 0.275159f,
0.00170948f, 0.319388f, 0.0328678f, 0.411649f, 0.496922f, 0.778794f,
0.634341f, 0.158655f, 0.0157559f, 0.195268f, 0.663882f, 0.148622f,
0.118159f, 0.552174f, 0.757064f, 0.854851f, 0.991449f, 0.349681f,
0.17858f, 0.774876f};
float Utils::precalc_rands_3[kPrecalcRandsCount] = {
0.29369f, 0.894838f, 0.857948f, 0.04309f, 0.0296678f, 0.180115f,
0.694884f, 0.227017f, 0.936936f, 0.746493f, 0.511976f, 0.231185f,
0.1333f, 0.524805f, 0.774586f, 0.395971f, 0.206664f, 0.274414f,
0.178939f, 0.88643f, 0.346536f, 0.22934f, 0.635988f, 0.589186f,
0.652835f, 0.195603f, 0.504794f, 0.831229f, 0.769911f, 0.494712f,
0.60128f, 0.367987f, 0.239279f, 0.0791311f, 0.469948f, 0.948189f,
0.760893f, 0.670452f, 0.753765f, 0.822003f, 0.628783f, 0.432039f,
0.226478f, 0.0678665f, 0.497384f, 0.110421f, 0.428975f, 0.446298f,
0.00813589f, 0.2634f, 0.434728f, 0.693152f, 0.547276f, 0.702469f,
0.407723f, 0.11742f, 0.235373f, 0.0738137f, 0.410148f, 0.231855f,
0.256911f, 0.879873f, 0.818198f, 0.73404f, 0.423038f, 0.577114f,
0.116636f, 0.247292f, 0.822178f, 0.817466f, 0.940992f, 0.593788f,
0.751732f, 0.0681611f, 0.38832f, 0.352672f, 0.174289f, 0.582884f,
0.0338663f, 0.460085f, 0.869757f, 0.854794f, 0.35513f, 0.477297f,
0.31343f, 0.545157f, 0.943892f, 0.383522f, 0.121732f, 0.131018f,
0.690497f, 0.231025f, 0.395681f, 0.144711f, 0.521456f, 0.192024f,
0.796611f, 0.64258f, 0.13998f, 0.560008f, 0.549709f, 0.831634f,
0.010101f, 0.684939f, 0.00884889f, 0.796426f, 0.603282f, 0.591985f,
0.731204f, 0.950351f, 0.408559f, 0.592352f, 0.76991f, 0.196648f,
0.376926f, 0.508574f, 0.809908f, 0.862359f, 0.863431f, 0.884588f,
0.895885f, 0.391311f, 0.976098f, 0.473118f, 0.286659f, 0.0946781f,
0.402437f, 0.347471f};
#else // USE_BAKED_RANDS
float Utils::precalc_rands_1[kPrecalcRandsCount];
float Utils::precalc_rands_2[kPrecalcRandsCount];
float Utils::precalc_rands_3[kPrecalcRandsCount];
#endif // USE_BAKED_RANDS
Utils::Utils() {
// Is this gonna be consistent cross-platform?... :-/
srand(543); // NOLINT
// Test our static-type-name functionality.
// This code runs at compile time and extracts human readable type names using
// __PRETTY_FUNCTION__ type functionality. However, it is dependent on
// specific compiler output and so could break easily if anything changes.
// Here we add some compile-time checks to alert us if that happens.
// Remember that results can vary per compiler; make sure we match
// one of the expected formats.
static_assert(static_type_name_constexpr<decltype(g_app_globals)>()
== "ballistica::AppGlobals *"
|| static_type_name_constexpr<decltype(g_app_globals)>()
== "ballistica::AppGlobals*"
|| static_type_name_constexpr<decltype(g_app_globals)>()
== "class ballistica::AppGlobals*");
Object::Ref<Node> testnode{};
static_assert(
static_type_name_constexpr<decltype(testnode)>()
== "ballistica::Object::Ref<ballistica::Node>"
|| static_type_name_constexpr<decltype(testnode)>()
== "class ballistica::Object::Ref<class ballistica::Node>");
// int testint{};
// static_assert(static_type_name_constexpr<decltype(testint)>() == "int");
// If anything above breaks, enable this code to debug/fix it.
// This will print a calculated type name as well as the full string
// it was parsed from. Use this to adjust the filtering as necessary so
// the resulting type name matches what is expected.
if (explicit_bool(false)) {
Log("static_type_name check; name is '"
+ static_type_name<decltype(testnode)>() + "' debug_full is '"
+ static_type_name<decltype(testnode)>(true) + "'");
}
// We now bake these in so they match across platforms...
#if USE_BAKED_RANDS
#else
// set up our precalculated rand vals
for (int i = 0; i < kPrecalcRandsCount; i++) {
precalc_rands_1[i] = static_cast<float>(rand()) / RAND_MAX; // NOLINT
precalc_rands_2[i] = static_cast<float>(rand()) / RAND_MAX; // NOLINT
precalc_rands_3[i] = static_cast<float>(rand()) / RAND_MAX; // NOLINT
}
#endif
huffman_ = std::make_unique<Huffman>();
}
Utils::~Utils() = default;
auto Utils::StringReplaceOne(std::string* target, const std::string& key,
const std::string& replacement) -> bool {
assert(target != nullptr);
size_t pos = target->find(key);
if (pos != std::string::npos) {
target->replace(pos, key.size(), replacement);
return true;
}
return false;
}
// from https://stackoverflow.com/questions/5343190/
// how-do-i-replace-all-instances-of-a-string-with-another-string/14678800
auto Utils::StringReplaceAll(std::string* target, const std::string& key,
const std::string& replacement) -> void {
assert(target != nullptr);
if (key.empty()) {
return;
}
std::string ws_ret;
ws_ret.reserve(target->length());
size_t start_pos = 0, pos;
while ((pos = target->find(key, start_pos)) != std::string::npos) {
ws_ret += target->substr(start_pos, pos - start_pos);
ws_ret += replacement;
pos += key.length();
start_pos = pos;
}
ws_ret += target->substr(start_pos);
target->swap(ws_ret); // faster than str = ws_ret;
}
auto Utils::IsValidUTF8(const std::string& val) -> bool {
std::string out = Utils::GetValidUTF8(val.c_str(), "bsivu8");
return (out == val);
}
static auto utf8_check_is_valid(const std::string& string) -> bool {
int c, i, ix, n, j;
for (i = 0, ix = static_cast<int>(string.length()); i < ix; i++) {
c = (unsigned char)string[i];
// if (c==0x09 || c==0x0a || c==0x0d
// || (0x20 <= c && c <= 0x7e) ) n = 0; // is_printable_ascii
if (0x00 <= c && c <= 0x7f) {
n = 0; // 0bbbbbbb
} else if ((c & 0xE0) == 0xC0) { // NOLINT
n = 1; // 110bbbbb
} else if (c == 0xed && i < (ix - 1)
&& ((unsigned char)string[i + 1] & 0xa0) == 0xa0) { // NOLINT
return false; // U+d800 to U+dfff
} else if ((c & 0xF0) == 0xE0) { // NOLINT
n = 2; // 1110bbbb
} else if ((c & 0xF8) == 0xF0) { // NOLINT
n = 3; // 11110bbb
} else {
// else if (($c & 0xFC) == 0xF8)
// n=4; // 111110bb //byte 5, unnecessary in 4 byte UTF-8
// else if (($c & 0xFE) == 0xFC)
// n=5; // 1111110b //byte 6, unnecessary in 4 byte UTF-8
return false;
}
for (j = 0; j < n && i < ix; j++) { // n bytes matching 10bbbbbb follow ?
// NOLINTNEXTLINE
if ((++i == ix) || (((unsigned char)string[i] & 0xC0) != 0x80)) {
return false;
}
}
}
return true;
}
// added by ericf from http://stackoverflow.com/questions/17316506/
// strip-invalid-utf8-from-string-in-c-c
// static std::string correct_non_utf_8(std::string *str) {
auto Utils::GetValidUTF8(const char* str, const char* loc) -> std::string {
int i, f_size = static_cast<int>(strlen(str));
unsigned char c, c2 = 0, c3, c4;
std::string to;
to.reserve(static_cast<size_t>(f_size));
// ok, it seems we're somehow letting some funky utf8 through that's
// causing crashes.. for now lets try this all-or-nothing func and return
// ascii only if it fails
if (!utf8_check_is_valid(str)) {
// now strip out anything but normal ascii...
for (i = 0; i < f_size; i++) {
c = (unsigned char)(str)[i];
if (c < 127) { // normal ASCII
to.append(1, c);
}
}
// phone home a few times for bad strings
static int logged_count = 0;
if (logged_count < 10) {
std::string log_str;
for (i = 0; i < f_size; i++) {
c = (unsigned char)(str)[i];
log_str += std::to_string(static_cast<int>(c));
if (i + 1 < f_size) {
log_str += ',';
}
}
logged_count++;
Log("GOT INVALID UTF8 SEQUENCE: (" + log_str + "); RETURNING '" + to
+ "'; LOC '" + loc + "'");
}
} else {
for (i = 0; i < f_size; i++) {
c = (unsigned char)(str)[i];
if (c < 32) { // control char
if (c == 9 || c == 10 || c == 13) { // allow only \t \n \r
to.append(1, c);
}
continue;
} else if (c < 127) { // normal ASCII
to.append(1, c);
continue;
} else if (c < 160) {
// control char (nothing should be defined here either
// ASCI, ISO_8859-1 or UTF8, so skipping)
if (c2 == 128) { // fix microsoft mess, add euro
to.append(1, (unsigned char)(226));
to.append(1, (unsigned char)(130));
to.append(1, (unsigned char)(172));
}
if (c2 == 133) { // fix IBM mess, add NEL = \n\r
to.append(1, 10);
to.append(1, 13);
}
continue;
} else if (c < 192) { // invalid for UTF8, converting ASCII
to.append(1, (unsigned char)194);
to.append(1, c);
continue;
} else if (c < 194) { // invalid for UTF8, converting ASCII
to.append(1, (unsigned char)195);
to.append(1, c - 64);
continue;
} else if (c < 224 && i + 1 < f_size) { // possibly 2byte UTF8
c2 = (unsigned char)(str)[i + 1];
if (c2 > 127 && c2 < 192) { // valid 2byte UTF8
if (c == 194 && c2 < 160) { // control char, skipping
} else {
to.append(1, c);
to.append(1, c2);
}
i++;
continue;
}
} else if (c < 240 && i + 2 < f_size) { // possibly 3byte UTF8
c2 = (unsigned char)(str)[i + 1];
c3 = (unsigned char)(str)[i + 2];
if (c2 > 127 && c2 < 192 && c3 > 127 && c3 < 192) { // valid 3byte UTF8
to.append(1, c);
to.append(1, c2);
to.append(1, c3);
i += 2;
continue;
}
} else if (c < 245 && i + 3 < f_size) { // possibly 4byte UTF8
c2 = (unsigned char)(str)[i + 1];
c3 = (unsigned char)(str)[i + 2];
c4 = (unsigned char)(str)[i + 3];
if (c2 > 127 && c2 < 192 && c3 > 127 && c3 < 192 && c4 > 127
&& c4 < 192) {
// valid 4byte UTF8
to.append(1, c);
to.append(1, c2);
to.append(1, c3);
to.append(1, c4);
i += 3;
continue;
}
}
// invalid UTF8, converting ASCII
// (c>245 || string too short for multi-byte))
to.append(1, (unsigned char)195);
to.append(1, c - 64);
}
}
return to;
}
auto Utils::UTF8StringLength(const char* val) -> int {
std::string valid_str = GetValidUTF8(val, "gusl1");
return u8_strlen(valid_str.c_str());
}
auto Utils::GetUTF8Value(const char* c) -> uint32_t {
int offset = 0;
uint32_t val = u8_nextchar(c, &offset);
// Hack: allow showing euro even if we don't support unicode font rendering.
if (!g_buildconfig.enable_os_font_rendering()) {
if (val == 8364) {
val = 0xE000;
}
}
return val;
}
auto Utils::UTF8FromUnicode(std::vector<uint32_t> unichars) -> std::string {
int buffer_size = static_cast<int>(unichars.size() * 4 + 1);
// at most 4 chars per unichar plus ending zero
std::vector<char> buffer(static_cast<size_t>(buffer_size));
int len = u8_toutf8(buffer.data(), buffer_size, unichars.data(),
static_cast<int>(unichars.size()));
assert(len == unichars.size());
buffer.resize(strlen(buffer.data()) + 1);
return buffer.data();
}
auto Utils::UnicodeFromUTF8(const std::string& s_in, const char* loc)
-> std::vector<uint32_t> {
std::string s = GetValidUTF8(s_in.c_str(), loc);
// worst case every char is a character (plus trailing 0)
std::vector<uint32_t> vals(s.size() + 1);
int converted = u8_toucs(&vals[0], static_cast<int>(vals.size()), s.c_str(),
static_cast<int>(s.size()));
vals.resize(static_cast<size_t>(converted));
return vals;
}
auto Utils::UTF8FromUnicodeChar(uint32_t c) -> std::string {
char buffer[10];
u8_toutf8(buffer, sizeof(buffer), &c, 1);
return buffer;
}
void Utils::AdvanceUTF8(const char** c) {
int offset = 0;
u8_nextchar(*c, &offset);
*c += offset;
}
auto Utils::GetJSONString(const char* s) -> std::string {
std::string str;
cJSON* str_obj = cJSON_CreateString(s);
char* str_buffer = cJSON_PrintUnformatted(str_obj);
str = str_buffer;
free(str_buffer);
cJSON_Delete(str_obj);
return str;
}
auto Utils::PtrToString(const void* val) -> std::string {
char buffer[128];
snprintf(buffer, sizeof(buffer), "%p", val);
return buffer;
}
static const char* g_default_random_names[] = {
"Flopsy", "Skippy", "Boomer", "Jolly", "Zeus", "Garth",
"Dizzy", "Mullet", "Ogre", "Ginger", "Nippy", "Murphy",
"Crom", "Sparky", "Wedge", "Arthur", "Benji", "Pan",
"Wallace", "Hamish", "Luke", "Cowboy", "Uncas", "Magua",
"Robin", "Lancelot", "Mad Dog", "Maximus", "Leonidas", "Don Quixote",
"Beowulf", "Gilgamesh", "Conan", "Cicero", "Elmer", "Flynn",
"Duck", "Uther", "Darkness", "Sunshine", "Willy", "Elvis",
"Dolph", "Rico", "Magoogan", "Willow", "Rose", "Egg",
"Thunder", "Jack", "Dude", "Walter", "Donny", "Larry",
"Chunk", "Socrates", nullptr};
static std::list<std::string>* g_random_names_list = nullptr;
auto Utils::GetRandomNameList() -> const std::list<std::string>& {
assert(InGameThread());
if (!g_random_names_list) {
// this will init the list with our default english names
SetRandomNameList(std::list<std::string>(1, "DEFAULT_NAMES"));
}
return *g_random_names_list;
}
void Utils::SetRandomNameList(const std::list<std::string>& custom_names) {
assert(InGameThread());
if (!g_random_names_list) {
g_random_names_list = new std::list<std::string>;
} else {
g_random_names_list->clear();
}
bool add_default_names = false;
if (custom_names.empty()) {
add_default_names = true;
}
for (const auto& custom_name : custom_names) {
if (custom_name == "DEFAULT_NAMES") {
add_default_names = true;
} else {
g_random_names_list->push_back(custom_name);
}
}
if (add_default_names) {
for (const char** c = g_default_random_names; *c != nullptr; c++) {
g_random_names_list->push_back(*c);
}
}
}
#define HEXVAL(x) ('0' + (x) + ((x) > 9u) * 7u)
static auto ToHex(const std::string& s_in) -> std::string {
uint32_t s_size = static_cast<int>(s_in.size());
std::string s_out;
s_out.resize(static_cast<size_t>(s_size) * 2);
for (uint32_t i = 0; i < s_size; i++) {
s_out[i * 2] =
static_cast<char>(HEXVAL((static_cast<uint32_t>(s_in[i])) >> 4u));
s_out[i * 2 + 1] =
static_cast<char>(HEXVAL((static_cast<uint32_t>(s_in[i]) & 15u)));
}
return s_out;
}
#undef HEXVAL
static auto FromHex(const std::string& s_in) -> std::string {
int s_size = static_cast<int>(s_in.size());
BA_PRECONDITION(s_size % 2 == 0);
s_size /= 2;
std::string s_out;
s_out.resize(static_cast<size_t>(s_size));
for (int i = 0; i < s_size; i++) {
auto val = (uint32_t)s_in[i * 2]; // NOLINT(cert-str34-c)
if (val >= '0' && val <= '9') {
s_out[i] = static_cast<char>((val - '0') << 4u);
} else if (val >= 'A' && val <= 'F') {
s_out[i] = static_cast<char>((10u + (val - 'A')) << 4u);
} else {
throw Exception();
}
val = (uint32_t)s_in[i * 2 + 1]; // NOLINT(cert-str34-c)
if (val >= '0' && val <= '9') {
s_out[i] =
static_cast<char>(static_cast<uint32_t>(s_out[i]) | (val - '0'));
} else if (val >= 'A' && val <= 'F') {
s_out[i] = static_cast<char>(static_cast<uint32_t>(s_out[i])
| (10 + (val - 'A')));
} else {
throw Exception();
}
}
return s_out;
}
static auto EncryptDecrypt(const std::string& to_encrypt) -> std::string {
assert(g_platform);
const char* key = g_platform->GetUniqueDeviceIdentifier().c_str();
int key_size =
static_cast<int>(g_platform->GetUniqueDeviceIdentifier().size());
std::string output = to_encrypt;
for (size_t i = 0; i < to_encrypt.size(); i++) {
output[i] = to_encrypt[i] ^ key[i % (key_size)]; // NOLINT
}
return output;
}
static auto EncryptDecryptCustom(const std::string& to_encrypt,
const std::string& key_in) -> std::string {
assert(g_platform);
const char* key = key_in.c_str();
int key_size = static_cast<int>(key_in.size());
std::string output = to_encrypt;
for (size_t i = 0; i < to_encrypt.size(); i++) {
output[i] = to_encrypt[i] ^ key[i % (key_size)]; // NOLINT
}
return output;
}
static auto PublicEncryptDecrypt(const std::string& to_encrypt) -> std::string {
std::string key_str = "create an account"; // A non-key-looking key.
const char* key = key_str.c_str();
int key_size = static_cast<int>(key_str.size());
std::string output = to_encrypt;
for (size_t i = 0; i < to_encrypt.size(); i++)
output[i] = to_encrypt[i] ^ key[i % (key_size)]; // NOLINT
return output;
}
auto Utils::LocalEncrypt(const std::string& s_in) -> std::string {
return ToHex(EncryptDecrypt(s_in));
}
auto Utils::LocalEncrypt2(const std::string& s_in) -> std::string {
std::string s = EncryptDecrypt(s_in);
return base64_encode((const unsigned char*)s.c_str(),
static_cast<int>(s.size()));
}
auto Utils::EncryptCustom(const std::string& s_in, const std::string& key)
-> std::string {
std::string s = EncryptDecryptCustom(s_in, key);
return base64_encode((const unsigned char*)s.c_str(),
static_cast<int>(s.size()));
}
auto Utils::LocalDecrypt(const std::string& s_in) -> std::string {
return EncryptDecrypt(FromHex(s_in));
}
auto Utils::LocalDecrypt2(const std::string& s_in) -> std::string {
return EncryptDecrypt(base64_decode(s_in));
}
auto Utils::DecryptCustom(const std::string& s_in, const std::string& key)
-> std::string {
return EncryptDecryptCustom(base64_decode(s_in), key);
}
auto Utils::PublicEncrypt(const std::string& s_in) -> std::string {
return ToHex(PublicEncryptDecrypt(s_in));
}
auto Utils::PublicDecrypt(const std::string& s_in) -> std::string {
return PublicEncryptDecrypt(FromHex(s_in));
}
auto Utils::PublicEncrypt2(const std::string& s_in) -> std::string {
std::string s = PublicEncryptDecrypt(s_in);
return base64_encode((const unsigned char*)s.c_str(),
static_cast<int>(s.size()));
}
auto Utils::PublicDecrypt2(const std::string& s_in) -> std::string {
return PublicEncryptDecrypt(base64_decode(s_in));
}
auto Utils::Sphrand(float radius) -> Vector3f {
while (true) {
float x = RandomFloat();
float y = RandomFloat();
float z = RandomFloat();
x = -1.0f + x * 2.0f;
y = -1.0f + y * 2.0f;
z = -1.0f + z * 2.0f;
if (x * x + y * y + z * z <= 1.0f) {
return {x * radius, y * radius, z * radius};
}
}
}
auto Utils::FileToString(const std::string& file_name) -> std::string {
std::ifstream file_stream{file_name};
if (file_stream.fail()) {
throw Exception("Error opening file for reading: '" + file_name + "'");
}
std::ostringstream str_stream{};
file_stream >> str_stream.rdbuf();
if (file_stream.fail() && !file_stream.eof()) {
throw Exception("Error reading file: '" + file_name + "'");
}
return str_stream.str();
}
static void WaitThenDie(millisecs_t wait, const std::string& action) {
Platform::SleepMS(wait);
throw std::runtime_error("Timed out waiting for " + action + "; aborting.");
}
void Utils::StartSuicideTimer(const std::string& action, millisecs_t delay) {
if (!g_app_globals->started_suicide) {
new std::thread(WaitThenDie, delay, action);
g_app_globals->started_suicide = true;
}
}
auto Utils::BaseName(const std::string& val) -> std::string {
const char* c = val.c_str();
const char* lastvalid = c;
while (*c != 0) {
if (*c == '/' || *c == '\\') {
lastvalid = c + 1;
}
++c;
}
return lastvalid;
}
} // namespace ballistica

View File

@ -0,0 +1,387 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_GENERIC_UTILS_H_
#define BALLISTICA_GENERIC_UTILS_H_
#include <algorithm>
#include <list>
#include <memory>
#include <string>
#include <vector>
// Need platform-specific headers here so we can inline calls to htonl/etc.
// (perhaps should move those functions to their own file?)
#if BA_OSTYPE_WINDOWS
#include <winsock2.h>
#else
#include <netinet/in.h>
#endif
#include <cstring>
#include "ballistica/ballistica.h"
namespace ballistica {
const int kPrecalcRandsCount = 128;
/// A holding tank for miscellaneous functionality not extensive enough to
/// warrant its own class. When possible, things should be moved out of here
/// into more organized locations.
class Utils {
public:
Utils();
~Utils();
static auto BaseName(const std::string& val) -> std::string;
static auto PtrToString(const void* val) -> std::string;
// This should probably live elsewhere...
static auto GetRandomNameList() -> const std::list<std::string>&;
static void SetRandomNameList(const std::list<std::string>& names);
static auto UnicodeFromUTF8(const std::string& s, const char* loc)
-> std::vector<uint32_t>;
static auto UTF8FromUnicode(std::vector<uint32_t> unichars) -> std::string;
static auto UTF8FromUnicodeChar(uint32_t c) -> std::string;
static auto UTF8StringLength(const char* val) -> int;
/// Start a timer to kill the app after the set length of time.
/// Use this during shutdown or when trying to send a crash-report before
/// dying just to ensure we don't hang indefinitely.
static void StartSuicideTimer(const std::string& action, millisecs_t delay);
/// Replace a single occurrence of key with replacement in the target string.
/// Returns whether a replacement occurred.
static auto StringReplaceOne(std::string* target, const std::string& key,
const std::string& replacement) -> bool;
static auto StringReplaceAll(std::string* target, const std::string& key,
const std::string& replacement) -> void;
/// Strip out or corrects invalid utf8.
/// This is run under the hood for all the above calls but in some cases
/// (such as the calls below) you may want to run it by hand.
/// Loc is included in debug log output if invalid utf8 is passed in.
static auto GetValidUTF8(const char* str, const char* loc) -> std::string;
/// Use this for debugging (not optimized for speed).
/// Currently just runs GetValidUTF8 and compares
/// results to original to see if anything got changed.
static auto IsValidUTF8(const std::string& val) -> bool;
// Escape a string so it can be embedded as part of a flattened json string.
static auto GetJSONString(const char* s) -> std::string;
// IMPORTANT - These run on 'trusted' utf8 - make sure you've run
// GetValidUTF8 on any data you pass to these
static auto GetUTF8Value(const char* s) -> uint32_t;
static void AdvanceUTF8(const char** s);
/* The following code uses bitwise operators to determine
if an unsigned integer, x, is a power of two. If x is a power of two,
x is represented in binary with only a single bit; therefore, subtraction
by one removes that bit and flips all the lower-order bits. The bitwise
and <http://www.cprogramming.com/tutorial/bitwise.html>
then effectively checks to see if any bit is the
same. If not, then it's a power of two.*/
static inline auto IsPowerOfTwo(unsigned int x) -> int {
return !((x - 1) & x);
}
// Yes this stuff should technically be using unsigned values for the
// bitwise stuff but I'm not brave enough to convert it at the moment.
// Made a quick attempt and everything blew up.
#pragma clang diagnostic push
#pragma ide diagnostic ignored "hicpp-signed-bitwise"
#pragma ide diagnostic ignored "bugprone-narrowing-conversions"
static inline auto FloatToHalfI(uint32_t i) -> uint16_t {
int s = (i >> 16) & 0x00008000; // NOLINT
int e = ((i >> 23) & 0x000000ff) - (127 - 15); // NOLINT
int m = i & 0x007fffff; // NOLINT
if (e <= 0) {
if (e < -10) {
return 0;
}
m = (m | 0x00800000) >> (1 - e);
return static_cast<uint16_t>(s | (m >> 13));
} else if (e == 0xff - (127 - 15)) {
if (m == 0) {
// Inf
return static_cast<uint16_t>(s | 0x7c00);
} else {
// NAN
m >>= 13;
return static_cast<uint16_t>(s | 0x7c00 | m | (m == 0));
}
} else {
if (e > 30) {
// Overflow
return static_cast<uint16_t>(s | 0x7c00);
}
return static_cast<uint16_t>(s | (e << 10) | (m >> 13));
}
}
static inline auto FloatToHalf(float i) -> uint16_t {
union {
float f;
uint32_t i;
} v{};
v.f = i;
return FloatToHalfI(v.i);
}
static inline auto HalfToFloatI(uint16_t y) -> uint32_t {
int s = (y >> 15) & 0x00000001;
int e = (y >> 10) & 0x0000001f;
int m = y & 0x000003ff;
if (e == 0) {
if (m == 0) { // Plus or minus zero
return static_cast<uint32_t>(s << 31);
} else { // Denormalized number -- renormalize it
while (!(m & 0x00000400)) {
m <<= 1;
e -= 1;
}
e += 1;
m &= ~0x00000400;
}
} else if (e == 31) {
if (m == 0) { // Inf
return static_cast<uint32_t>((s << 31) | 0x7f800000);
} else { // NaN
return static_cast<uint32_t>((s << 31) | 0x7f800000 | (m << 13));
}
}
e = e + (127 - 15);
m = m << 13;
return static_cast<uint32_t>((s << 31) | (e << 23) | m);
}
static inline auto HalfToFloat(uint16_t y) -> float {
union {
float f;
uint32_t i;
} v{};
v.i = HalfToFloatI(y);
return v.f;
}
// Value embedding/extracting in buffers.
// Note to self:
// Whenever its possible to do so cleanly, we should migrate to storing
// everything in little-endian and kill off the NBO versions of these.
// I don't anticipate having to run on big-endian hardware anytime soon
// and it'll save us a few cycles. (plus things are a sloppy mix of
// network-byte-ordered and native-ordered as it stands now).
/// Embed a single bool in a buffer.
static inline void EmbedBool(char** b, bool i) {
**b = i; // NOLINT
(*b)++;
}
/// Embed up to 8 bools in a buffer (in a single byte) use ExtractBools to
/// pull them out.
static inline void EmbedBools(char** b, bool i1, bool i2 = false,
bool i3 = false, bool i4 = false,
bool i5 = false, bool i6 = false,
bool i7 = false, bool i8 = false) {
**b = uint8_t(i1) | (uint8_t(i2) << 1) | (uint8_t(i3) << 2) // NOLINT
| (uint8_t(i4) << 3) | (uint8_t(i5) << 4) // NOLINT
| (uint8_t(i6) << 5) | (uint8_t(i7) << 6) // NOLINT
| (uint8_t(i8) << 7); // NOLINT
(*b)++;
}
static inline void EmbedInt8(char** b, int8_t i) {
**b = i;
(*b)++;
}
/// Embed a 2 byte int (short) into a buffer in network byte order.
static inline void EmbedInt16NBO(char** b, int16_t i) {
i = htons(i);
memcpy(*b, &i, sizeof(i));
*b += 2;
}
/// Embed a 4 byte int into a buffer in network-byte-order.
static inline void EmbedInt32NBO(char** b, int32_t i) {
i = htonl(i);
memcpy(*b, &i, sizeof(i));
*b += 4;
}
/// Embed a float in 16 bit "half" format (loses some precision) in
/// network-byte-order.
static inline void EmbedFloat16NBO(char** b, float f) {
uint16_t val = htons(FloatToHalf(f));
memcpy(*b, &val, sizeof(val));
*b += 2;
}
/// Embed 4 byte float into a buffer.
static inline void EmbedFloat32(char** b, float f) {
memcpy(*b, &f, 4);
*b += 4;
}
/// Embed a string into a buffer.
static inline void EmbedString(char** b, const char* s) {
strcpy(*b, s); // NOLINT
*b += strlen(*b) + 1;
}
static inline auto EmbeddedStringSize(const char* s) -> int {
return static_cast<int>(strlen(s) + 1);
}
/// Embed a string in a buffer.
static inline void EmbedString(char** b, const std::string& s) {
strcpy(*b, s.c_str()); // NOLINT
*b += s.size() + 1;
}
/// Return the number of bytes an embedded string with occupy.
static inline auto EmbeddedStringSize(const std::string& s) -> int {
return static_cast<int>(s.size() + 1);
}
/// Extract a string from a buffer.
static inline auto ExtractString(const char** b) -> std::string {
std::string s = *b;
*b += s.size() + 1;
return s;
}
/// Extract a single bool from a buffer.
static inline auto ExtractBool(const char** b) -> bool {
bool i = (**b != 0);
(*b)++;
return i;
}
/// Extract multiple bools from a buffer.
static inline void ExtractBools(const char** b, bool* i1, bool* i2 = nullptr,
bool* i3 = nullptr, bool* i4 = nullptr,
bool* i5 = nullptr, bool* i6 = nullptr,
bool* i7 = nullptr, bool* i8 = nullptr) {
auto i = static_cast<uint8_t>(**b);
*i1 = static_cast<bool>(i & 0x01);
if (i2) *i2 = static_cast<bool>((i >> 1) & 0x01);
if (i3) *i3 = static_cast<bool>((i >> 2) & 0x01);
if (i4) *i4 = static_cast<bool>((i >> 3) & 0x01);
if (i5) *i5 = static_cast<bool>((i >> 4) & 0x01);
if (i6) *i6 = static_cast<bool>((i >> 5) & 0x01);
if (i7) *i7 = static_cast<bool>((i >> 6) & 0x01);
if (i8) *i8 = static_cast<bool>((i >> 7) & 0x01);
(*b)++;
}
#pragma clang diagnostic pop
/// Extract a 1 byte int from a buffer.
static inline auto ExtractInt8(const char** b) -> int8_t {
int8_t i = **b;
(*b)++;
return i;
}
/// Extract a 2 byte int from a network-byte-order buffer.
static inline auto ExtractInt16NBO(const char** b) -> int16_t {
int16_t i;
memcpy(&i, *b, sizeof(i));
*b += 2;
return ntohs(i); // NOLINT
}
/// Extract a 4 byte int from a network-byte-order buffer.
static inline auto ExtractInt32NBO(const char** b) -> int32_t {
int32_t i;
memcpy(&i, *b, sizeof(i));
*b += 4;
return ntohl(i); // NOLINT
}
/// Extract a 2 byte (half) float from a network-byte-order buffer.
static inline auto ExtractFloat16NBO(const char** b) -> float {
uint16_t i;
memcpy(&i, *b, sizeof(i));
*b += 2;
return HalfToFloat(ntohs(i)); // NOLINT
}
/// Extract a 4 byte float from a buffer.
static inline auto ExtractFloat32(const char** b) -> float {
float i;
memcpy(&i, *b, 4);
*b += 4;
return i;
}
/// Return whether a sequence of some type pointer has nullptr members.
template <typename T>
static auto HasNullMembers(const T& sequence) -> bool {
for (auto&& i : sequence) {
if (i == nullptr) {
return true;
}
}
return false;
}
/// Simple lists of pre-calculated random values between 0 and 1
/// (with no particular distribution)
static float precalc_rands_1[];
static float precalc_rands_2[];
static float precalc_rands_3[];
auto huffman() -> Huffman* { return huffman_.get(); }
/// Encrypt a string in a manner specific to this device.
static auto LocalEncrypt(const std::string& s) -> std::string;
static auto LocalEncrypt2(const std::string& s) -> std::string;
/// Decode a local string that was encoded specific to this device.
/// Throws an exception on failure.
static auto LocalDecrypt(const std::string& s) -> std::string;
static auto LocalDecrypt2(const std::string& s) -> std::string;
/// Encrypt a string using a custom key.
static auto EncryptCustom(const std::string& s, const std::string& key)
-> std::string;
/// Decrypt a string using a custom key.
static auto DecryptCustom(const std::string& s, const std::string& key)
-> std::string;
/// Encrypt/decrypt strings to send to the master-server
static auto PublicEncrypt(const std::string& s) -> std::string;
static auto PublicDecrypt(const std::string& s) -> std::string;
static auto PublicEncrypt2(const std::string& s) -> std::string;
static auto PublicDecrypt2(const std::string& s) -> std::string;
// FIXME - move to a nice math-y place
static auto Sphrand(float radius = 1.0f) -> Vector3f;
// read a file into a string, throwing an Exception on error.
static auto FileToString(const std::string& file_name) -> std::string;
// fixme: move this to a 'Math' class?..
static auto SmoothStep(float edge0, float edge1, float x) -> float {
float t;
t = std::min(1.0f, std::max(0.0f, (x - edge0) / (edge1 - edge0)));
return t * t * (3.0f - 2.0f * t);
}
private:
std::unique_ptr<Huffman> huffman_;
};
} // namespace ballistica
#endif // BALLISTICA_GENERIC_UTILS_H_

View File

@ -33,9 +33,7 @@ if TYPE_CHECKING:
def get_legal_notice_private() -> str:
"""Return the one line legal notice we expect private files to have."""
# We just use the first line of the mit license (just the copyright)
from efrotools import MIT_LICENSE
return MIT_LICENSE.splitlines()[0]
return 'Copyright (c) 2011-2020 Eric Froemling'
@dataclass
@ -287,27 +285,33 @@ class Updater:
can_auto_update=can_auto_update))
def _check_header(self, fname: str) -> None:
from efrotools import get_public_license
# Make sure its define guard is correct.
guard = (fname[4:].upper().replace('/', '_').replace('.', '_') + '_')
with open(fname) as fhdr:
lines = fhdr.read().splitlines()
if self._public:
raise RuntimeError('FIXME: Check for full license.')
# Look for copyright/legal-notice line(s).
line = '// ' + get_legal_notice_private()
# Look for public license line (public or private repo)
# or private license line (private repo only)
line_private = '// ' + get_legal_notice_private()
line_public = get_public_license('c++')
lnum = 0
if lines[lnum] != line:
# Allow auto-correcting if it looks close already
# (don't want to blow away an unrelated line)
allow_auto = 'Copyright' in lines[
lnum] and 'Eric Froemling' in lines[lnum]
self._add_line_correction(fname,
line_number=lnum,
expected=line,
can_auto_update=allow_auto)
if self._public:
if lines[lnum] != line_public:
# Allow auto-correcting from private to public line
allow_auto = lines[lnum] == line_private
self._add_line_correction(fname,
line_number=lnum,
expected=line_public,
can_auto_update=allow_auto)
else:
if lines[lnum] not in [line_public, line_private]:
self._add_line_correction(fname,
line_number=lnum,
expected=line_private,
can_auto_update=False)
# Check for header guard at top
line = '#ifndef ' + guard

View File

@ -27,27 +27,6 @@ PYVER = '3.8'
# Python binary assumed by these tools.
PYTHON_BIN = f'python{PYVER}' if platform.system() != 'Windows' else 'python'
MIT_LICENSE = """Copyright (c) 2011-2020 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.
"""
def explicit_bool(value: bool) -> bool:
"""Simply return input value; can avoid unreachable-code type warnings."""

View File

@ -64,7 +64,7 @@ def formatcode(projroot: Path, full: bool) -> None:
def cpplint(projroot: Path, full: bool) -> None:
"""Run lint-checking on all code deemed lint-able."""
# pylint: disable=too-many-locals
# pylint: disable=too-many-locals, too-many-statements
import tempfile
from concurrent.futures import ThreadPoolExecutor
from multiprocessing import cpu_count
@ -124,6 +124,13 @@ def cpplint(projroot: Path, full: bool) -> None:
codelines[headercheckline] = (
" if False and include and include.group(1) in ('cfenv',")
# Skip copyright line check (our public repo code is MIT licensed
# so not crucial to keep track of who wrote exactly what)
copyrightline = codelines.index(
' """Logs an error if no Copyright'
' message appears at the top of the file."""')
codelines[copyrightline] = ' return'
# Don't complain about unknown NOLINT categories.
# (we use them for clang-tidy)
unknownlintline = codelines.index(