From 87b6716d59a34bf585cdaaf267899d384e26b8af Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Thu, 1 Oct 2020 13:52:02 -0500 Subject: [PATCH] Adding more C++ sources --- .efrocachemap | 20 +- .idea/dictionaries/ericf.xml | 15 +- config/config.json | 16 + src/ballistica/ballistica.cc | 343 +++++++ src/ballistica/ballistica.h | 226 +++++ src/ballistica/core/context.cc | 142 +++ src/ballistica/core/context.h | 128 +++ src/ballistica/core/exception.cc | 83 ++ src/ballistica/core/exception.h | 82 ++ src/ballistica/core/fatal_error.cc | 169 ++++ src/ballistica/core/fatal_error.h | 33 + src/ballistica/core/inline.cc | 11 + src/ballistica/core/inline.h | 143 +++ src/ballistica/core/logging.cc | 214 +++++ src/ballistica/core/logging.h | 35 + src/ballistica/core/macros.cc | 106 +++ src/ballistica/core/macros.h | 166 ++++ src/ballistica/core/module.cc | 50 + src/ballistica/core/module.h | 70 ++ src/ballistica/core/object.cc | 233 +++++ src/ballistica/core/object.h | 671 +++++++++++++ src/ballistica/core/thread.cc | 630 ++++++++++++ src/ballistica/core/thread.h | 249 +++++ src/ballistica/core/types.h | 1068 +++++++++++++++++++++ src/ballistica/generic/base64.cc | 161 ++++ src/ballistica/generic/base64.h | 16 + src/ballistica/generic/buffer.h | 95 ++ src/ballistica/generic/huffman.cc | 590 ++++++++++++ src/ballistica/generic/huffman.h | 61 ++ src/ballistica/generic/json.cc | 1105 ++++++++++++++++++++++ src/ballistica/generic/json.h | 239 +++++ src/ballistica/generic/lambda_runnable.h | 38 + src/ballistica/generic/real_timer.h | 49 + src/ballistica/generic/runnable.cc | 11 + src/ballistica/generic/runnable.h | 23 + src/ballistica/generic/timer.cc | 55 ++ src/ballistica/generic/timer.h | 41 + src/ballistica/generic/timer_list.cc | 271 ++++++ src/ballistica/generic/timer_list.h | 68 ++ src/ballistica/generic/utf8.cc | 449 +++++++++ src/ballistica/generic/utf8.h | 85 ++ src/ballistica/generic/utils.cc | 639 +++++++++++++ src/ballistica/generic/utils.h | 387 ++++++++ tools/batools/updateproject.py | 38 +- tools/efrotools/__init__.py | 21 - tools/efrotools/code.py | 9 +- 46 files changed, 9299 insertions(+), 55 deletions(-) create mode 100644 src/ballistica/ballistica.cc create mode 100644 src/ballistica/ballistica.h create mode 100644 src/ballistica/core/context.cc create mode 100644 src/ballistica/core/context.h create mode 100644 src/ballistica/core/exception.cc create mode 100644 src/ballistica/core/exception.h create mode 100644 src/ballistica/core/fatal_error.cc create mode 100644 src/ballistica/core/fatal_error.h create mode 100644 src/ballistica/core/inline.cc create mode 100644 src/ballistica/core/inline.h create mode 100644 src/ballistica/core/logging.cc create mode 100644 src/ballistica/core/logging.h create mode 100644 src/ballistica/core/macros.cc create mode 100644 src/ballistica/core/macros.h create mode 100644 src/ballistica/core/module.cc create mode 100644 src/ballistica/core/module.h create mode 100644 src/ballistica/core/object.cc create mode 100644 src/ballistica/core/object.h create mode 100644 src/ballistica/core/thread.cc create mode 100644 src/ballistica/core/thread.h create mode 100644 src/ballistica/core/types.h create mode 100644 src/ballistica/generic/base64.cc create mode 100644 src/ballistica/generic/base64.h create mode 100644 src/ballistica/generic/buffer.h create mode 100644 src/ballistica/generic/huffman.cc create mode 100644 src/ballistica/generic/huffman.h create mode 100644 src/ballistica/generic/json.cc create mode 100644 src/ballistica/generic/json.h create mode 100644 src/ballistica/generic/lambda_runnable.h create mode 100644 src/ballistica/generic/real_timer.h create mode 100644 src/ballistica/generic/runnable.cc create mode 100644 src/ballistica/generic/runnable.h create mode 100644 src/ballistica/generic/timer.cc create mode 100644 src/ballistica/generic/timer.h create mode 100644 src/ballistica/generic/timer_list.cc create mode 100644 src/ballistica/generic/timer_list.h create mode 100644 src/ballistica/generic/utf8.cc create mode 100644 src/ballistica/generic/utf8.h create mode 100644 src/ballistica/generic/utils.cc create mode 100644 src/ballistica/generic/utils.h diff --git a/.efrocachemap b/.efrocachemap index 846e4a06..fb411c5e 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -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" } \ No newline at end of file diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index 5bde834d..fae1c944 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -29,8 +29,8 @@ achname achs acinstance - ack ack'ed + ack acked acks acnt @@ -151,8 +151,8 @@ bacommon badguy bafoundation - ballistica ballistica's + ballistica ballisticacore ballisticacorecb bamaster @@ -793,8 +793,8 @@ gamedata gameinstance gamemap - gamepad gamepad's + gamepad gamepadadvanced gamepads gamepadselect @@ -1177,8 +1177,8 @@ lsqlite lssl lstart - lstr lstr's + lstr lstrs lsval ltex @@ -1362,6 +1362,9 @@ nosub nosyncdir nosyncdirs + nosyncfile + nosyncfiles + nosynctool nosynctools notdir npos @@ -1800,8 +1803,8 @@ sessionname sessionplayer sessionplayers - sessionteam sessionteam's + sessionteam sessionteams sessiontype setactivity @@ -2131,8 +2134,8 @@ txtw typeargs typecheck - typechecker typechecker's + typechecker typedval typeshed typestr diff --git a/config/config.json b/config/config.json index 793059ca..ac760956 100644 --- a/config/config.json +++ b/config/config.json @@ -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": [ diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc new file mode 100644 index 00000000..82fa265b --- /dev/null +++ b/src/ballistica/ballistica.cc @@ -0,0 +1,343 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/ballistica.h" + +#include + +#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(); + network_write_thread->AddModule(); + media_thread->AddModule(); + g_main_thread->AddModule(); + audio_thread->AddModule(); + + // 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(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 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( + Platform::GetCurrentMilliseconds())); // NOLINT + uint32_t tval = static_cast(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 diff --git a/src/ballistica/ballistica.h b/src/ballistica/ballistica.h new file mode 100644 index 00000000..b615fa98 --- /dev/null +++ b/src/ballistica/ballistica.h @@ -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 +// and float/double in , 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 +#include +#include +#include +#include +#include +#include +#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(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( + (static_cast(rand()) / RAND_MAX)); // NOLINT +} + +auto SetPythonException(PyExcType python_type, const char* description) -> void; + +} // namespace ballistica + +#endif // __cplusplus + +#endif // BALLISTICA_BALLISTICA_H_ diff --git a/src/ballistica/core/context.cc b/src/ballistica/core/context.cc new file mode 100644 index 00000000..1c0b15df --- /dev/null +++ b/src/ballistica/core/context.cc @@ -0,0 +1,142 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/core/context.h" + +#include + +#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(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(c)); + return uiContext; +} + +ScopedSetContext::ScopedSetContext(const Object::Ref& 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) -> 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 { + throw Exception("GetTexture() not supported in this context"); +} + +auto ContextTarget::GetSound(const std::string& name) -> Object::Ref { + throw Exception("GetSound() not supported in this context"); +} + +auto ContextTarget::GetData(const std::string& name) -> Object::Ref { + throw Exception("GetData() not supported in this context"); +} + +auto ContextTarget::GetModel(const std::string& name) -> Object::Ref { + throw Exception("GetModel() not supported in this context"); +} + +auto ContextTarget::GetCollideModel(const std::string& name) + -> Object::Ref { + throw Exception("GetCollideModel() not supported in this context"); +} + +} // namespace ballistica diff --git a/src/ballistica/core/context.h b/src/ballistica/core/context.h new file mode 100644 index 00000000..89cdb714 --- /dev/null +++ b/src/ballistica/core/context.h @@ -0,0 +1,128 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_CORE_CONTEXT_H_ +#define BALLISTICA_CORE_CONTEXT_H_ + +#include + +#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 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 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) -> int; + virtual void DeleteTimer(TimeType timetype, int timer_id); + + virtual auto GetTexture(const std::string& name) -> Object::Ref; + virtual auto GetSound(const std::string& name) -> Object::Ref; + virtual auto GetData(const std::string& name) -> Object::Ref; + virtual auto GetModel(const std::string& name) -> Object::Ref; + virtual auto GetCollideModel(const std::string& name) + -> Object::Ref; + + // 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& 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_ diff --git a/src/ballistica/core/exception.cc b/src/ballistica/core/exception.cc new file mode 100644 index 00000000..8327415e --- /dev/null +++ b/src/ballistica/core/exception.cc @@ -0,0 +1,83 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/core/exception.h" + +#include + +#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(&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(this)->full_description_ = + message_ + "\nThrown from " + thread_name_ + " thread:\n" + + stack_trace_->GetDescription(); + } else { + const_cast(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 diff --git a/src/ballistica/core/exception.h b/src/ballistica/core/exception.h new file mode 100644 index 00000000..92c36af3 --- /dev/null +++ b/src/ballistica/core/exception.h @@ -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 +#include + +#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_ diff --git a/src/ballistica/core/fatal_error.cc b/src/ballistica/core/fatal_error.cc new file mode 100644 index 00000000..a4e81aee --- /dev/null +++ b/src/ballistica/core/fatal_error.cc @@ -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(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 diff --git a/src/ballistica/core/fatal_error.h b/src/ballistica/core/fatal_error.h new file mode 100644 index 00000000..3388287c --- /dev/null +++ b/src/ballistica/core/fatal_error.h @@ -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 + +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_ diff --git a/src/ballistica/core/inline.cc b/src/ballistica/core/inline.cc new file mode 100644 index 00000000..a84b8e11 --- /dev/null +++ b/src/ballistica/core/inline.cc @@ -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 diff --git a/src/ballistica/core/inline.h b/src/ballistica/core/inline.h new file mode 100644 index 00000000..944b8587 --- /dev/null +++ b/src/ballistica/core/inline.h @@ -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 +#include +#include + +// 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 +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::value + && !std::is_same::value + && !std::is_same::value + && !std::is_same::value + && !std::is_same::value + && !std::is_same::value + && !std::is_same::value + && !std::is_same::value, + "static_cast_check_fit cannot be used with floats or doubles."); +#if BA_DEBUG_BUILD + assert(static_cast(static_cast(in)) == in); +#endif + return static_cast(in); +} + +/// Like static_cast_check_fit, but runs checks even in release builds and +/// throws an Exception on failure. +template +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::value + && !std::is_same::value + && !std::is_same::value + && !std::is_same::value + && !std::is_same::value + && !std::is_same::value + && !std::is_same::value + && !std::is_same::value, + "static_cast_always_checked cannot be used with floats or doubles."); + auto out = static_cast(in); + if (static_cast(out) != in) { + throw Exception("static_cast_check_fit_always failed for value " + + std::to_string(in) + "."); + } + return static_cast(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 +auto static_cast_check_type(IN_TYPE in) -> OUT_TYPE { + auto out_static = static_cast(in); +#if BA_DEBUG_BUILD + auto out_dynamic = dynamic_cast(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 +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]"; +#elif defined(_MSC_VER) + name = __FUNCSIG__; + prefix = + "class std::basic_string_view > " + "__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 +static auto static_type_name(bool debug_full = false) -> std::string { + return std::string(static_type_name_constexpr(debug_full)); +} + +} // namespace ballistica + +#endif // __cplusplus + +#endif // BALLISTICA_CORE_INLINE_H_ diff --git a/src/ballistica/core/logging.cc b/src/ballistica/core/logging.cc new file mode 100644 index 00000000..8a3f32a1 --- /dev/null +++ b/src/ballistica/core/logging.cc @@ -0,0 +1,214 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/core/logging.h" + +#include + +#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 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\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 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 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 diff --git a/src/ballistica/core/logging.h b/src/ballistica/core/logging.h new file mode 100644 index 00000000..bd2ab768 --- /dev/null +++ b/src/ballistica/core/logging.h @@ -0,0 +1,35 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_CORE_LOGGING_H_ +#define BALLISTICA_CORE_LOGGING_H_ + +#include + +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_ diff --git a/src/ballistica/core/macros.cc b/src/ballistica/core/macros.cc new file mode 100644 index 00000000..73aee581 --- /dev/null +++ b/src/ballistica/core/macros.cc @@ -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 diff --git a/src/ballistica/core/macros.h b/src/ballistica/core/macros.h new file mode 100644 index 00000000..65b81043 --- /dev/null +++ b/src/ballistica/core/macros.h @@ -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 +#include +#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 \ + "" +#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_ diff --git a/src/ballistica/core/module.cc b/src/ballistica/core/module.cc new file mode 100644 index 00000000..626f8c6f --- /dev/null +++ b/src/ballistica/core/module.cc @@ -0,0 +1,50 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/core/module.h" + +#include + +#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) -> 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 runnables; + runnables_.swap(runnables); + for (Runnable* i : runnables) { + i->Run(); + delete i; + } +} + +} // namespace ballistica diff --git a/src/ballistica/core/module.h b/src/ballistica/core/module.h new file mode 100644 index 00000000..4bcccf1c --- /dev/null +++ b/src/ballistica/core/module.h @@ -0,0 +1,70 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_CORE_MODULE_H_ +#define BALLISTICA_CORE_MODULE_H_ + +#include +#include +#include +#include + +#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 + 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) -> Timer*; + + private: + std::string name_; + int id_{}; + std::list runnables_; + Thread* thread_{}; +}; + +} // namespace ballistica + +#endif // BALLISTICA_CORE_MODULE_H_ diff --git a/src/ballistica/core/object.cc b/src/ballistica/core/object.cc new file mode 100644 index 00000000..ff6e63e6 --- /dev/null +++ b/src/ballistica/core/object.cc @@ -0,0 +1,233 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/core/object.h" + +#include +#include +#include +#include +#include + +#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 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 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 > 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 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 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 diff --git a/src/ballistica/core/object.h b/src/ballistica/core/object.h new file mode 100644 index 00000000..7e0f8b7b --- /dev/null +++ b/src/ballistica/core/object.h @@ -0,0 +1,671 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_CORE_OBJECT_H_ +#define BALLISTICA_CORE_OBJECT_H_ + +#include +#include +#include + +#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 + class Ref; + template + 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 + 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(obj_); + } + + // These operators throw exceptions if the object is dead. + auto operator*() const -> T& { + if (!obj_) { + throw Exception("Invalid dereference of " + static_type_name()); + } + + // Yes, reinterpret_cast is evil, but we make sure + // we only operate on cases where this is valid + // (see Acquire()). + return *reinterpret_cast(obj_); + } + auto operator->() const -> T* { + if (!obj_) { + throw Exception("Invalid dereference of " + static_type_name()); + } + + // Yes, reinterpret_cast is evil, but we make sure + // we only operate on cases where this is valid + // (see Acquire()). + return reinterpret_cast(obj_); + } + + // Assign/compare with any compatible pointer. + template + auto operator=(U* ptr) -> WeakRef& { + 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(obj_) == ptr); + assert(static_cast(obj_) == ptr); + assert(dynamic_cast(obj_) == ptr); + return *this; + } + template + auto operator==(U* ptr) -> bool { + return (get() == ptr); + } + template + 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& ref) -> WeakRef& { + *this = ref.get(); + return *this; + } + auto operator==(const WeakRef& ref) -> bool { + return (get() == ref.get()); + } + auto operator!=(const WeakRef& ref) -> bool { + return (get() != ref.get()); + } + + // Assign/compare with any compatible strong-ref. + template + auto operator=(const Ref& ref) -> WeakRef& { + *this = ref.get(); + return *this; + } + + template + auto operator==(const Ref& ref) -> bool { + return (get() == ref.get()); + } + + template + auto operator!=(const Ref& ref) -> bool { + return (get() != ref.get()); + } + + // Assign/compare with any compatible weak-ref. + template + auto operator=(const WeakRef& ref) -> WeakRef& { + *this = ref.get(); + return *this; + } + + template + auto operator==(const WeakRef& ref) -> bool { + return (get() == ref.get()); + } + + template + auto operator!=(const WeakRef& 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& ref) { *this = ref.get(); } + + // From a compatible pointer. + template + explicit WeakRef(U* ptr) { + *this = ptr; + } + + // From a compatible strong ref. + template + explicit WeakRef(const Ref& ref) { + *this = ref; + } + + // From a compatible weak ref. + template + explicit WeakRef(const WeakRef& ref) { + *this = ref; + } + + private: + void Acquire(T* obj) { + if (obj == nullptr) { + throw Exception("Acquiring invalid ptr of " + static_type_name()); + } +#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(obj) == reinterpret_cast(obj)); + + // More random sanity checking. + assert(dynamic_cast(reinterpret_cast(obj)) == obj); + obj_ = obj; + } + }; // WeakRef + + // Strong-ref. + template + 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()); + } + return *obj_; + } + auto operator->() const -> T* { + if (!obj_) { + throw Exception("Invalid dereference of " + static_type_name()); + } + return obj_; + } + auto exists() const -> bool { return (obj_ != nullptr); } + void Clear() { Release(); } + + // Assign/compare with any compatible pointer. + template + auto operator=(U* ptr) -> Ref& { + Release(); + if (ptr) { + Acquire(ptr); + } + return *this; + } + template + auto operator==(U* ptr) -> bool { + return (get() == ptr); + } + template + auto operator!=(U* ptr) -> bool { + return (get() != ptr); + } + + auto operator==(const Ref& ref) -> bool { return (get() == ref.get()); } + auto operator!=(const Ref& 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& ref) -> Ref& { + assert(this != &ref); // Shouldn't be self-assigning. + *this = ref.get(); + return *this; + } + + // Assign/compare with any compatible strong-ref. + template + auto operator=(const Ref& ref) -> Ref& { + *this = ref.get(); + return *this; + } + + template + auto operator==(const Ref& ref) -> bool { + return (get() == ref.get()); + } + + template + auto operator!=(const Ref& ref) -> bool { + return (get() != ref.get()); + } + + // Assign/compare from any compatible weak-ref. + template + auto operator=(const WeakRef& ref) -> Ref& { + *this = ref.get(); + return *this; + } + + template + auto operator==(const WeakRef& ref) -> bool { + return (get() == ref.get()); + } + + template + auto operator!=(const WeakRef& 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& ref) { *this = ref.get(); } + + // From a compatible pointer. + template + explicit Ref(U* ptr) { + *this = ptr; + } + + // From a compatible strong ref. + template + explicit Ref(const Ref& ref) { + *this = ref; + } + + // From a compatible weak ref. + template + explicit Ref(const WeakRef& ref) { + *this = ref; + } + + private: + void Acquire(T* obj) { + if (obj == nullptr) { + throw Exception("Acquiring invalid ptr of " + static_type_name()); + } + +#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(): 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 + [[nodiscard]] static auto New(ARGS&&... args) -> Object::Ref { + auto* ptr = new TALLOC(std::forward(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(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 + [[nodiscard]] static auto NewDeferred(ARGS&&... args) -> T* { + T* ptr = new T(std::forward(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 + static auto MakeRefCounted(T* ptr) -> Object::Ref { +#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(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 + [[nodiscard]] static auto NewUnmanaged(ARGS&&... args) -> T* { + T* ptr = new T(std::forward(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 +auto PointersToRefs(const std::vector& ptrs) + -> std::vector > { + std::vector > refs; + refs.reserve(ptrs.size()); + for (typename std::vector::const_iterator i = ptrs.begin(); + i != ptrs.end(); i++) { + refs.push_back(Object::Ref(*i)); + } + return refs; +} + +/// Convert a vector of ptrs into a vector of refs. +template +auto PointersToWeakRefs(const std::vector& ptrs) + -> std::vector > { + std::vector > refs; + refs.reserve(ptrs.size()); + for (typename std::vector::const_iterator i = ptrs.begin(); + i != ptrs.end(); i++) { + refs.push_back(Object::WeakRef(*i)); + } + return refs; +} + +/// Convert a vector of refs to a vector of ptrs. +template +auto RefsToPointers(const std::vector >& refs) + -> std::vector { + std::vector 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 +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 +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() : ""; +} + +// 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 +#else +#define BA_DEBUG_PTR(TYPE) TYPE* +#endif + +} // namespace ballistica + +#endif // BALLISTICA_CORE_OBJECT_H_ diff --git a/src/ballistica/core/thread.cc b/src/ballistica/core/thread.cc new file mode 100644 index 00000000..f9283720 --- /dev/null +++ b/src/ballistica/core/thread.cc @@ -0,0 +1,630 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/core/thread.h" + +#include + +#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 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 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(data)->ThreadMain(); +} + +auto Thread::RunAudioThread(void* data) -> int { + return static_cast(data)->ThreadMain(); +} + +auto Thread::RunBGDynamicThread(void* data) -> int { + return static_cast(data)->ThreadMain(); +} + +auto Thread::RunNetworkWriteThread(void* data) -> int { + return static_cast(data)->ThreadMain(); +} + +auto Thread::RunStdInputThread(void* data) -> int { + return static_cast(data)->ThreadMain(); +} + +auto Thread::RunMediaThread(void* data) -> int { + return static_cast(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 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 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 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(thread_message.pval); + tl->Launch(this); + auto cmd = + static_cast(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(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* 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 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 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 lock(data_to_client_mutex_); + + uint32_t cmd; + ReadFromThread(&lock, &cmd, sizeof(cmd)); + assert(static_cast(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(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 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(m.type)) + ")"; + break; + } + if (m.type == ThreadMessage::Type::kRunnable) { + // Runnable* e; + // e = static_cast(m.pval); + { + std::string m_name = g_platform->DemangleCXXSymbol( + typeid(*(static_cast(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 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* 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(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) -> 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 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 += ""; + } + return name; +#else + return "unknown"; +#endif +} + +} // namespace ballistica diff --git a/src/ballistica/core/thread.h b/src/ballistica/core/thread.h new file mode 100644 index 00000000..cb940299 --- /dev/null +++ b/src/ballistica/core/thread.h @@ -0,0 +1,249 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_CORE_THREAD_H_ +#define BALLISTICA_CORE_THREAD_H_ + +#include +#include +#include +#include +#include +#include + +#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 + struct ModuleLauncherTemplate : public ModuleLauncher { + void Launch(Thread* g) override { new MODULETYPE(g); } + }; + + template + 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 + void AddModule() { + switch (type_) { + case ThreadType::kStandard: { + // Launching a module in the current thread: do it immediately. + if (IsCurrent()) { + ModuleLauncherTemplate launcher; + launcher.Launch(this); + } else { + // Launching a module in another thread; + // send a module-launcher and wait for the confirmation. + ModuleLauncherTemplate launcher; + ModuleLauncher* tl = &launcher; + PushThreadMessage( + ThreadMessage(ThreadMessage::Type::kNewModule, 0, tl)); + std::unique_lock lock(data_to_client_mutex_); + uint32_t cmd; + ReadFromThread(&lock, &cmd, sizeof(cmd)); + assert(static_cast(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 + void AddModule(ARGTYPE arg) { + switch (type_) { + case ThreadType::kStandard: { + // Launching a module in the current thread: do it immediately. + if (IsCurrent()) { + ModuleLauncherArgTemplate launcher(arg); + launcher.Launch(this); + } else { + // Launching a module in another thread; + // send a module-launcher and wait for the confirmation. + ModuleLauncherArgTemplate launcher(arg); + ModuleLauncher* tl = &launcher; + PushThreadMessage( + ThreadMessage(ThreadMessage::Type::kNewModule, 0, tl)); + + std::unique_lock lock(data_to_client_mutex_); + + uint32_t cmd; + + ReadFromThread(&lock, &cmd, sizeof(cmd)); + + assert(static_cast(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) -> 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* 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* messages); + void PushThreadMessage(const ThreadMessage& t); + std::condition_variable thread_message_cv_; + std::mutex thread_message_mutex_; + std::list thread_messages_; + int thread_message_count_ = 0; + std::condition_variable data_to_client_cv_; + std::mutex data_to_client_mutex_; + std::list > data_to_client_; + std::vector modules_; + auto GetModule(int id) -> Module* { + assert(id >= 0 && id < static_cast(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_ diff --git a/src/ballistica/core/types.h b/src/ballistica/core/types.h new file mode 100644 index 00000000..3fe73091 --- /dev/null +++ b/src/ballistica/core/types.h @@ -0,0 +1,1068 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_CORE_TYPES_H_ +#define BALLISTICA_CORE_TYPES_H_ + +// Types used throughout the project. +// This header should not depend on any others in the project. +// Types can be defined (or predeclared) here if the are used +// in a significant number of places. The aim is to reduce the +// overall number of headers a given source file needs to pull in, +// helping to keep compile times down. + +#ifdef __cplusplus + +// Predeclare a few global namespace things +// (just enough to pass some pointers around without +// requiring system-ish headers). +#if BA_ENABLE_AUDIO +typedef struct ALCcontext_struct ALCcontext; +#endif +typedef struct _object PyObject; +typedef struct _ts PyThreadState; + +#if BA_SDL_BUILD || BA_MINSDL_BUILD +union SDL_Event; +struct SDL_Keysym; +typedef struct _SDL_Joystick SDL_Joystick; +#endif + +namespace ballistica { + +// Used internally for time values. +typedef int64_t millisecs_t; + +// We predeclare all our main ba classes here so that we can +// avoid pulling in their full headers as much as possible +// to keep compile times down. + +class Account; +class App; +class AppConfig; +class AppGlobals; +class AreaOfInterest; +class Audio; +class AudioServer; +class AudioStreamer; +class AudioSource; +class BGDynamics; +class BGDynamicsServer; +class BGDynamicsDrawSnapshot; +class BGDynamicsEmission; +class BGDynamicsFuse; +struct BGDynamicsFuseData; +class BGDynamicsHeightCache; +class BGDynamicsShadow; +struct BGDynamicsShadowData; +class BGDynamicsVolumeLight; +struct BGDynamicsVolumeLightData; +class ButtonWidget; +struct cJSON; +class Camera; +class ClientControllerInterface; +class ClientInputDevice; +class ClientSession; +class CollideModel; +class CollideModelData; +class Collision; +class CollisionCache; +class Connection; +class ConnectionToClient; +class Context; +class ContextTarget; +class ConnectionToClientUDP; +class ConnectionToHost; +class ConnectionToHostUDP; +class ContainerWidget; +class Console; +class CubeMapTexture; +class Data; +class DataData; +class Dynamics; +class FrameDef; +struct FriendScoreSet; +class Game; +class GLContext; +class GlobalsNode; +class Graphics; +class GraphicsServer; +class HostActivity; +class HostSession; +class Huffman; +class ImageMesh; +class ImageWidget; +class Input; +class InputDevice; +struct JointFixedEF; +class Joystick; +class KeyboardInput; +class Material; +class MaterialAction; +class MaterialComponent; +class MaterialConditionNode; +class MaterialContext; +class Matrix44f; +class Media; +class MediaComponentData; +class MediaServer; +class MeshBufferBase; +class MeshBufferVertexSprite; +class MeshBufferVertexSimpleFull; +class MeshBufferVertexSmokeFull; +class Mesh; +class MeshData; +class MeshDataClientHandle; +class MeshIndexBuffer16; +class MeshIndexedSimpleFull; +class MeshIndexedSmokeFull; +class MeshRendererData; +class Model; +class ModelData; +class ModelRendererData; +class NetClientThread; +class NetGraph; +class Networking; +class NetworkReader; +class NetworkWriteModule; +class Node; +class NodeType; +class NodeAttribute; +class NodeAttributeConnection; +class NodeAttributeUnbound; +class Object; +class ObjectComponent; +class GameStream; +class Part; +class Python; +class Platform; +class Player; +class PlayerNode; +class PlayerSpec; +class PythonClassCollideModel; +class PythonClassMaterial; +class PythonClassModel; +class PythonClassSound; +class PythonClassTexture; +class Python; +class PythonRef; +class PythonCommand; +class PythonContextCall; +template +class RealTimer; +class Rect; +class Renderer; +class RenderComponent; +class RenderCommandBuffer; +class RenderPass; +class RenderTarget; +class ReplayClientSession; +class RemoteAppServer; +class RemoteControlInput; +class RigidBody; +class RootUI; +class RootWidget; +class Runnable; +class Scene; +class ScoreToBeat; +class SDLApp; +class SDLContext; +class Session; +class SockAddr; +class Sound; +class SoundData; +class SpriteMesh; +class StackWidget; +class StdInputModule; +class Module; +class TelnetServer; +class TestInput; +class TextGroup; +class TextGraphics; +class TextMesh; +class TextPacker; +class Texture; +class TextureData; +class TexturePreloadData; +class TextureRendererData; +class TextWidget; +class Thread; +class Timer; +class TimerList; +class TouchInput; +class UI; +class Utils; +class Vector2f; +class Vector3f; +class Vector4f; +class VRApp; +class VRGraphics; +class Widget; + +// BA_EXPORT_PYTHON_ENUM +/// Types of input a controller can send to the game. +/// +/// Category: Enums +/// +enum class InputType { + kUpDown = 2, + kLeftRight, + kJumpPress, + kJumpRelease, + kPunchPress, + kPunchRelease, + kBombPress, + kBombRelease, + kPickUpPress, + kPickUpRelease, + kRun, + kFlyPress, + kFlyRelease, + kStartPress, + kStartRelease, + kHoldPositionPress, + kHoldPositionRelease, + kLeftPress, + kLeftRelease, + kRightPress, + kRightRelease, + kUpPress, + kUpRelease, + kDownPress, + kDownRelease, + kLast // Sentinel +}; + +typedef int64_t TimerMedium; + +// BA_EXPORT_PYTHON_ENUM +/// The overall scale the UI is being rendered for. Note that this is +/// independent of pixel resolution. For example, a phone and a desktop PC +/// might render the game at similar pixel resolutions but the size they +/// display content at will vary significantly. +/// +/// Category: Enums +/// +/// 'large' is used for devices such as desktop PCs where fine details can +/// be clearly seen. UI elements are generally smaller on the screen +/// and more content can be seen at once. +/// +/// 'medium' is used for devices such as tablets, TVs, or VR headsets. +/// This mode strikes a balance between clean readability and amount of +/// content visible. +/// +/// 'small' is used primarily for phones or other small devices where +/// content needs to be presented as large and clear in order to remain +/// readable from an average distance. +enum class UIScale { + kLarge, + kMedium, + kSmall, + kLast // Sentinel. +}; + +// BA_EXPORT_PYTHON_ENUM +/// Specifies the type of time for various operations to target/use. +/// +/// Category: Enums +/// +/// 'sim' time is the local simulation time for an activity or session. +/// It can proceed at different rates depending on game speed, stops +/// for pauses, etc. +/// +/// 'base' is the baseline time for an activity or session. It proceeds +/// consistently regardless of game speed or pausing, but may stop during +/// occurrences such as network outages. +/// +/// 'real' time is mostly based on clock time, with a few exceptions. It may +/// not advance while the app is backgrounded for instance. (the engine +/// attempts to prevent single large time jumps from occurring) +enum class TimeType { + kSim, + kBase, + kReal, + kLast // Sentinel. +}; + +// BA_EXPORT_PYTHON_ENUM +/// Specifies the format time values are provided in. +/// +/// Category: Enums +enum class TimeFormat { + kSeconds, + kMilliseconds, + kLast // Sentinel. +}; + +// BA_EXPORT_PYTHON_ENUM +/// Permissions that can be requested from the OS. +/// +/// Category: Enums +enum class Permission { + kStorage, + kLast // Sentinel. +}; + +// BA_EXPORT_PYTHON_ENUM +/// Special characters the game can print. +/// +/// Category: Enums +enum class SpecialChar { + kDownArrow, + kUpArrow, + kLeftArrow, + kRightArrow, + kTopButton, + kLeftButton, + kRightButton, + kBottomButton, + kDelete, + kShift, + kBack, + kLogoFlat, + kRewindButton, + kPlayPauseButton, + kFastForwardButton, + kDpadCenterButton, + kOuyaButtonO, + kOuyaButtonU, + kOuyaButtonY, + kOuyaButtonA, + kOuyaLogo, + kLogo, + kTicket, + kGooglePlayGamesLogo, + kGameCenterLogo, + kDiceButton1, + kDiceButton2, + kDiceButton3, + kDiceButton4, + kGameCircleLogo, + kPartyIcon, + kTestAccount, + kTicketBacking, + kTrophy1, + kTrophy2, + kTrophy3, + kTrophy0a, + kTrophy0b, + kTrophy4, + kLocalAccount, + kAlibabaLogo, + kFlagUnitedStates, + kFlagMexico, + kFlagGermany, + kFlagBrazil, + kFlagRussia, + kFlagChina, + kFlagUnitedKingdom, + kFlagCanada, + kFlagIndia, + kFlagJapan, + kFlagFrance, + kFlagIndonesia, + kFlagItaly, + kFlagSouthKorea, + kFlagNetherlands, + kFedora, + kHal, + kCrown, + kYinYang, + kEyeBall, + kSkull, + kHeart, + kDragon, + kHelmet, + kMushroom, + kNinjaStar, + kVikingHelmet, + kMoon, + kSpider, + kFireball, + kFlagUnitedArabEmirates, + kFlagQatar, + kFlagEgypt, + kFlagKuwait, + kFlagAlgeria, + kFlagSaudiArabia, + kFlagMalaysia, + kFlagCzechRepublic, + kFlagAustralia, + kFlagSingapore, + kOculusLogo, + kSteamLogo, + kNvidiaLogo, + kFlagIran, + kFlagPoland, + kFlagArgentina, + kFlagPhilippines, + kFlagChile, + kMikirog, + kLast // Sentinel +}; + +enum class MediaType { kTexture, kCollideModel, kModel, kSound, kData, kLast }; + +/// Python exception types we can raise from our own exceptions. +enum class PyExcType { + kRuntime, + kAttribute, + kIndex, + kType, + kValue, + kContext, + kNotFound, + kNodeNotFound, + kActivityNotFound, + kSessionNotFound, + kSessionPlayerNotFound, + kInputDeviceNotFound, + kDelegateNotFound, + kWidgetNotFound +}; + +enum class SystemTextureID { + kUIAtlas, + kButtonSquare, + kWhite, + kFontSmall0, + kFontBig, + kCursor, + kBoxingGlove, + kShield, + kExplosion, + kTextClearButton, + kWindowHSmallVMed, + kWindowHSmallVSmall, + kGlow, + kScrollWidget, + kScrollWidgetGlow, + kFlagPole, + kScorch, + kScorchBig, + kShadow, + kLight, + kShadowSharp, + kLightSharp, + kShadowSoft, + kLightSoft, + kSparks, + kEye, + kEyeTint, + kFuse, + kShrapnel1, + kSmoke, + kCircle, + kCircleOutline, + kCircleNoAlpha, + kCircleOutlineNoAlpha, + kCircleShadow, + kSoftRect, + kSoftRect2, + kSoftRectVertical, + kStartButton, + kBombButton, + kOuyaAButton, + kBackIcon, + kNub, + kArrow, + kMenuButton, + kUsersButton, + kActionButtons, + kTouchArrows, + kTouchArrowsActions, + kRGBStripes, + kUIAtlas2, + kFontSmall1, + kFontSmall2, + kFontSmall3, + kFontSmall4, + kFontSmall5, + kFontSmall6, + kFontSmall7, + kFontExtras, + kFontExtras2, + kFontExtras3, + kFontExtras4, + kCharacterIconMask, + kBlack, + kWings +}; + +enum class SystemCubeMapTextureID { + kReflectionChar, + kReflectionPowerup, + kReflectionSoft, + kReflectionSharp, + kReflectionSharper, + kReflectionSharpest +}; + +enum class SystemSoundID { + kDeek = 0, + kBlip, + kBlank, + kPunch, + kClick, + kErrorBeep, + kSwish, + kSwish2, + kSwish3, + kTap, + kCorkPop, + kGunCock, + kTickingCrazy, + kSparkle, + kSparkle2, + kSparkle3 +}; + +enum class SystemDataID {}; + +enum class SystemModelID { + kButtonSmallTransparent, + kButtonSmallOpaque, + kButtonMediumTransparent, + kButtonMediumOpaque, + kButtonBackTransparent, + kButtonBackOpaque, + kButtonBackSmallTransparent, + kButtonBackSmallOpaque, + kButtonTabTransparent, + kButtonTabOpaque, + kButtonLargeTransparent, + kButtonLargeOpaque, + kButtonLargerTransparent, + kButtonLargerOpaque, + kButtonSquareTransparent, + kButtonSquareOpaque, + kCheckTransparent, + kScrollBarThumbTransparent, + kScrollBarThumbOpaque, + kScrollBarThumbSimple, + kScrollBarThumbShortTransparent, + kScrollBarThumbShortOpaque, + kScrollBarThumbShortSimple, + kScrollBarTroughTransparent, + kTextBoxTransparent, + kImage1x1, + kImage1x1FullScreen, + kImage2x1, + kImage4x1, + kImage16x1, +#if BA_VR_BUILD + kImage1x1VRFullScreen, + kVROverlay, + kVRFade, +#endif + kOverlayGuide, + kWindowHSmallVMedTransparent, + kWindowHSmallVMedOpaque, + kWindowHSmallVSmallTransparent, + kWindowHSmallVSmallOpaque, + kSoftEdgeOutside, + kSoftEdgeInside, + kBoxingGlove, + kShield, + kFlagPole, + kFlagStand, + kScorch, + kEyeBall, + kEyeBallIris, + kEyeLid, + kHairTuft1, + kHairTuft1b, + kHairTuft2, + kHairTuft3, + kHairTuft4, + kShrapnel1, + kShrapnelSlime, + kShrapnelBoard, + kShockWave, + kFlash, + kCylinder, + kArrowFront, + kArrowBack, + kActionButtonLeft, + kActionButtonTop, + kActionButtonRight, + kActionButtonBottom, + kBox, + kLocator, + kLocatorBox, + kLocatorCircle, + kLocatorCircleOutline, + kCrossOut, + kWing +}; + +enum class NodeCollideAttr { + /// Whether or not a collision should occur at all. + /// If this is false for either node in the final context, + /// no collide events are run. + kCollideNode +}; + +enum class PartCollideAttr { + /// Whether or not a collision should occur at all. + /// If this is false for either surface in the final context, + /// no collide events are run. + kCollide, + + /// Whether to honor node-collisions. + /// Turn this on if you want a collision to occur even if + /// The part is ignoring collisions with your node due + /// to an existing NodeModAction. + kUseNodeCollide, + + /// Whether a physical collision happens. + kPhysical, + + /// Friction for physical collisions. + kFriction, + + /// Stiffness for physical collisions. + kStiffness, + + /// Damping for physical collisions. + kDamping, + + /// Bounce for physical collisions. + kBounce +}; + +enum class MaterialCondition { + /// Always evaluates to true. + kTrue, + + /// Always evaluates to false. + kFalse, + + /// Dst part contains specified material; requires 1 arg - material id. + kDstIsMaterial, + + /// Dst part does not contain specified material; requires 1 arg - material + /// id. + kDstNotMaterial, + + /// Dst part is in specified node; requires 1 arg - node id. + kDstIsNode, + + /// Dst part not in specified node; requires 1 arg - node id. + kDstNotNode, + + /// Dst part is specified part; requires 2 args, node id, part id. + kDstIsPart, + + /// Dst part not specified part; requires 2 args, node id, part id. + kDstNotPart, + + /// Dst part contains src material; no args. + kSrcDstSameMaterial, + + /// Dst part does not contain the src material; no args. + kSrcDstDiffMaterial, + + /// Dst and src parts in same node; no args. + kSrcDstSameNode, + + /// Dst and src parts in different node; no args. + kSrcDstDiffNode, + + /// Src part younger than specified value; requires 1 arg - age. + kSrcYoungerThan, + + /// Src part equal to or older than specified value; requires 1 arg - age. + kSrcOlderThan, + + /// Dst part younger than specified value; requires 1 arg - age. + kDstYoungerThan, + + /// Dst part equal to or older than specified value; requires 1 arg - age. + kDstOlderThan, + + /// Src part is already colliding with a part on dst node; no args. + kCollidingDstNode, + + /// Src part is not already colliding with a part on dst node; no args. + kNotCollidingDstNode, + + /// Set to collide at current point in rule evaluation. + kEvalColliding, + + /// Set to not collide at current point in rule evaluation. + kEvalNotColliding +}; + +/// Types of shading. +/// These do not necessarily correspond to actual shader objects in the renderer +/// (a single shader may handle more than one of these, etc). +/// These are simply categories of looks. +enum class ShadingType { + kSimpleColor, + kSimpleColorTransparent, + kSimpleColorTransparentDoubleSided, + kSimpleTexture, + kSimpleTextureModulated, + kSimpleTextureModulatedColorized, + kSimpleTextureModulatedColorized2, + kSimpleTextureModulatedColorized2Masked, + kSimpleTextureModulatedTransparent, + kSimpleTextureModulatedTransFlatness, + kSimpleTextureModulatedTransparentDoubleSided, + kSimpleTextureModulatedTransparentColorized, + kSimpleTextureModulatedTransparentColorized2, + kSimpleTextureModulatedTransparentColorized2Masked, + kSimpleTextureModulatedTransparentShadow, + kSimpleTexModulatedTransShadowFlatness, + kSimpleTextureModulatedTransparentGlow, + kSimpleTextureModulatedTransparentGlowMaskUV2, + kObject, + kObjectTransparent, + kObjectLightShadowTransparent, + kSpecial, + kShield, + kObjectReflect, + kObjectReflectTransparent, + kObjectReflectAddTransparent, + kObjectLightShadow, + kObjectReflectLightShadow, + kObjectReflectLightShadowDoubleSided, + kObjectReflectLightShadowColorized, + kObjectReflectLightShadowColorized2, + kObjectReflectLightShadowAdd, + kObjectReflectLightShadowAddColorized, + kObjectReflectLightShadowAddColorized2, + kSmoke, + kSmokeOverlay, + kPostProcess, + kPostProcessEyes, + kPostProcessNormalDistort, + kSprite, + kCount +}; + +enum class DrawType { kTriangles, kPoints }; + +/// Hints to the renderer - stuff that is changed rarely should be static, +/// and stuff changed often should be dynamic. +enum class MeshDrawType { kStatic, kDynamic }; + +enum class ReflectionType { + kNone, + kChar, + kPowerup, + kSoft, + kSharp, + kSharper, + kSharpest +}; + +/// Command values sent across the wire in netplay. +/// Must remain consistent across versions! +enum class SessionCommand { + kBaseTimeStep, + kStepSceneGraph, + kAddSceneGraph, + kRemoveSceneGraph, + kAddNode, + kNodeOnCreate, + kSetForegroundSceneGraph, + kRemoveNode, + kAddMaterial, + kRemoveMaterial, + kAddMaterialComponent, + kAddTexture, + kRemoveTexture, + kAddModel, + kRemoveModel, + kAddSound, + kRemoveSound, + kAddCollideModel, + kRemoveCollideModel, + kConnectNodeAttribute, + kNodeMessage, + kSetNodeAttrFloat, + kSetNodeAttrInt32, + kSetNodeAttrBool, + kSetNodeAttrFloats, + kSetNodeAttrInt32s, + kSetNodeAttrString, + kSetNodeAttrNode, + kSetNodeAttrNodeNull, + kSetNodeAttrNodes, + kSetNodeAttrPlayer, + kSetNodeAttrPlayerNull, + kSetNodeAttrMaterials, + kSetNodeAttrTexture, + kSetNodeAttrTextureNull, + kSetNodeAttrTextures, + kSetNodeAttrSound, + kSetNodeAttrSoundNull, + kSetNodeAttrSounds, + kSetNodeAttrModel, + kSetNodeAttrModelNull, + kSetNodeAttrModels, + kSetNodeAttrCollideModel, + kSetNodeAttrCollideModelNull, + kSetNodeAttrCollideModels, + kPlaySoundAtPosition, + kPlaySound, + kEmitBGDynamics, + kEndOfFile, + kDynamicsCorrection, + kScreenMessageBottom, + kScreenMessageTop, + kAddData, + kRemoveData +}; + +/// Standard messages to send to nodes. +/// Note: the names of these in python are their camelback forms, +/// so SELF_STATE is "selfState", etc. +enum class NodeMessageType { + /// Generic flash - no args. + kFlash, + /// Celebrate message - one int arg for duration. + kCelebrate, + /// Left-hand celebrate message - one int arg for duration. + kCelebrateL, + /// Right-hand celebrate message - one int arg for duration. + kCelebrateR, + /// Instantaneous impulse 3 vector floats. + kImpulse, + kKickback, + /// Knock the target out for an amount of time. + kKnockout, + /// Make a hurt sound. + kHurtSound, + /// You've been picked up.. lose balance or whatever. + kPickedUp, + /// Make a jump sound. + kJumpSound, + /// Make an attack sound. + kAttackSound, + /// Tell the player to scream. + kScreamSound, + /// Move to stand upon the given point facing the given angle. + /// 3 position floats and one angle float. + kStand, + /// Add or remove footing from a node. + /// First arg is an int - either 1 or -1 for add or subtract. + kFooting +}; + +enum class AccountState { kSignedOut, kSigningIn, kSignedIn }; + +enum class CameraMode { kFollow, kOrbit }; + +enum class MeshDataType { + kIndexedSimpleSplit, + kIndexedObjectSplit, + kIndexedSimpleFull, + kIndexedDualTextureFull, + kIndexedSmokeFull, + kSprite +}; + +struct TouchEvent { + enum class Type { kDown, kUp, kMoved, kCanceled }; + Type type{}; + void* touch{}; + bool overall{}; // For sanity-checks. + float x{}; + float y{}; +}; + +// Standard vertex structs used in rendering/fileIO/etc. +// Remember to make sure components are on 4 byte boundaries. +// (need to find out how strict we need to be on Metal, Vulkan, etc). + +struct VertexSimpleSplitStatic { + uint16_t uv[2]; +}; + +struct VertexSimpleSplitDynamic { + float position[3]; +}; + +struct VertexSimpleFull { + float position[3]; + uint16_t uv[2]; +}; + +struct VertexDualTextureFull { + float position[3]; + uint16_t uv[2]; + uint16_t uv2[2]; +}; + +struct VertexObjectSplitStatic { + uint16_t uv[2]; +}; + +struct VertexObjectSplitDynamic { + float position[3]; + int16_t normal[3]; + int8_t padding[2]; +}; + +struct VertexObjectFull { + float position[3]; + uint16_t uv[2]; + int16_t normal[3]; + uint8_t padding[2]; +}; + +struct VertexSmokeFull { + float position[3]; + float uv[2]; + uint8_t color[4]; + uint8_t diffuse; + uint8_t padding1[3]; + uint8_t erode; + uint8_t padding2[3]; +}; + +struct VertexSprite { + float position[3]; + uint16_t uv[2]; + float size; + float color[4]; +}; + +enum class MeshFormat { + /// 16bit UV, 8bit normal, 8bit pt-index. + kUV16N8Index8, + /// 16bit UV, 8bit normal, 16bit pt-index. + kUV16N8Index16, + /// 16bit UV, 8bit normal, 32bit pt-index. + kUV16N8Index32 +}; + +enum class TextureType { k2D, kCubeMap }; + +enum class TextureFormat { + kNone, + kRGBA_8888, + kRGB_888, + kRGBA_4444, + kRGB_565, + kDXT1, + kDXT5, + kETC1, + kPVR2, + kPVR4, + kETC2_RGB, + kETC2_RGBA +}; + +enum class TextureCompressionType { kS3TC, kPVR, kETC1, kETC2 }; + +enum class TextureMinQuality { kLow, kMedium, kHigh }; + +enum NodeAttributeFlag { kNodeAttributeFlagReadOnly = 1u }; + +enum class NodeAttributeType { + kFloat, + kFloatArray, + kInt, + kIntArray, + kBool, + kString, + kNode, + kNodeArray, + kPlayer, + kMaterialArray, + kTexture, + kTextureArray, + kSound, + kSoundArray, + kModel, + kModelArray, + kCollideModel, + kCollideModelArray +}; + +enum class ThreadType { + /// A normal thread spun up by us. + kStandard, + /// For wrapping a ballistica thread around the existing main thread. + kMain +}; + +/// Used for module-thread identification. +/// Mostly just for debugging, through a few things are affected by this +/// (the GAME thread manages the python GIL, etc). +enum class ThreadIdentifier { + kInvalid, + kGame, + kMedia, + kFileOut, + kMain, + kAudio, + kNetworkWrite, + kSuicide, + kStdin, + kBGDynamics +}; + +enum class AccountType { + kInvalid, + kTest, + kGameCenter, + kGameCircle, + kGooglePlay, + kDevice, + kServer, + kOculus, + kSteam, + kNvidiaChina +}; + +enum class GraphicsQuality { + /// Bare minimum graphics. + kLow, + /// Basic graphics; no post-processing. + kMedium, + /// Graphics with bare minimum post-processing. + kHigh, + /// Graphics with full post-processing. + kHigher, + /// Select graphics options automatically. + kAuto +}; + +enum class TextMeshEntryType { kRegular, kExtras, kOSRendered }; + +enum ModelDrawFlags { kModelDrawFlagNoReflection = 1 }; + +enum class LightShadowType { kNone, kTerrain, kObject }; + +enum class TextureQuality { kAuto, kHigh, kMedium, kLow }; + +typedef Node* NodeCreateFunc(Scene* sg); + +enum class BenchmarkType { kNone, kCPU, kGPU }; + +#if BA_VR_BUILD +enum class VRHandType { kNone, kDaydreamRemote, kOculusTouchL, kOculusTouchR }; +struct VRHandState { + VRHandType type = VRHandType::kNone; + float tx = 0.0f; + float ty = 0.0f; + float tz = 0.0f; + float yaw = 0.0f; + float pitch = 0.0f; + float roll = 0.0f; +}; +struct VRHandsState { + VRHandState l; + VRHandState r; +}; +#endif // BA_VR_BUILD + +} // namespace ballistica + +#endif // __cplusplus + +#endif // BALLISTICA_CORE_TYPES_H_ diff --git a/src/ballistica/generic/base64.cc b/src/ballistica/generic/base64.cc new file mode 100644 index 00000000..b7a76cab --- /dev/null +++ b/src/ballistica/generic/base64.cc @@ -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 + +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((char_array_3[0] & 0xfcu) >> 2u); + char_array_4[1] = + static_cast(((char_array_3[0] & 0x03u) << 4u) + + ((char_array_3[1] & 0xf0u) >> 4u)); + char_array_4[2] = + static_cast(((char_array_3[1] & 0x0fu) << 2u) + + ((char_array_3[2] & 0xc0u) >> 6u)); + char_array_4[3] = static_cast(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((char_array_3[0] & 0xfcu) >> 2u); + char_array_4[1] = static_cast( + ((char_array_3[0] & 0x03u) << 4u) + ((char_array_3[1] & 0xf0u) >> 4u)); + char_array_4[2] = static_cast( + ((char_array_3[1] & 0x0fu) << 2u) + ((char_array_3[2] & 0xc0u) >> 6u)); + char_array_4[3] = static_cast(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(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(base64_chars.find(char_array_4[i])); + } + + char_array_3[0] = static_cast( + (char_array_4[0] << 2u) + ((char_array_4[1] & 0x30u) >> 4u)); + char_array_3[1] = static_cast( + ((char_array_4[1] & 0xfu) << 4u) + ((char_array_4[2] & 0x3cu) >> 2u)); + char_array_3[2] = static_cast( + ((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(base64_chars.find(char_array_4[j])); + } + char_array_3[0] = static_cast( + (char_array_4[0] << 2u) + ((char_array_4[1] & 0x30u) >> 4u)); + char_array_3[1] = static_cast( + ((char_array_4[1] & 0xfu) << 4u) + ((char_array_4[2] & 0x3cu) >> 2u)); + char_array_3[2] = static_cast( + ((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 diff --git a/src/ballistica/generic/base64.h b/src/ballistica/generic/base64.h new file mode 100644 index 00000000..dcc5deef --- /dev/null +++ b/src/ballistica/generic/base64.h @@ -0,0 +1,16 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GENERIC_BASE64_H_ +#define BALLISTICA_GENERIC_BASE64_H_ + +#include + +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_ diff --git a/src/ballistica/generic/buffer.h b/src/ballistica/generic/buffer.h new file mode 100644 index 00000000..7ac2399b --- /dev/null +++ b/src/ballistica/generic/buffer.h @@ -0,0 +1,95 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GENERIC_BUFFER_H_ +#define BALLISTICA_GENERIC_BUFFER_H_ + +#include + +#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 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(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(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(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_ diff --git a/src/ballistica/generic/huffman.cc b/src/ballistica/generic/huffman.cc new file mode 100644 index 00000000..18e1550a --- /dev/null +++ b/src/ballistica/generic/huffman.cc @@ -0,0 +1,590 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/generic/huffman.h" + +#include +#include + +#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& src) + -> std::vector { +#if BA_HUFFMAN_NET_COMPRESSION + + auto length = static_cast(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(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 out(length_out, 0); + + // first byte gives our number of empty trailing bits + char* ptr = reinterpret_cast(out.data()); + int bit = 0; + + *ptr = static_cast(8 - bit_count % 8); + if (*ptr == 8) { + *ptr = 0; + } + ptr++; + + for (uint32_t i = 0; i < length; i++) { + DoWriteBits(&ptr, &bit, nodes_[static_cast(data[i])].val, + nodes_[static_cast(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(out.data()) == length_out + || (ptr - reinterpret_cast(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& src) + -> std::vector { +#if BA_HUFFMAN_NET_COMPRESSION + + auto length = static_cast(src.size()); + BA_PRECONDITION(length > 0); + + const char* data = (const char*)src.data(); + + auto remainder = static_cast(*data & 0x0F); + bool compressed = *data >> 7; + + if (compressed) { + std::vector 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((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((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(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(ptr[bit / 8]); + } else { + BA_PRECONDITION((bit / 8 + 1) < (length - 1)); + val = (static_cast(ptr[bit / 8]) >> bit % 8) + | (static_cast(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(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(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(data[i])].val, + nodes[static_cast(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(ptr[bit / 8]); + else + val = (static_cast(ptr[bit / 8]) >> bit % 8) + | (static_cast(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(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(*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(node_count - 255); + nodes_[smallest2].parent = static_cast(node_count - 255); + nodes_[node_count].right_child = static_cast(smallest1); + nodes_[node_count].left_child = static_cast(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(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(i << 1); + } else { + nodes_[i].val = static_cast( + 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 diff --git a/src/ballistica/generic/huffman.h b/src/ballistica/generic/huffman.h new file mode 100644 index 00000000..44a17f53 --- /dev/null +++ b/src/ballistica/generic/huffman.h @@ -0,0 +1,61 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GENERIC_HUFFMAN_H_ +#define BALLISTICA_GENERIC_HUFFMAN_H_ + +#include + +#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& src) -> std::vector; + auto decompress(const std::vector& src) -> std::vector; + 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_ diff --git a/src/ballistica/generic/json.cc b/src/ballistica/generic/json.cc new file mode 100644 index 00000000..87a0184e --- /dev/null +++ b/src/ballistica/generic/json.cc @@ -0,0 +1,1105 @@ +// Copyright (c) 2011-2020 Eric Froemling +// Derived from code licensed as follows: + +/* + 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. +*/ + +/* cJSON */ +/* JSON parser in C. */ + +#include "ballistica/generic/json.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace ballistica { + +// Should tidy this up but don't want to risk breaking it at the moment. +#pragma clang diagnostic push +#pragma ide diagnostic ignored "hicpp-signed-bitwise" +#pragma ide diagnostic ignored "bugprone-narrowing-conversions" +#pragma ide diagnostic ignored "cppcoreguidelines-narrowing-conversions" + +static const char* ep; + +auto cJSON_GetErrorPtr() -> const char* { return ep; } + +static auto cJSON_strcasecmp(const char* s1, const char* s2) -> int { + if (!s1) return (s1 == s2) ? 0 : 1; + if (!s2) return 1; + for (; tolower(*s1) == tolower(*s2); ++s1, ++s2) + if (*s1 == 0) return 0; + return tolower(*(const unsigned char*)s1) + - tolower(*(const unsigned char*)s2); +} + +static void* (*cJSON_malloc)(size_t sz) = malloc; +static void (*cJSON_free)(void* ptr) = free; + +static auto cJSON_strdup(const char* str) -> char* { + size_t len; + char* copy; + + len = strlen(str) + 1; + if (!(copy = static_cast(cJSON_malloc(len)))) { + return nullptr; + } + memcpy(copy, str, len); + return copy; +} + +void cJSON_InitHooks(cJSON_Hooks* hooks) { + if (!hooks) { /* Reset hooks */ + cJSON_malloc = malloc; + cJSON_free = free; + return; + } + + cJSON_malloc = (hooks->malloc_fn) ? hooks->malloc_fn : malloc; + cJSON_free = (hooks->free_fn) ? hooks->free_fn : free; +} + +/* Internal constructor. */ +static auto cJSON_New_Item() -> cJSON* { + auto* node = static_cast(cJSON_malloc(sizeof(cJSON))); + if (node) memset(node, 0, sizeof(cJSON)); + return node; +} + +/* Delete a cJSON structure. */ +void cJSON_Delete(cJSON* c) { + cJSON* next; + while (c) { + next = c->next; + if (!(c->type & cJSON_IsReference) && c->child) cJSON_Delete(c->child); + if (!(c->type & cJSON_IsReference) && c->valuestring) + cJSON_free(c->valuestring); + if (c->string) cJSON_free(c->string); + cJSON_free(c); + c = next; + } +} + +/* Parse the input text to generate a number, and populate the result into item. + */ +static auto parse_number(cJSON* item, const char* num) -> const char* { + double n = 0, sign = 1, scale = 0; + int subscale = 0, signsubscale = 1; + + if (*num == '-') { + sign = -1; + num++; + } /* Has sign? */ + if (*num == '0') num++; /* is zero */ + if (*num >= '1' && *num <= '9') do + n = (n * 10.0f) + (*num++ - '0'); + while (*num >= '0' && *num <= '9'); /* Number? */ + if (*num == '.' && num[1] >= '0' && num[1] <= '9') { + num++; + do { + n = (n * 10.0f) + (*num++ - '0'); + scale--; + } while (*num >= '0' && *num <= '9'); + } /* Fractional part? */ + if (*num == 'e' || *num == 'E') /* Exponent? */ + { + num++; + if (*num == '+') + num++; + else if (*num == '-') { + signsubscale = -1; + num++; /* With sign? */ + } + while (*num >= '0' && *num <= '9') + subscale = (subscale * 10) + (*num++ - '0'); /* Number? */ + } + + n = sign * n + * pow(10.0f, + (scale + subscale * signsubscale)); /* number = +/- number.fraction + * 10^+/- exponent */ + + item->valuedouble = n; + item->valueint = (int)n; + item->type = cJSON_Number; + return num; +} + +/* Render the number nicely from the given item into a string. */ +static auto print_number(cJSON* item) -> char* { + char* str; + double d = item->valuedouble; + if (fabs(((double)item->valueint) - d) <= DBL_EPSILON && d <= INT_MAX + && d >= INT_MIN) { + str = (char*)cJSON_malloc(21); /* 2^64+1 can be represented in 21 chars. */ + if (str) sprintf(str, "%d", item->valueint); + } else { + str = (char*)cJSON_malloc(64); /* This is a nice tradeoff. */ + if (str) { + if (fabs(floor(d) - d) <= DBL_EPSILON && fabs(d) < 1.0e60) + sprintf(str, "%.0f", d); + else if (fabs(d) < 1.0e-6 || fabs(d) > 1.0e9) + sprintf(str, "%e", d); + else + sprintf(str, "%f", d); + } + } + return str; +} + +static auto parse_hex4(const char* str) -> unsigned { + unsigned h = 0; + if (*str >= '0' && *str <= '9') + h += (*str) - '0'; + else if (*str >= 'A' && *str <= 'F') + h += 10 + (*str) - 'A'; + else if (*str >= 'a' && *str <= 'f') + h += 10 + (*str) - 'a'; + else + return 0; + h = h << 4; + str++; + if (*str >= '0' && *str <= '9') + h += (*str) - '0'; + else if (*str >= 'A' && *str <= 'F') + h += 10 + (*str) - 'A'; + else if (*str >= 'a' && *str <= 'f') + h += 10 + (*str) - 'a'; + else + return 0; + h = h << 4; + str++; + if (*str >= '0' && *str <= '9') + h += (*str) - '0'; + else if (*str >= 'A' && *str <= 'F') + h += 10 + (*str) - 'A'; + else if (*str >= 'a' && *str <= 'f') + h += 10 + (*str) - 'a'; + else + return 0; + h = h << 4; + str++; + if (*str >= '0' && *str <= '9') + h += (*str) - '0'; + else if (*str >= 'A' && *str <= 'F') + h += 10 + (*str) - 'A'; + else if (*str >= 'a' && *str <= 'f') + h += 10 + (*str) - 'a'; + else + return 0; + return h; +} + +/* Parse the input text into an unescaped cstring, and populate item. */ +static const unsigned char firstByteMark[7] = {0x00, 0x00, 0xC0, 0xE0, + 0xF0, 0xF8, 0xFC}; +static auto parse_string(cJSON* item, const char* str) -> const char* { + const char* ptr = str + 1; + char* ptr2; + char* out; + size_t len = 0; + unsigned uc, uc2; + if (*str != '\"') { + ep = str; + return nullptr; + } /* not a string! */ + + while (*ptr != '\"' && *ptr && ++len) { + if (*ptr++ == '\\') { + ptr++; /* Skip escaped quotes. */ + } + } + + // This is how long we need for the string, roughly. + out = (char*)cJSON_malloc(len + 1); + if (!out) return nullptr; + + ptr = str + 1; + ptr2 = out; + while (*ptr != '\"' && *ptr) { + if (*ptr != '\\') { + *ptr2++ = *ptr++; + } else { + ptr++; + switch (*ptr) { + case 'b': + *ptr2++ = '\b'; + break; + case 'f': + *ptr2++ = '\f'; + break; + case 'n': + *ptr2++ = '\n'; + break; + case 'r': + *ptr2++ = '\r'; + break; + case 't': + *ptr2++ = '\t'; + break; + case 'u': /* transcode utf16 to utf8. */ + uc = parse_hex4(ptr + 1); + ptr += 4; /* get the unicode char. */ + + if ((uc >= 0xDC00 && uc <= 0xDFFF) || uc == 0) { + break; // check for invalid. + } + + // UTF16 surrogate pairs. + if (uc >= 0xD800 && uc <= 0xDBFF) { + if (ptr[1] != '\\' || ptr[2] != 'u') + break; /* missing second-half of surrogate. */ + uc2 = parse_hex4(ptr + 3); + ptr += 6; + if (uc2 < 0xDC00 || uc2 > 0xDFFF) + break; /* invalid second-half of surrogate. */ + uc = 0x10000 + (((uc & 0x3FF) << 10) | (uc2 & 0x3FF)); + } + + len = 4; + if (uc < 0x80) { + len = 1; + } else if (uc < 0x800) { + len = 2; + } else if (uc < 0x10000) { + len = 3; + } + ptr2 += len; + + switch (len) { + case 4: // NOLINT(bugprone-branch-clone) + *--ptr2 = static_cast((uc | 0x80) & 0xBF); + uc >>= 6; + case 3: + *--ptr2 = static_cast((uc | 0x80) & 0xBF); + uc >>= 6; + case 2: + *--ptr2 = static_cast((uc | 0x80) & 0xBF); + uc >>= 6; + case 1: + *--ptr2 = static_cast(uc | firstByteMark[len]); + default: + break; + } + ptr2 += len; + break; + default: + *ptr2++ = *ptr; + break; + } + ptr++; + } + } + *ptr2 = 0; + if (*ptr == '\"') { + ptr++; + } + item->valuestring = out; + item->type = cJSON_String; + return ptr; +} + +/* Render the cstring provided to an escaped version that can be printed. */ +static auto print_string_ptr(const char* str) -> char* { + const char* ptr; + char *ptr2, *out; + size_t len = 0; + unsigned char token; + + if (!str) { + return cJSON_strdup(""); + } + ptr = str; + while ((token = static_cast(*ptr)) && ++len) { + if (strchr("\"\\\b\f\n\r\t", token)) { + len++; + } else if (token < 32) { + len += 5; + } + ptr++; + } + + out = (char*)cJSON_malloc(len + 3); + if (!out) { + return nullptr; + } + + ptr2 = out; + ptr = str; + *ptr2++ = '\"'; + while (*ptr) { + if ((unsigned char)*ptr > 31 && *ptr != '\"' && *ptr != '\\') { + *ptr2++ = *ptr++; + } else { + *ptr2++ = '\\'; + switch (token = static_cast(*ptr++)) { + case '\\': + *ptr2++ = '\\'; + break; + case '\"': + *ptr2++ = '\"'; + break; + case '\b': + *ptr2++ = 'b'; + break; + case '\f': + *ptr2++ = 'f'; + break; + case '\n': + *ptr2++ = 'n'; + break; + case '\r': + *ptr2++ = 'r'; + break; + case '\t': + *ptr2++ = 't'; + break; + default: + sprintf(ptr2, "u%04x", token); + ptr2 += 5; + break; /* escape and print */ + } + } + } + *ptr2++ = '\"'; + *ptr2 = 0; + return out; +} +/* Invote print_string_ptr (which is useful) on an item. */ +static auto print_string(cJSON* item) -> char* { + return print_string_ptr(item->valuestring); +} + +/* Predeclare these prototypes. */ +static auto parse_value(cJSON* item, const char* value) -> const char*; +static auto print_value(cJSON* item, int depth, int fmt) -> char*; +static auto parse_array(cJSON* item, const char* value) -> const char*; +static auto print_array(cJSON* item, int depth, int fmt) -> char*; +static auto parse_object(cJSON* item, const char* value) -> const char*; +static auto print_object(cJSON* item, int depth, int fmt) -> char*; + +/* Utility to jump whitespace and cr/lf */ +static auto skip(const char* in) -> const char* { + while (in && *in && (unsigned char)*in <= 32) in++; + return in; +} + +/* Parse an object - create a new root, and populate. */ +auto cJSON_ParseWithOpts(const char* value, const char** return_parse_end, + int require_null_terminated) -> cJSON* { + cJSON* c = cJSON_New_Item(); + ep = nullptr; + if (!c) { + return nullptr; /* memory fail */ + } + + const char* end = parse_value(c, skip(value)); + if (!end) { + cJSON_Delete(c); + return nullptr; + } /* parse failure. ep is set. */ + + /* if we require null-terminated JSON without appended garbage, skip and then + * check for a null terminator */ + if (require_null_terminated) { + end = skip(end); + if (*end) { + cJSON_Delete(c); + ep = end; + return nullptr; + } + } + if (return_parse_end) *return_parse_end = end; + return c; +} +/* Default options for cJSON_Parse */ +auto cJSON_Parse(const char* value) -> cJSON* { + return cJSON_ParseWithOpts(value, nullptr, 0); +} + +/* Render a cJSON item/entity/structure to text. */ +auto cJSON_Print(cJSON* item) -> char* { return print_value(item, 0, 1); } +auto cJSON_PrintUnformatted(cJSON* item) -> char* { + return print_value(item, 0, 0); +} + +/* Parser core - when encountering text, process appropriately. */ +static auto parse_value(cJSON* item, const char* value) -> const char* { + if (!value) { + return nullptr; /* Fail on null. */ + } + if (!strncmp(value, "null", 4)) { + item->type = cJSON_NULL; + return value + 4; + } + if (!strncmp(value, "false", 5)) { + item->type = cJSON_False; + return value + 5; + } + if (!strncmp(value, "true", 4)) { + item->type = cJSON_True; + item->valueint = 1; + return value + 4; + } + if (*value == '\"') { + return parse_string(item, value); + } + if (*value == '-' || (*value >= '0' && *value <= '9')) { + return parse_number(item, value); + } + if (*value == '[') { + return parse_array(item, value); + } + if (*value == '{') { + return parse_object(item, value); + } + + ep = value; + return nullptr; /* failure. */ +} + +/* Render a value to text. */ +static auto print_value(cJSON* item, int depth, int fmt) -> char* { + char* out = nullptr; + if (!item) { + return nullptr; + } + switch ((item->type) & 255) { + case cJSON_NULL: + out = cJSON_strdup("null"); + break; + case cJSON_False: + out = cJSON_strdup("false"); + break; + case cJSON_True: + out = cJSON_strdup("true"); + break; + case cJSON_Number: + out = print_number(item); + break; + case cJSON_String: + out = print_string(item); + break; + case cJSON_Array: + out = print_array(item, depth, fmt); + break; + case cJSON_Object: + out = print_object(item, depth, fmt); + break; + default: + break; + } + return out; +} + +/* Build an array from input text. */ +static auto parse_array(cJSON* item, const char* value) -> const char* { + cJSON* child; + if (*value != '[') { + ep = value; + return nullptr; + } /* not an array! */ + + item->type = cJSON_Array; + value = skip(value + 1); + if (*value == ']') { + return value + 1; /* empty array. */ + } + + item->child = child = cJSON_New_Item(); + if (!item->child) { + return nullptr; /* memory fail */ + } + value = skip( + parse_value(child, skip(value))); /* skip any spacing, get the value. */ + if (!value) return nullptr; + + while (*value == ',') { + cJSON* new_item; + if (!(new_item = cJSON_New_Item())) { + return nullptr; /* memory fail */ + } + child->next = new_item; + new_item->prev = child; + child = new_item; + value = skip(parse_value(child, skip(value + 1))); + if (!value) { + return nullptr; /* memory fail */ + } + } + + if (*value == ']') { + return value + 1; /* end of array */ + } + ep = value; + return nullptr; /* malformed. */ +} + +/* Render an array to text */ +static auto print_array(cJSON* item, int depth, int fmt) -> char* { + char** entries; + char *out = nullptr, *ptr, *ret; + size_t len = 5; + cJSON* child = item->child; + int numentries = 0, i = 0, fail = 0; + + /* How many entries in the array? */ + while (child) { + numentries++; + child = child->next; + } + /* Explicitly handle numentries==0 */ + if (!numentries) { + out = (char*)cJSON_malloc(3); + if (out) { + strcpy(out, "[]"); // NOLINT + } + return out; + } + /* Allocate an array to hold the values for each */ + entries = (char**)cJSON_malloc(numentries * sizeof(char*)); + if (!entries) { + return nullptr; + } + memset(entries, 0, numentries * sizeof(char*)); + /* Retrieve all the results: */ + child = item->child; + while (child && !fail) { + ret = print_value(child, depth + 1, fmt); + entries[i++] = ret; + if (ret) + len += strlen(ret) + 2 + (fmt ? 1 : 0); + else + fail = 1; + child = child->next; + } + + /* If we didn't fail, try to malloc the output string */ + if (!fail) { + out = (char*)cJSON_malloc(len); + } + /* If that fails, we fail. */ + if (!out) { + fail = 1; + } + + /* Handle failure. */ + if (fail) { + for (i = 0; i < numentries; i++) + if (entries[i]) cJSON_free(entries[i]); + cJSON_free(entries); + return nullptr; + } + + /* Compose the output array. */ + *out = '['; + ptr = out + 1; + *ptr = 0; + for (i = 0; i < numentries; i++) { + strcpy(ptr, entries[i]); // NOLINT + ptr += strlen(entries[i]); + if (i != numentries - 1) { + *ptr++ = ','; + if (fmt) *ptr++ = ' '; + *ptr = 0; + } + cJSON_free(entries[i]); + } + cJSON_free(entries); + *ptr++ = ']'; + *ptr = 0; + return out; +} + +/* Build an object from the text. */ +static auto parse_object(cJSON* item, const char* value) -> const char* { + cJSON* child; + if (*value != '{') { + ep = value; + return nullptr; + } /* not an object! */ + + item->type = cJSON_Object; + value = skip(value + 1); + if (*value == '}') return value + 1; /* empty array. */ + + item->child = child = cJSON_New_Item(); + if (!item->child) return nullptr; + value = skip(parse_string(child, skip(value))); + if (!value) return nullptr; + child->string = child->valuestring; + child->valuestring = nullptr; + if (*value != ':') { + ep = value; + return nullptr; + } /* fail! */ + value = skip(parse_value( + child, skip(value + 1))); /* skip any spacing, get the value. */ + if (!value) return nullptr; + + while (*value == ',') { + cJSON* new_item; + if (!(new_item = cJSON_New_Item())) return nullptr; /* memory fail */ + child->next = new_item; + new_item->prev = child; + child = new_item; + value = skip(parse_string(child, skip(value + 1))); + if (!value) return nullptr; + child->string = child->valuestring; + child->valuestring = nullptr; + if (*value != ':') { + ep = value; + return nullptr; + } /* fail! */ + value = skip(parse_value( + child, skip(value + 1))); /* skip any spacing, get the value. */ + if (!value) return nullptr; + } + + if (*value == '}') return value + 1; /* end of array */ + ep = value; + return nullptr; /* malformed. */ +} + +/* Render an object to text. */ +static auto print_object(cJSON* item, int depth, int fmt) -> char* { + char *out = nullptr, *ptr, *ret, *str; + size_t len = 7; + int i = 0; + int j; + cJSON* child = item->child; + int numentries = 0, fail = 0; + /* Count the number of entries. */ + while (child) { + numentries++; + child = child->next; + } + // Explicitly handle empty object case. + if (!numentries) { + out = (char*)cJSON_malloc(static_cast(fmt ? depth + 4 : 3)); + if (!out) { + return nullptr; + } + ptr = out; + *ptr++ = '{'; + if (fmt) { + *ptr++ = '\n'; + for (i = 0; i < depth - 1; i++) *ptr++ = '\t'; + } + *ptr++ = '}'; + *ptr = 0; + return out; + } + + // Allocate space for the names and the objects. + char** entries = (char**)cJSON_malloc(numentries * sizeof(char*)); + if (!entries) return nullptr; + char** names = (char**)cJSON_malloc(numentries * sizeof(char*)); + if (!names) { + cJSON_free(entries); + return nullptr; + } + memset(entries, 0, sizeof(char*) * numentries); + memset(names, 0, sizeof(char*) * numentries); + + // Collect all the results into our arrays. + child = item->child; + depth++; + if (fmt) len += depth; + while (child) { + names[i] = str = print_string_ptr(child->string); + entries[i++] = ret = print_value(child, depth, fmt); + if (str && ret) + len += strlen(ret) + strlen(str) + 2 + (fmt ? 2 + depth : 0); + else + fail = 1; + child = child->next; + } + + // Try to allocate the output string. + if (!fail) out = (char*)cJSON_malloc(len); + if (!out) fail = 1; + + // Handle failure. + if (fail) { + for (i = 0; i < numentries; i++) { + if (names[i]) cJSON_free(names[i]); + if (entries[i]) cJSON_free(entries[i]); + } + cJSON_free(names); + cJSON_free(entries); + return nullptr; + } + + // Compose the output. + *out = '{'; + ptr = out + 1; + if (fmt) *ptr++ = '\n'; + *ptr = 0; + for (i = 0; i < numentries; i++) { + if (fmt) + for (j = 0; j < depth; j++) *ptr++ = '\t'; + strcpy(ptr, names[i]); // NOLINT + ptr += strlen(names[i]); + *ptr++ = ':'; + if (fmt) *ptr++ = '\t'; + strcpy(ptr, entries[i]); // NOLINT + ptr += strlen(entries[i]); + if (i != numentries - 1) *ptr++ = ','; + if (fmt) *ptr++ = '\n'; + *ptr = 0; + cJSON_free(names[i]); + cJSON_free(entries[i]); + } + + cJSON_free(names); + cJSON_free(entries); + if (fmt) + for (i = 0; i < depth - 1; i++) *ptr++ = '\t'; + *ptr++ = '}'; + *ptr = 0; + return out; +} + +// Get Array size/item / object item. +auto cJSON_GetArraySize(cJSON* array) -> int { + cJSON* c = array->child; + int i = 0; + while (c) { + i++; + c = c->next; + } + return i; +} +auto cJSON_GetArrayItem(cJSON* array, int item) -> cJSON* { + cJSON* c = array->child; + while (c && item > 0) { + item--; + c = c->next; + } + return c; +} +auto cJSON_GetObjectItem(cJSON* object, const char* string) -> cJSON* { + cJSON* c = object->child; + while (c && cJSON_strcasecmp(c->string, string)) c = c->next; + return c; +} + +// Utility for array list handling. +static void suffix_object(cJSON* prev, cJSON* item) { + prev->next = item; + item->prev = prev; +} +// Utility for handling references. +static auto create_reference(cJSON* item) -> cJSON* { + cJSON* ref = cJSON_New_Item(); + if (!ref) return nullptr; + memcpy(ref, item, sizeof(cJSON)); + ref->string = nullptr; + ref->type |= cJSON_IsReference; + ref->next = ref->prev = nullptr; + return ref; +} + +// Add item to array/object. +void cJSON_AddItemToArray(cJSON* array, cJSON* item) { + cJSON* c = array->child; + if (!item) return; + if (!c) { + array->child = item; + } else { + while (c && c->next) c = c->next; + suffix_object(c, item); + } +} +void cJSON_AddItemToObject(cJSON* object, const char* string, cJSON* item) { + if (!item) return; + if (item->string) cJSON_free(item->string); + item->string = cJSON_strdup(string); + cJSON_AddItemToArray(object, item); +} +void cJSON_AddItemReferenceToArray(cJSON* array, cJSON* item) { + cJSON_AddItemToArray(array, create_reference(item)); +} +void cJSON_AddItemReferenceToObject(cJSON* object, const char* string, + cJSON* item) { + cJSON_AddItemToObject(object, string, create_reference(item)); +} + +auto cJSON_DetachItemFromArray(cJSON* array, int which) -> cJSON* { + cJSON* c = array->child; + while (c && which > 0) { + c = c->next; + which--; + } + if (!c) return nullptr; + if (c->prev) c->prev->next = c->next; + if (c->next) c->next->prev = c->prev; + if (c == array->child) array->child = c->next; + c->prev = c->next = nullptr; + return c; +} +void cJSON_DeleteItemFromArray(cJSON* array, int which) { + cJSON_Delete(cJSON_DetachItemFromArray(array, which)); +} +auto cJSON_DetachItemFromObject(cJSON* object, const char* string) -> cJSON* { + int i = 0; + cJSON* c = object->child; + while (c && cJSON_strcasecmp(c->string, string)) { + i++; + c = c->next; + } + if (c) return cJSON_DetachItemFromArray(object, i); + return nullptr; +} +void cJSON_DeleteItemFromObject(cJSON* object, const char* string) { + cJSON_Delete(cJSON_DetachItemFromObject(object, string)); +} + +// Replace array/object items with new ones. +void cJSON_ReplaceItemInArray(cJSON* array, int which, cJSON* newitem) { + cJSON* c = array->child; + while (c && which > 0) { + c = c->next; + which--; + } + if (!c) return; + newitem->next = c->next; + newitem->prev = c->prev; + if (newitem->next) newitem->next->prev = newitem; + if (c == array->child) + array->child = newitem; + else + newitem->prev->next = newitem; + c->next = c->prev = nullptr; + cJSON_Delete(c); +} +void cJSON_ReplaceItemInObject(cJSON* object, const char* string, + cJSON* newitem) { + int i = 0; + cJSON* c = object->child; + while (c && cJSON_strcasecmp(c->string, string)) { + i++; + c = c->next; + } + if (c) { + newitem->string = cJSON_strdup(string); + cJSON_ReplaceItemInArray(object, i, newitem); + } +} + +// Create basic types. +auto cJSON_CreateNull() -> cJSON* { + cJSON* item = cJSON_New_Item(); + if (item) item->type = cJSON_NULL; + return item; +} +auto cJSON_CreateTrue() -> cJSON* { + cJSON* item = cJSON_New_Item(); + if (item) item->type = cJSON_True; + return item; +} +auto cJSON_CreateFalse() -> cJSON* { + cJSON* item = cJSON_New_Item(); + if (item) item->type = cJSON_False; + return item; +} +auto cJSON_CreateBool(int b) -> cJSON* { + cJSON* item = cJSON_New_Item(); + if (item) item->type = b ? cJSON_True : cJSON_False; + return item; +} +auto cJSON_CreateNumber(double num) -> cJSON* { + cJSON* item = cJSON_New_Item(); + if (item) { + item->type = cJSON_Number; + item->valuedouble = num; + item->valueint = (int)num; + } + return item; +} +auto cJSON_CreateString(const char* string) -> cJSON* { + cJSON* item = cJSON_New_Item(); + if (item) { + item->type = cJSON_String; + item->valuestring = cJSON_strdup(string); + } + return item; +} +auto cJSON_CreateArray() -> cJSON* { + cJSON* item = cJSON_New_Item(); + if (item) item->type = cJSON_Array; + return item; +} +auto cJSON_CreateObject() -> cJSON* { + cJSON* item = cJSON_New_Item(); + if (item) item->type = cJSON_Object; + return item; +} + +// Create Arrays. +auto cJSON_CreateIntArray(const int* numbers, int count) -> cJSON* { + int i; + cJSON *n, *p = nullptr, *a = cJSON_CreateArray(); + for (i = 0; a && i < count; i++) { + n = cJSON_CreateNumber(numbers[i]); + if (!i) + a->child = n; + else + suffix_object(p, n); + p = n; + } + return a; +} +auto cJSON_CreateFloatArray(const float* numbers, int count) -> cJSON* { + int i; + cJSON *n, *p = nullptr, *a = cJSON_CreateArray(); + for (i = 0; a && i < count; i++) { + n = cJSON_CreateNumber(numbers[i]); + if (!i) + a->child = n; + else + suffix_object(p, n); + p = n; + } + return a; +} +auto cJSON_CreateDoubleArray(const double* numbers, int count) -> cJSON* { + int i; + cJSON *n, *p = nullptr, *a = cJSON_CreateArray(); + for (i = 0; a && i < count; i++) { + n = cJSON_CreateNumber(numbers[i]); + if (!i) + a->child = n; + else + suffix_object(p, n); + p = n; + } + return a; +} +auto cJSON_CreateStringArray(const char** strings, int count) -> cJSON* { + int i; + cJSON *n, *p = nullptr, *a = cJSON_CreateArray(); + for (i = 0; a && i < count; i++) { + n = cJSON_CreateString(strings[i]); + if (!i) + a->child = n; + else + suffix_object(p, n); + p = n; + } + return a; +} + +// Duplication. +auto cJSON_Duplicate(cJSON* item, int recurse) -> cJSON* { + cJSON *newitem, *cptr, *nptr = nullptr, *newchild; + /* Bail on bad ptr */ + if (!item) return nullptr; + /* Create new item */ + newitem = cJSON_New_Item(); + if (!newitem) return nullptr; + /* Copy over all vars */ + newitem->type = item->type & (~cJSON_IsReference); + newitem->valueint = item->valueint; + newitem->valuedouble = item->valuedouble; + if (item->valuestring) { + newitem->valuestring = cJSON_strdup(item->valuestring); + if (!newitem->valuestring) { + cJSON_Delete(newitem); + return nullptr; + } + } + if (item->string) { + newitem->string = cJSON_strdup(item->string); + if (!newitem->string) { + cJSON_Delete(newitem); + return nullptr; + } + } + /* If non-recursive, then we're done! */ + if (!recurse) return newitem; + /* Walk the ->next chain for the child. */ + cptr = item->child; + while (cptr) { + newchild = cJSON_Duplicate( + cptr, 1); /* Duplicate (with recurse) each item in the ->next chain */ + if (!newchild) { + cJSON_Delete(newitem); + return nullptr; + } + if (nptr) { + nptr->next = newchild; + newchild->prev = nptr; + nptr = newchild; + } /* If newitem->child already set, then crosswire ->prev and ->next and + move on */ + else { + newitem->child = newchild; + nptr = newchild; + } /* Set newitem->child and move to it */ + cptr = cptr->next; + } + return newitem; +} + +void cJSON_Minify(char* json) { + char* into = json; + while (*json) { + if (*json == ' ') + json++; // NOLINT(bugprone-branch-clone) + else if (*json == '\t') + json++; // Whitespace characters. + else if (*json == '\r') + json++; + else if (*json == '\n') + json++; + else if (*json == '/' && json[1] == '/') + while (*json && *json != '\n') + json++; // double-slash comments, to end of line. + else if (*json == '/' && json[1] == '*') { + while (*json && !(*json == '*' && json[1] == '/')) json++; + json += 2; + } // multiline comments. + else if (*json == '\"') { + *into++ = *json++; + while (*json && *json != '\"') { + if (*json == '\\') *into++ = *json++; + *into++ = *json++; + } + *into++ = *json++; + } // string literals, which are \" sensitive. + else { + *into++ = *json++; // All other characters. + } + } + *into = 0; // and null-terminate. +} + +#pragma clang diagnostic pop + +} // namespace ballistica diff --git a/src/ballistica/generic/json.h b/src/ballistica/generic/json.h new file mode 100644 index 00000000..7577f178 --- /dev/null +++ b/src/ballistica/generic/json.h @@ -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_ diff --git a/src/ballistica/generic/lambda_runnable.h b/src/ballistica/generic/lambda_runnable.h new file mode 100644 index 00000000..c89ed72d --- /dev/null +++ b/src/ballistica/generic/lambda_runnable.h @@ -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 +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 +auto NewLambdaRunnable(const F& lambda) -> Object::Ref { + return Object::New>(lambda); +} + +// Same but returns the raw pointer instead of a ref; +// (used when passing across threads). +template +auto NewLambdaRunnableRaw(const F& lambda) -> Runnable* { + return Object::NewDeferred>(lambda); +} + +} // namespace ballistica + +#endif // BALLISTICA_GENERIC_LAMBDA_RUNNABLE_H_ diff --git a/src/ballistica/generic/real_timer.h b/src/ballistica/generic/real_timer.h new file mode 100644 index 00000000..8e324221 --- /dev/null +++ b/src/ballistica/generic/real_timer.h @@ -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 +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(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* timer) + : delegate_(delegate), timer_(timer) {} + void Run() override { delegate_->HandleRealTimerExpired(timer_); } + + private: + RealTimer* timer_; + T* delegate_; + }; + int timer_id_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GENERIC_REAL_TIMER_H_ diff --git a/src/ballistica/generic/runnable.cc b/src/ballistica/generic/runnable.cc new file mode 100644 index 00000000..108a3275 --- /dev/null +++ b/src/ballistica/generic/runnable.cc @@ -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 diff --git a/src/ballistica/generic/runnable.h b/src/ballistica/generic/runnable.h new file mode 100644 index 00000000..0d600fba --- /dev/null +++ b/src/ballistica/generic/runnable.h @@ -0,0 +1,23 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GENERIC_RUNNABLE_H_ +#define BALLISTICA_GENERIC_RUNNABLE_H_ + +#include + +#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_ diff --git a/src/ballistica/generic/timer.cc b/src/ballistica/generic/timer.cc new file mode 100644 index 00000000..a45e40c2 --- /dev/null +++ b/src/ballistica/generic/timer.cc @@ -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 diff --git a/src/ballistica/generic/timer.h b/src/ballistica/generic/timer.h new file mode 100644 index 00000000..9609286b --- /dev/null +++ b/src/ballistica/generic/timer.h @@ -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_; + // FIXME: Shouldn't have friend classes in different files. + friend class TimerList; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GENERIC_TIMER_H_ diff --git a/src/ballistica/generic/timer_list.cc b/src/ballistica/generic/timer_list.cc new file mode 100644 index 00000000..05143e00 --- /dev/null +++ b/src/ballistica/generic/timer_list.cc @@ -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) -> 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 diff --git a/src/ballistica/generic/timer_list.h b/src/ballistica/generic/timer_list.h new file mode 100644 index 00000000..daf5d7df --- /dev/null +++ b/src/ballistica/generic/timer_list.h @@ -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 +#include + +#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) -> 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_ diff --git a/src/ballistica/generic/utf8.cc b/src/ballistica/generic/utf8.cc new file mode 100644 index 00000000..ce668344 --- /dev/null +++ b/src/ballistica/generic/utf8.cc @@ -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 +#include +#include +#include +#if _WIN32 || _WIN64 +#include +#else +#include +#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((ch >> 6) | 0xC0); + *dest++ = static_cast((ch & 0x3F) | 0x80); + } else if (ch < 0x10000) { + if (dest >= dest_end - 2) return i; + *dest++ = static_cast((ch >> 12) | 0xE0); + *dest++ = static_cast(((ch >> 6) & 0x3F) | 0x80); + *dest++ = static_cast((ch & 0x3F) | 0x80); + } else if (ch < 0x110000) { + if (dest >= dest_end - 3) return i; + *dest++ = static_cast((ch >> 18) | 0xF0); + *dest++ = static_cast(((ch >> 12) & 0x3F) | 0x80); + *dest++ = static_cast(((ch >> 6) & 0x3F) | 0x80); + *dest++ = static_cast((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((ch >> 6) | 0xC0); + dest[1] = static_cast((ch & 0x3F) | 0x80); + return 2; + } + if (ch < 0x10000) { + dest[0] = static_cast((ch >> 12) | 0xE0); + dest[1] = static_cast(((ch >> 6) & 0x3F) | 0x80); + dest[2] = static_cast((ch & 0x3F) | 0x80); + return 3; + } + if (ch < 0x110000) { + dest[0] = static_cast((ch >> 18) | 0xF0); + dest[1] = static_cast(((ch >> 12) & 0x3F) | 0x80); + dest[2] = static_cast(((ch >> 6) & 0x3F) | 0x80); + dest[3] = static_cast((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(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(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(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(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(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(sz), "\\n"); + else if (ch == L'\t') + return snprintf(buf, static_cast(sz), "\\t"); + else if (ch == L'\r') + return snprintf(buf, static_cast(sz), "\\r"); + else if (ch == L'\b') + return snprintf(buf, static_cast(sz), "\\b"); + else if (ch == L'\f') + return snprintf(buf, static_cast(sz), "\\f"); + else if (ch == L'\v') + return snprintf(buf, static_cast(sz), "\\v"); + else if (ch == L'\a') + return snprintf(buf, static_cast(sz), "\\a"); + else if (ch == L'\\') + return snprintf(buf, static_cast(sz), "\\\\"); + else if (ch < 32 || ch == 0x7f) + return snprintf(buf, static_cast(sz), "\\x%hhX", (unsigned char)ch); + else if (ch > 0xFFFF) + return snprintf(buf, static_cast(sz), "\\U%.8X", (uint32_t)ch); + else if (ch >= 0x80 && ch <= 0xFFFF) + return snprintf(buf, static_cast(sz), "\\u%.4hX", + (unsigned short)ch); + + return snprintf(buf, static_cast(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(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(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(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 diff --git a/src/ballistica/generic/utf8.h b/src/ballistica/generic/utf8.h new file mode 100644 index 00000000..1d7a46ed --- /dev/null +++ b/src/ballistica/generic/utf8.h @@ -0,0 +1,85 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GENERIC_UTF8_H_ +#define BALLISTICA_GENERIC_UTF8_H_ + +#include + +#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_ diff --git a/src/ballistica/generic/utils.cc b/src/ballistica/generic/utils.cc new file mode 100644 index 00000000..fbe93847 --- /dev/null +++ b/src/ballistica/generic/utils.cc @@ -0,0 +1,639 @@ +// Copyright (c) 2011-2020 Eric Froemling + +#include "ballistica/generic/utils.h" + +#include +#include +#include +#include +#include +#include + +#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() + == "ballistica::AppGlobals *" + || static_type_name_constexpr() + == "ballistica::AppGlobals*" + || static_type_name_constexpr() + == "class ballistica::AppGlobals*"); + Object::Ref testnode{}; + static_assert( + static_type_name_constexpr() + == "ballistica::Object::Ref" + || static_type_name_constexpr() + == "class ballistica::Object::Ref"); + + // int testint{}; + // static_assert(static_type_name_constexpr() == "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() + "' debug_full is '" + + static_type_name(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(rand()) / RAND_MAX; // NOLINT + precalc_rands_2[i] = static_cast(rand()) / RAND_MAX; // NOLINT + precalc_rands_3[i] = static_cast(rand()) / RAND_MAX; // NOLINT + } +#endif + huffman_ = std::make_unique(); +} + +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(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(strlen(str)); + unsigned char c, c2 = 0, c3, c4; + std::string to; + to.reserve(static_cast(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(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 unichars) -> std::string { + int buffer_size = static_cast(unichars.size() * 4 + 1); + // at most 4 chars per unichar plus ending zero + std::vector buffer(static_cast(buffer_size)); + int len = u8_toutf8(buffer.data(), buffer_size, unichars.data(), + static_cast(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 { + std::string s = GetValidUTF8(s_in.c_str(), loc); + // worst case every char is a character (plus trailing 0) + std::vector vals(s.size() + 1); + int converted = u8_toucs(&vals[0], static_cast(vals.size()), s.c_str(), + static_cast(s.size())); + vals.resize(static_cast(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* g_random_names_list = nullptr; + +auto Utils::GetRandomNameList() -> const std::list& { + assert(InGameThread()); + if (!g_random_names_list) { + // this will init the list with our default english names + SetRandomNameList(std::list(1, "DEFAULT_NAMES")); + } + return *g_random_names_list; +} + +void Utils::SetRandomNameList(const std::list& custom_names) { + assert(InGameThread()); + if (!g_random_names_list) { + g_random_names_list = new std::list; + } 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(s_in.size()); + std::string s_out; + s_out.resize(static_cast(s_size) * 2); + for (uint32_t i = 0; i < s_size; i++) { + s_out[i * 2] = + static_cast(HEXVAL((static_cast(s_in[i])) >> 4u)); + s_out[i * 2 + 1] = + static_cast(HEXVAL((static_cast(s_in[i]) & 15u))); + } + return s_out; +} +#undef HEXVAL + +static auto FromHex(const std::string& s_in) -> std::string { + int s_size = static_cast(s_in.size()); + BA_PRECONDITION(s_size % 2 == 0); + s_size /= 2; + std::string s_out; + s_out.resize(static_cast(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((val - '0') << 4u); + } else if (val >= 'A' && val <= 'F') { + s_out[i] = static_cast((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(static_cast(s_out[i]) | (val - '0')); + } else if (val >= 'A' && val <= 'F') { + s_out[i] = static_cast(static_cast(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(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(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(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(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(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(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 diff --git a/src/ballistica/generic/utils.h b/src/ballistica/generic/utils.h new file mode 100644 index 00000000..8d401297 --- /dev/null +++ b/src/ballistica/generic/utils.h @@ -0,0 +1,387 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GENERIC_UTILS_H_ +#define BALLISTICA_GENERIC_UTILS_H_ + +#include +#include +#include +#include +#include + +// 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 +#else +#include +#endif +#include + +#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&; + static void SetRandomNameList(const std::list& names); + + static auto UnicodeFromUTF8(const std::string& s, const char* loc) + -> std::vector; + static auto UTF8FromUnicode(std::vector 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 + + 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(s | (m >> 13)); + } else if (e == 0xff - (127 - 15)) { + if (m == 0) { + // Inf + return static_cast(s | 0x7c00); + } else { + // NAN + m >>= 13; + return static_cast(s | 0x7c00 | m | (m == 0)); + } + } else { + if (e > 30) { + // Overflow + return static_cast(s | 0x7c00); + } + + return static_cast(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(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((s << 31) | 0x7f800000); + } else { // NaN + return static_cast((s << 31) | 0x7f800000 | (m << 13)); + } + } + e = e + (127 - 15); + m = m << 13; + return static_cast((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(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(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(**b); + *i1 = static_cast(i & 0x01); + if (i2) *i2 = static_cast((i >> 1) & 0x01); + if (i3) *i3 = static_cast((i >> 2) & 0x01); + if (i4) *i4 = static_cast((i >> 3) & 0x01); + if (i5) *i5 = static_cast((i >> 4) & 0x01); + if (i6) *i6 = static_cast((i >> 5) & 0x01); + if (i7) *i7 = static_cast((i >> 6) & 0x01); + if (i8) *i8 = static_cast((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 + 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_; +}; + +} // namespace ballistica + +#endif // BALLISTICA_GENERIC_UTILS_H_ diff --git a/tools/batools/updateproject.py b/tools/batools/updateproject.py index 6ac8a2c1..feb428d3 100755 --- a/tools/batools/updateproject.py +++ b/tools/batools/updateproject.py @@ -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 diff --git a/tools/efrotools/__init__.py b/tools/efrotools/__init__.py index 6fac48dc..83a6a5ca 100644 --- a/tools/efrotools/__init__.py +++ b/tools/efrotools/__init__.py @@ -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.""" diff --git a/tools/efrotools/code.py b/tools/efrotools/code.py index 2b8ba44e..34b79035 100644 --- a/tools/efrotools/code.py +++ b/tools/efrotools/code.py @@ -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(