mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-27 01:13:13 +08:00
Adding more C++ sources
This commit is contained in:
parent
0804d7fcbb
commit
87b6716d59
@ -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"
|
||||
}
|
||||
15
.idea/dictionaries/ericf.xml
generated
15
.idea/dictionaries/ericf.xml
generated
@ -29,8 +29,8 @@
|
||||
<w>achname</w>
|
||||
<w>achs</w>
|
||||
<w>acinstance</w>
|
||||
<w>ack</w>
|
||||
<w>ack'ed</w>
|
||||
<w>ack</w>
|
||||
<w>acked</w>
|
||||
<w>acks</w>
|
||||
<w>acnt</w>
|
||||
@ -151,8 +151,8 @@
|
||||
<w>bacommon</w>
|
||||
<w>badguy</w>
|
||||
<w>bafoundation</w>
|
||||
<w>ballistica</w>
|
||||
<w>ballistica's</w>
|
||||
<w>ballistica</w>
|
||||
<w>ballisticacore</w>
|
||||
<w>ballisticacorecb</w>
|
||||
<w>bamaster</w>
|
||||
@ -793,8 +793,8 @@
|
||||
<w>gamedata</w>
|
||||
<w>gameinstance</w>
|
||||
<w>gamemap</w>
|
||||
<w>gamepad</w>
|
||||
<w>gamepad's</w>
|
||||
<w>gamepad</w>
|
||||
<w>gamepadadvanced</w>
|
||||
<w>gamepads</w>
|
||||
<w>gamepadselect</w>
|
||||
@ -1177,8 +1177,8 @@
|
||||
<w>lsqlite</w>
|
||||
<w>lssl</w>
|
||||
<w>lstart</w>
|
||||
<w>lstr</w>
|
||||
<w>lstr's</w>
|
||||
<w>lstr</w>
|
||||
<w>lstrs</w>
|
||||
<w>lsval</w>
|
||||
<w>ltex</w>
|
||||
@ -1362,6 +1362,9 @@
|
||||
<w>nosub</w>
|
||||
<w>nosyncdir</w>
|
||||
<w>nosyncdirs</w>
|
||||
<w>nosyncfile</w>
|
||||
<w>nosyncfiles</w>
|
||||
<w>nosynctool</w>
|
||||
<w>nosynctools</w>
|
||||
<w>notdir</w>
|
||||
<w>npos</w>
|
||||
@ -1800,8 +1803,8 @@
|
||||
<w>sessionname</w>
|
||||
<w>sessionplayer</w>
|
||||
<w>sessionplayers</w>
|
||||
<w>sessionteam</w>
|
||||
<w>sessionteam's</w>
|
||||
<w>sessionteam</w>
|
||||
<w>sessionteams</w>
|
||||
<w>sessiontype</w>
|
||||
<w>setactivity</w>
|
||||
@ -2131,8 +2134,8 @@
|
||||
<w>txtw</w>
|
||||
<w>typeargs</w>
|
||||
<w>typecheck</w>
|
||||
<w>typechecker</w>
|
||||
<w>typechecker's</w>
|
||||
<w>typechecker</w>
|
||||
<w>typedval</w>
|
||||
<w>typeshed</w>
|
||||
<w>typestr</w>
|
||||
|
||||
@ -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": [
|
||||
|
||||
343
src/ballistica/ballistica.cc
Normal file
343
src/ballistica/ballistica.cc
Normal file
@ -0,0 +1,343 @@
|
||||
// Copyright (c) 2011-2020 Eric Froemling
|
||||
|
||||
#include "ballistica/ballistica.h"
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "ballistica/app/app.h"
|
||||
#include "ballistica/app/app_config.h"
|
||||
#include "ballistica/app/app_globals.h"
|
||||
#include "ballistica/audio/audio.h"
|
||||
#include "ballistica/audio/audio_server.h"
|
||||
#include "ballistica/core/fatal_error.h"
|
||||
#include "ballistica/core/logging.h"
|
||||
#include "ballistica/core/thread.h"
|
||||
#include "ballistica/dynamics/bg/bg_dynamics.h"
|
||||
#include "ballistica/dynamics/bg/bg_dynamics_server.h"
|
||||
#include "ballistica/game/account.h"
|
||||
#include "ballistica/graphics/graphics.h"
|
||||
#include "ballistica/graphics/graphics_server.h"
|
||||
#include "ballistica/input/input.h"
|
||||
#include "ballistica/media/media.h"
|
||||
#include "ballistica/media/media_server.h"
|
||||
#include "ballistica/networking/network_write_module.h"
|
||||
#include "ballistica/networking/networking.h"
|
||||
#include "ballistica/platform/platform.h"
|
||||
#include "ballistica/python/python.h"
|
||||
#include "ballistica/scene/scene.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
// These are set automatically via script; don't change here.
|
||||
const int kAppBuildNumber = 20192;
|
||||
const char* kAppVersion = "1.5.26";
|
||||
const char* kBlessingHash = nullptr;
|
||||
|
||||
// Our standalone globals.
|
||||
// These are separated out for easy access.
|
||||
// Everything else should go into AppGlobals (or more ideally into a class).
|
||||
int g_early_log_writes{10};
|
||||
Thread* g_main_thread{};
|
||||
AppGlobals* g_app_globals{};
|
||||
AppConfig* g_app_config{};
|
||||
App* g_app{};
|
||||
Account* g_account{};
|
||||
Game* g_game{};
|
||||
BGDynamics* g_bg_dynamics{};
|
||||
BGDynamicsServer* g_bg_dynamics_server{};
|
||||
Platform* g_platform{};
|
||||
Utils* g_utils{};
|
||||
UI* g_ui{};
|
||||
Graphics* g_graphics{};
|
||||
Python* g_python{};
|
||||
Input* g_input{};
|
||||
GraphicsServer* g_graphics_server{};
|
||||
Media* g_media{};
|
||||
Audio* g_audio{};
|
||||
MediaServer* g_media_server{};
|
||||
AudioServer* g_audio_server{};
|
||||
StdInputModule* g_std_input_module{};
|
||||
NetworkReader* g_network_reader{};
|
||||
Networking* g_networking{};
|
||||
NetworkWriteModule* g_network_write_module{};
|
||||
TextGraphics* g_text_graphics{};
|
||||
|
||||
// Basic overview of our bootstrapping process:
|
||||
// 1: All threads and globals are created and provisioned. Everything above
|
||||
// should exist at the end of this step (if it is going to exist).
|
||||
// Threads should not be talking to each other yet at this point.
|
||||
// 2: The system is set in motion. Game thread is told to load/apply the config.
|
||||
// This kicks off an initial-screen-creation message sent to the
|
||||
// graphics-server thread. Other systems are informed that bootstrapping
|
||||
// is complete and they are free to talk to each other. Initial input-devices
|
||||
// are added, media loads can begin (at least ones not dependent on the
|
||||
// screen/renderer), etc.
|
||||
// 3: The initial screen is created on the graphics-server thread in response
|
||||
// to the message sent from the game thread. A completion notice is sent
|
||||
// back to the game thread when done.
|
||||
// 4: Back on the game thread, any renderer-dependent media-loads/etc. can begin
|
||||
// and lastly the initial game session is kicked off.
|
||||
|
||||
auto BallisticaMain(int argc, char** argv) -> int {
|
||||
try {
|
||||
// Even at the absolute start of execution we should be able to
|
||||
// phone home on errors. Set BA_CRASH_TEST=1 to test this.
|
||||
if (const char* crashenv = getenv("BA_CRASH_TEST")) {
|
||||
if (!strcmp(crashenv, "1")) {
|
||||
FatalError("Fatal-Error-Test");
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Phase 1: Create and provision all globals.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
g_app_globals = new AppGlobals(argc, argv);
|
||||
g_platform = Platform::Create();
|
||||
g_platform->PostInit();
|
||||
g_account = new Account();
|
||||
g_utils = new Utils();
|
||||
Scene::Init();
|
||||
|
||||
// Create a Thread wrapper around the current (main) thread.
|
||||
g_main_thread = new Thread(ThreadIdentifier::kMain, ThreadType::kMain);
|
||||
|
||||
// Spin up g_app.
|
||||
g_platform->CreateApp();
|
||||
|
||||
// Spin up our other standard threads.
|
||||
auto* media_thread = new Thread(ThreadIdentifier::kMedia);
|
||||
g_app_globals->pausable_threads.push_back(media_thread);
|
||||
auto* audio_thread = new Thread(ThreadIdentifier::kAudio);
|
||||
g_app_globals->pausable_threads.push_back(audio_thread);
|
||||
auto* game_thread = new Thread(ThreadIdentifier::kGame);
|
||||
g_app_globals->pausable_threads.push_back(game_thread);
|
||||
auto* network_write_thread = new Thread(ThreadIdentifier::kNetworkWrite);
|
||||
g_app_globals->pausable_threads.push_back(network_write_thread);
|
||||
|
||||
// And add our other standard modules to them.
|
||||
game_thread->AddModule<Game>();
|
||||
network_write_thread->AddModule<NetworkWriteModule>();
|
||||
media_thread->AddModule<MediaServer>();
|
||||
g_main_thread->AddModule<GraphicsServer>();
|
||||
audio_thread->AddModule<AudioServer>();
|
||||
|
||||
// Now let the platform spin up any other threads/modules it uses.
|
||||
// (bg-dynamics in non-headless builds, stdin/stdout where applicable, etc.)
|
||||
g_platform->CreateAuxiliaryModules();
|
||||
|
||||
// Ok at this point we can be considered up-and-running.
|
||||
g_app_globals->is_bootstrapped = true;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Phase 2: Set things in motion.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Ok; now that we're bootstrapped, tell the game thread to read and apply
|
||||
// the config which should kick off the real action.
|
||||
g_game->PushApplyConfigCall();
|
||||
|
||||
// Let the app and platform do whatever else it wants here such as adding
|
||||
// initial input devices/etc.
|
||||
g_app->OnBootstrapComplete();
|
||||
g_platform->OnBootstrapComplete();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Phase 3/4: Create a screen and/or kick off game (in other threads).
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
if (g_app->UsesEventLoop()) {
|
||||
// On our event-loop using platforms we now simply sit in our event loop
|
||||
// until the app is quit.
|
||||
g_main_thread->RunEventLoop(false);
|
||||
} else {
|
||||
// In this case we'll now simply return and let the OS feed us events
|
||||
// until the app quits.
|
||||
// However we may need to 'prime the pump' first. For instance,
|
||||
// if the main thread event loop is driven by frame draws, it may need to
|
||||
// manually pump events until drawing begins (otherwise it will never
|
||||
// process the 'create-screen' event and wind up deadlocked).
|
||||
g_app->PrimeEventPump();
|
||||
}
|
||||
} catch (const std::exception& exc) {
|
||||
std::string error_msg =
|
||||
std::string("Unhandled exception in BallisticaMain(): ") + exc.what();
|
||||
|
||||
FatalError::ReportFatalError(error_msg, true);
|
||||
bool exit_cleanly = !IsUnmodifiedBlessedBuild();
|
||||
bool handled = FatalError::HandleFatalError(exit_cleanly, true);
|
||||
|
||||
// Do the default thing if it's not been handled.
|
||||
if (!handled) {
|
||||
if (exit_cleanly) {
|
||||
exit(1);
|
||||
} else {
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
// printf("BLESSED? %d\n", static_cast<int>(IsUnmodifiedBlessedBuild()));
|
||||
|
||||
g_platform->WillExitMain(false);
|
||||
return g_app_globals->return_value;
|
||||
}
|
||||
|
||||
auto GetRealTime() -> millisecs_t {
|
||||
millisecs_t t = g_platform->GetTicks();
|
||||
|
||||
// If we're at a different time than our last query, do our funky math.
|
||||
if (t != g_app_globals->last_real_time_ticks) {
|
||||
std::lock_guard<std::mutex> lock(g_app_globals->real_time_mutex);
|
||||
millisecs_t passed = t - g_app_globals->last_real_time_ticks;
|
||||
|
||||
// GetTicks() is supposed to be monotonic but I've seen 'passed'
|
||||
// equal -1 even when it is using std::chrono::steady_clock. Let's do
|
||||
// our own filtering here to make 100% sure we don't go backwards.
|
||||
if (passed < 0) {
|
||||
passed = 0;
|
||||
} else {
|
||||
// Super big times-passed probably means we went to sleep or something;
|
||||
// clamp to a reasonable value.
|
||||
if (passed > 250) {
|
||||
passed = 250;
|
||||
}
|
||||
}
|
||||
g_app_globals->real_time += passed;
|
||||
g_app_globals->last_real_time_ticks = t;
|
||||
}
|
||||
return g_app_globals->real_time;
|
||||
}
|
||||
|
||||
auto FatalError(const std::string& message) -> void {
|
||||
FatalError::ReportFatalError(message, false);
|
||||
bool exit_cleanly = !IsUnmodifiedBlessedBuild();
|
||||
bool handled = FatalError::HandleFatalError(exit_cleanly, false);
|
||||
assert(handled);
|
||||
}
|
||||
|
||||
auto GetUniqueSessionIdentifier() -> const std::string& {
|
||||
static std::string session_id;
|
||||
static bool have_session_id = false;
|
||||
if (!have_session_id) {
|
||||
srand(static_cast<unsigned int>(
|
||||
Platform::GetCurrentMilliseconds())); // NOLINT
|
||||
uint32_t tval = static_cast<uint32_t>(rand()); // NOLINT
|
||||
assert(g_platform);
|
||||
session_id = g_platform->GetUniqueDeviceIdentifier() + std::to_string(tval);
|
||||
have_session_id = true;
|
||||
if (session_id.size() >= 100) {
|
||||
Log("WARNING: session id longer than it should be.");
|
||||
}
|
||||
}
|
||||
return session_id;
|
||||
}
|
||||
|
||||
auto InGameThread() -> bool {
|
||||
return (g_game && g_game->thread()->IsCurrent());
|
||||
}
|
||||
|
||||
auto InMainThread() -> bool {
|
||||
return (g_app_globals
|
||||
&& std::this_thread::get_id() == g_app_globals->main_thread_id);
|
||||
}
|
||||
|
||||
auto InGraphicsThread() -> bool {
|
||||
return (g_graphics_server && g_graphics_server->thread()->IsCurrent());
|
||||
}
|
||||
|
||||
auto InAudioThread() -> bool {
|
||||
return (g_audio_server && g_audio_server->thread()->IsCurrent());
|
||||
}
|
||||
|
||||
auto InBGDynamicsThread() -> bool {
|
||||
#if !BA_HEADLESS_BUILD
|
||||
return (g_bg_dynamics_server && g_bg_dynamics_server->thread()->IsCurrent());
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
auto InMediaThread() -> bool {
|
||||
return (g_media_server && g_media_server->thread()->IsCurrent());
|
||||
}
|
||||
|
||||
auto InNetworkWriteThread() -> bool {
|
||||
return (g_network_write_module
|
||||
&& g_network_write_module->thread()->IsCurrent());
|
||||
}
|
||||
|
||||
auto GetInterfaceType() -> UIScale { return g_app_globals->ui_scale; }
|
||||
|
||||
void Log(const std::string& msg, bool to_stdout, bool to_server) {
|
||||
Logging::Log(msg, to_stdout, to_server);
|
||||
}
|
||||
|
||||
auto IsVRMode() -> bool { return g_app_globals->vr_mode; }
|
||||
|
||||
auto IsStdinATerminal() -> bool { return g_app_globals->is_stdin_a_terminal; }
|
||||
|
||||
void ScreenMessage(const std::string& s, const Vector3f& color) {
|
||||
if (g_game) {
|
||||
g_game->PushScreenMessage(s, color);
|
||||
} else {
|
||||
Log("ScreenMessage before g_game init (will be lost): '" + s + "'");
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenMessage(const std::string& msg) {
|
||||
ScreenMessage(msg, {1.0f, 1.0f, 1.0f});
|
||||
}
|
||||
|
||||
auto GetCurrentThreadName() -> std::string {
|
||||
return Thread::GetCurrentThreadName();
|
||||
}
|
||||
|
||||
auto IsBootstrapped() -> bool { return g_app_globals->is_bootstrapped; }
|
||||
|
||||
// Used by our built in exception type.
|
||||
void SetPythonException(PyExcType python_type, const char* description) {
|
||||
Python::SetPythonException(python_type, description);
|
||||
}
|
||||
|
||||
auto IsUnmodifiedBlessedBuild() -> bool {
|
||||
// Assume debug builds are not blessed (we'll determine this after
|
||||
// we finish calcing blessing hash, but this we don't get false positives
|
||||
// up until that point)
|
||||
if (g_buildconfig.debug_build()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return false if we're unblessed or it seems that the user is likely
|
||||
// mucking around with stuff. If we just don't know yet
|
||||
// (for instance if blessing has calc hasn't completed) we assume we're
|
||||
// clean.
|
||||
if (g_app_globals && g_app_globals->user_ran_commands) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If they're using custom app scripts, just consider it modified.
|
||||
// Otherwise can can tend to get errors in early bootstrapping before
|
||||
// we've been able to calc hashes to see if things are modified.
|
||||
if (g_platform && g_platform->using_custom_app_python_dir()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we don't have an embedded blessing hash, we're not blessed. Duh.
|
||||
if (kBlessingHash == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we have an embedded hash and we've calced ours
|
||||
// and it doesn't match, consider ourself modified.
|
||||
return !(g_app_globals && !g_app_globals->calced_blessing_hash.empty()
|
||||
&& g_app_globals->calced_blessing_hash != kBlessingHash);
|
||||
}
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
// If desired, define main() in the global namespace.
|
||||
#if BA_DEFINE_MAIN
|
||||
auto main(int argc, char** argv) -> int {
|
||||
return ballistica::BallisticaMain(argc, argv);
|
||||
}
|
||||
#endif
|
||||
226
src/ballistica/ballistica.h
Normal file
226
src/ballistica/ballistica.h
Normal file
@ -0,0 +1,226 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_BALLISTICA_H_
|
||||
#define BALLISTICA_BALLISTICA_H_
|
||||
|
||||
// Try to ensure they're providing proper config stuff.
|
||||
#ifndef BA_HAVE_CONFIG
|
||||
#error platform config has not been defined!
|
||||
#endif
|
||||
|
||||
// FIXME: We need to update to C++17 to get unified std::abs().
|
||||
// Until we do that, int types are defined in <cstdlib>
|
||||
// and float/double in <cmath>, meaning its possible to call the wrong
|
||||
// version if we aren't careful and only include one header.
|
||||
// For now just including both here at the top level to hopefully
|
||||
// minimize problems.
|
||||
#ifdef __cplusplus
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#endif
|
||||
|
||||
#include "ballistica/core/exception.h"
|
||||
#include "ballistica/core/inline.h"
|
||||
#include "ballistica/core/macros.h"
|
||||
#include "ballistica/core/types.h"
|
||||
|
||||
// BA 2.0 UI testing.
|
||||
#define BA_TOOLBAR_TEST 0
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
extern const int kAppBuildNumber;
|
||||
extern const char* kAppVersion;
|
||||
extern const char* kBlessingHash;
|
||||
|
||||
// Protocol version we host games with and write replays to.
|
||||
// This should be incremented whenever there are changes made to the
|
||||
// session-commands layer (new/removed/changed nodes, attrs, data files,
|
||||
// behavior, etc.)
|
||||
// Note that the packet/gamepacket/message layer can vary more organically based
|
||||
// on build-numbers of connected clients/servers since none of that data is
|
||||
// stored; this just needs to be observed for all the scene stuff that
|
||||
// goes into replays since a single stream can get played/replayed on different
|
||||
// builds (as long as they support that protocol version).
|
||||
const int kProtocolVersion = 33;
|
||||
|
||||
// Oldest protocol version we can act as a client to.
|
||||
// This can generally be left as-is as long as only
|
||||
// new nodes/attrs/commands are added and existing
|
||||
// stuff is unchanged.
|
||||
const int kProtocolVersionMin = 24;
|
||||
|
||||
// FIXME: We should separate out connection protocol from scene protocol. We
|
||||
// want to be able to watch really old replays if possible but being able to
|
||||
// connect to old clients is much less important (and slows progress).
|
||||
|
||||
// Protocol additions:
|
||||
// 25: added a few new achievement graphics and new node attrs for displaying
|
||||
// stuff in front of the UI
|
||||
// 26: added penguin
|
||||
// 27: added templates for LOTS of characters
|
||||
// 28: added cyborg and enabled fallback sounds and textures
|
||||
// 29: added bunny and eggs
|
||||
// 30: added support for resource-strings in text-nodes and screen-messages
|
||||
// 31: added support for short-form resource-strings, time-display-node, and
|
||||
// string-to-string attr connections
|
||||
// 32: added json based player profiles message, added shield
|
||||
// alwaysShowHealthBar attr
|
||||
// 33: handshake/handshake-response now send json dicts instead of
|
||||
// just player-specs
|
||||
// 34: new image_node enums, data assets.
|
||||
|
||||
const int kDefaultPort = 43210;
|
||||
const int kDefaultTelnetPort = 43250;
|
||||
|
||||
const float kTVBorder = 0.075f;
|
||||
const float kVRBorder = 0.085f;
|
||||
|
||||
// Largest UDP packets we attempt to send.
|
||||
// (is there a definitive answer on what this should be?)
|
||||
const int kMaxPacketSize = 700;
|
||||
|
||||
// Extra bytes added to message packets.
|
||||
const int kMessagePacketHeaderSize = 6;
|
||||
|
||||
// The screen, no matter what size/aspect, will always
|
||||
// fit this virtual rectangle, so placing UI elements within
|
||||
// these coords is always safe.
|
||||
// (we currently match the screen ratio of an iPhone 5).
|
||||
const int kBaseVirtualResX = 1207;
|
||||
const int kBaseVirtualResY = 680;
|
||||
|
||||
// Magic numbers at the start of our file types.
|
||||
const int kBobFileID = 45623;
|
||||
const int kCobFileID = 13466;
|
||||
const int kBrpFileID = 83749;
|
||||
|
||||
const float kPi = 3.1415926535897932384626433832795028841971693993751f;
|
||||
const float kPiDeg = kPi / 180.0f;
|
||||
const float kDegPi = 180.0f / kPi;
|
||||
|
||||
// Sim step size in milliseconds.
|
||||
const int kGameStepMilliseconds = 8;
|
||||
|
||||
// Sim step size in seconds.
|
||||
const float kGameStepSeconds =
|
||||
(static_cast<float>(kGameStepMilliseconds) / 1000.0f);
|
||||
|
||||
// Globals.
|
||||
extern int g_early_log_writes;
|
||||
extern Account* g_account;
|
||||
extern App* g_app;
|
||||
extern AppConfig* g_app_config;
|
||||
extern AppGlobals* g_app_globals;
|
||||
extern Audio* g_audio;
|
||||
extern AudioServer* g_audio_server;
|
||||
extern BGDynamics* g_bg_dynamics;
|
||||
extern BGDynamicsServer* g_bg_dynamics_server;
|
||||
extern Context* g_context;
|
||||
extern Game* g_game;
|
||||
extern Graphics* g_graphics;
|
||||
extern GraphicsServer* g_graphics_server;
|
||||
extern Input* g_input;
|
||||
extern Thread* g_main_thread;
|
||||
extern Media* g_media;
|
||||
extern MediaServer* g_media_server;
|
||||
extern Networking* g_networking;
|
||||
extern NetworkReader* g_network_reader;
|
||||
extern NetworkWriteModule* g_network_write_module;
|
||||
extern Platform* g_platform;
|
||||
extern Python* g_python;
|
||||
extern StdInputModule* g_std_input_module;
|
||||
extern TextGraphics* g_text_graphics;
|
||||
extern UI* g_ui;
|
||||
extern Utils* g_utils;
|
||||
|
||||
/// Main ballistica entry point.
|
||||
auto BallisticaMain(int argc, char** argv) -> int;
|
||||
|
||||
/// Return a string that should be universally unique to this device and
|
||||
/// running instance of the app.
|
||||
auto GetUniqueSessionIdentifier() -> const std::string&;
|
||||
|
||||
/// Have our main threads/modules all been inited yet?
|
||||
auto IsBootstrapped() -> bool;
|
||||
|
||||
/// Does it appear that we are a blessed build with no known user-modifications?
|
||||
auto IsUnmodifiedBlessedBuild() -> bool;
|
||||
|
||||
// The following is a smattering of convenience functions declared in our top
|
||||
// level namespace. Functionality can be exposed here if it is used often
|
||||
// enough that avoiding the extra class includes seems like an overall
|
||||
// compile-time/convenience win.
|
||||
|
||||
// Print a momentary message on the screen.
|
||||
auto ScreenMessage(const std::string& msg) -> void;
|
||||
auto ScreenMessage(const std::string& msg, const Vector3f& color) -> void;
|
||||
|
||||
/// Log a fatal error and kill the app.
|
||||
/// Can be called from any thread at any time.
|
||||
/// message is a message to be shown to the user if possible.
|
||||
/// This will attempt to ship all accumulated logs to the master-server
|
||||
/// so the standard Log() call can be used before this to include extra
|
||||
/// info not relevant to the end user.
|
||||
auto FatalError(const std::string& message = "") -> void;
|
||||
|
||||
// Check current-threads.
|
||||
auto InMainThread() -> bool; // (main and graphics are same currently)
|
||||
auto InGraphicsThread() -> bool; // (main and graphics are same currently)
|
||||
auto InGameThread() -> bool;
|
||||
auto InAudioThread() -> bool;
|
||||
auto InBGDynamicsThread() -> bool;
|
||||
auto InMediaThread() -> bool;
|
||||
auto InNetworkWriteThread() -> bool;
|
||||
|
||||
/// Return a human-readable name for the current thread.
|
||||
auto GetCurrentThreadName() -> std::string;
|
||||
|
||||
/// Write a string to the log.
|
||||
/// This will go to stdout, windows debug log, android log, etc.
|
||||
/// A trailing newline will be added.
|
||||
auto Log(const std::string& msg, bool to_stdout = true, bool to_server = true)
|
||||
-> void;
|
||||
|
||||
auto GetInterfaceType() -> UIScale;
|
||||
|
||||
/// Return true if stdin seems to be coming from a terminal
|
||||
/// (so we know to print prompts, etc).
|
||||
auto IsStdinATerminal() -> bool;
|
||||
|
||||
/// Are we running in a VR environment?
|
||||
auto IsVRMode() -> bool;
|
||||
|
||||
/// Are we running headless?
|
||||
inline auto HeadlessMode() -> bool {
|
||||
// (currently a build-time value but this could change later)
|
||||
return g_buildconfig.headless_build();
|
||||
}
|
||||
|
||||
/// Return a lightly-filtered 'real' time value in milliseconds.
|
||||
/// The value returned here will never go backwards or skip ahead
|
||||
/// by significant amounts (even if the app has been sleeping or whatnot).
|
||||
auto GetRealTime() -> millisecs_t;
|
||||
|
||||
/// Return a random float value. Not guaranteed to be deterministic or
|
||||
/// consistent across platforms.
|
||||
inline auto RandomFloat() -> float {
|
||||
// FIXME: should convert this to something thread-safe.
|
||||
return static_cast<float>(
|
||||
(static_cast<double>(rand()) / RAND_MAX)); // NOLINT
|
||||
}
|
||||
|
||||
auto SetPythonException(PyExcType python_type, const char* description) -> void;
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // __cplusplus
|
||||
|
||||
#endif // BALLISTICA_BALLISTICA_H_
|
||||
142
src/ballistica/core/context.cc
Normal file
142
src/ballistica/core/context.cc
Normal file
@ -0,0 +1,142 @@
|
||||
// Copyright (c) 2011-2020 Eric Froemling
|
||||
|
||||
#include "ballistica/core/context.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "ballistica/game/host_activity.h"
|
||||
#include "ballistica/generic/runnable.h"
|
||||
#include "ballistica/ui/ui.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
// Dynamically allocate this; don't want it torn down on quit.
|
||||
Context* g_context = nullptr;
|
||||
|
||||
void Context::Init() {
|
||||
assert(!g_context);
|
||||
g_context = new Context(nullptr);
|
||||
}
|
||||
|
||||
ContextTarget::ContextTarget() = default;
|
||||
ContextTarget::~ContextTarget() = default;
|
||||
|
||||
auto ContextTarget::GetHostSession() -> HostSession* { return nullptr; }
|
||||
|
||||
auto ContextTarget::GetAsHostActivity() -> HostActivity* { return nullptr; }
|
||||
auto ContextTarget::GetAsUIContext() -> UI* { return nullptr; }
|
||||
auto ContextTarget::GetMutableScene() -> Scene* { return nullptr; }
|
||||
|
||||
Context::Context() : target(g_context->target) { assert(InGameThread()); }
|
||||
|
||||
auto Context::operator==(const Context& other) const -> bool {
|
||||
return (target.get() == other.target.get());
|
||||
}
|
||||
|
||||
Context::Context(ContextTarget* target_in) : target(target_in) {}
|
||||
|
||||
auto Context::GetHostSession() const -> HostSession* {
|
||||
assert(InGameThread());
|
||||
if (target.exists()) return target->GetHostSession();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto Context::GetHostActivity() const -> HostActivity* {
|
||||
ContextTarget* c = target.get();
|
||||
HostActivity* a = c ? c->GetAsHostActivity() : nullptr;
|
||||
assert(a == dynamic_cast<HostActivity*>(c)); // This should always match.
|
||||
return a;
|
||||
}
|
||||
|
||||
auto Context::GetMutableScene() const -> Scene* {
|
||||
ContextTarget* c = target.get();
|
||||
Scene* sg = c ? c->GetMutableScene() : nullptr;
|
||||
return sg;
|
||||
}
|
||||
|
||||
auto Context::GetUIContext() const -> UI* {
|
||||
ContextTarget* c = target.get();
|
||||
UI* uiContext = c ? c->GetAsUIContext() : nullptr;
|
||||
assert(uiContext == dynamic_cast<UI*>(c));
|
||||
return uiContext;
|
||||
}
|
||||
|
||||
ScopedSetContext::ScopedSetContext(const Object::Ref<ContextTarget>& target) {
|
||||
assert(InGameThread());
|
||||
assert(g_context);
|
||||
context_prev_ = *g_context;
|
||||
g_context->target = target;
|
||||
}
|
||||
|
||||
ScopedSetContext::ScopedSetContext(ContextTarget* target) {
|
||||
assert(InGameThread());
|
||||
assert(g_context);
|
||||
context_prev_ = *g_context;
|
||||
g_context->target = target;
|
||||
}
|
||||
|
||||
ScopedSetContext::ScopedSetContext(const Context& context) {
|
||||
assert(InGameThread());
|
||||
assert(g_context);
|
||||
context_prev_ = *g_context;
|
||||
*g_context = context;
|
||||
}
|
||||
|
||||
ScopedSetContext::~ScopedSetContext() {
|
||||
assert(InGameThread());
|
||||
assert(g_context);
|
||||
// Restore old.
|
||||
*g_context = context_prev_;
|
||||
}
|
||||
|
||||
auto ContextTarget::NewTimer(TimeType timetype, TimerMedium length, bool repeat,
|
||||
const Object::Ref<Runnable>& runnable) -> int {
|
||||
// Make sure the passed runnable has a ref-count already
|
||||
// (don't want them to rely on us to create initial one).
|
||||
assert(runnable.exists());
|
||||
assert(runnable->is_valid_refcounted_object());
|
||||
|
||||
switch (timetype) {
|
||||
case TimeType::kSim:
|
||||
throw Exception("Can't create 'sim' type timers in this context");
|
||||
case TimeType::kBase:
|
||||
throw Exception("Can't create 'base' type timers in this context");
|
||||
case TimeType::kReal:
|
||||
throw Exception("Can't create 'real' type timers in this context");
|
||||
default:
|
||||
throw Exception("Can't create that type timer in this context");
|
||||
}
|
||||
}
|
||||
void ContextTarget::DeleteTimer(TimeType timetype, int timer_id) {
|
||||
// We throw on NewTimer; lets just ignore anything that comes
|
||||
// through here to avoid messing up destructors.
|
||||
Log("ContextTarget::DeleteTimer() called; unexpected.");
|
||||
}
|
||||
|
||||
auto ContextTarget::GetTime(TimeType timetype) -> millisecs_t {
|
||||
throw Exception("Unsupported time type for this context");
|
||||
}
|
||||
|
||||
auto ContextTarget::GetTexture(const std::string& name)
|
||||
-> Object::Ref<Texture> {
|
||||
throw Exception("GetTexture() not supported in this context");
|
||||
}
|
||||
|
||||
auto ContextTarget::GetSound(const std::string& name) -> Object::Ref<Sound> {
|
||||
throw Exception("GetSound() not supported in this context");
|
||||
}
|
||||
|
||||
auto ContextTarget::GetData(const std::string& name) -> Object::Ref<Data> {
|
||||
throw Exception("GetData() not supported in this context");
|
||||
}
|
||||
|
||||
auto ContextTarget::GetModel(const std::string& name) -> Object::Ref<Model> {
|
||||
throw Exception("GetModel() not supported in this context");
|
||||
}
|
||||
|
||||
auto ContextTarget::GetCollideModel(const std::string& name)
|
||||
-> Object::Ref<CollideModel> {
|
||||
throw Exception("GetCollideModel() not supported in this context");
|
||||
}
|
||||
|
||||
} // namespace ballistica
|
||||
128
src/ballistica/core/context.h
Normal file
128
src/ballistica/core/context.h
Normal file
@ -0,0 +1,128 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_CORE_CONTEXT_H_
|
||||
#define BALLISTICA_CORE_CONTEXT_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "ballistica/core/object.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
// Stores important environmental state such as the recipient of commands.
|
||||
// Callbacks and other mechanisms should save/restore the context so that their
|
||||
// effects properly apply to the place they came from.
|
||||
class Context {
|
||||
public:
|
||||
static void Init();
|
||||
|
||||
static auto current() -> const Context& {
|
||||
assert(g_context);
|
||||
|
||||
// Context can only be accessed from the game thread.
|
||||
BA_PRECONDITION(InGameThread());
|
||||
|
||||
return *g_context;
|
||||
}
|
||||
static void set_current(const Context& context) {
|
||||
// Context can only be accessed from the game thread.
|
||||
BA_PRECONDITION(InGameThread());
|
||||
|
||||
*g_context = context;
|
||||
}
|
||||
|
||||
// Return the current context target, raising an Exception if there is none.
|
||||
static auto current_target() -> ContextTarget& {
|
||||
ContextTarget* t = current().target.get();
|
||||
if (t == nullptr) {
|
||||
throw Exception("No context target set.");
|
||||
}
|
||||
return *t;
|
||||
}
|
||||
|
||||
// Default constructor will capture a copy of the current global context.
|
||||
Context();
|
||||
explicit Context(ContextTarget* sgc);
|
||||
auto operator==(const Context& other) const -> bool;
|
||||
|
||||
Object::WeakRef<ContextTarget> target;
|
||||
|
||||
// If the current Context is (or is part of) a HostSession, return it;
|
||||
// otherwise return nullptr. be aware that this will return a session if the
|
||||
// context is *either* a host-activity or a host-session
|
||||
auto GetHostSession() const -> HostSession*;
|
||||
|
||||
// return the current context as an HostActivity if it is one; otherwise
|
||||
// nullptr (faster than a dynamic_cast)
|
||||
auto GetHostActivity() const -> HostActivity*;
|
||||
|
||||
// if the current context contains a scene that can be manipulated by
|
||||
// standard commands, this returns it. This includes host-sessions,
|
||||
// host-activities, and the UI context.
|
||||
auto GetMutableScene() const -> Scene*;
|
||||
|
||||
// return the current context as a UIContext if it is one; otherwise nullptr
|
||||
// (faster than a dynamic_cst)
|
||||
auto GetUIContext() const -> UI*;
|
||||
};
|
||||
|
||||
// An interface for interaction with the engine; loading and wrangling media,
|
||||
// nodes, etc.
|
||||
// Note: it would seem like in an ideal world this could just be a pure
|
||||
// virtual interface.
|
||||
// However various things use WeakRef<ContextTarget> so technically they do
|
||||
// all need to inherit from Object anyway.
|
||||
class ContextTarget : public Object {
|
||||
public:
|
||||
ContextTarget();
|
||||
~ContextTarget() override;
|
||||
|
||||
// returns the HostSession associated with this context, (if there is one).
|
||||
virtual auto GetHostSession() -> HostSession*;
|
||||
|
||||
// Utility functions for casting; faster than dynamic_cast.
|
||||
virtual auto GetAsHostActivity() -> HostActivity*;
|
||||
virtual auto GetAsUIContext() -> UI*;
|
||||
virtual auto GetMutableScene() -> Scene*;
|
||||
|
||||
// Timer create/destroy functions.
|
||||
// Times are specified in milliseconds.
|
||||
// Exceptions should be thrown for unsupported timetypes in NewTimer.
|
||||
// Default NewTimer implementation throws a descriptive error, so it can
|
||||
// be useful to fall back on for unsupported cases.
|
||||
// NOTE: make sure runnables passed in here already have non-zero
|
||||
// ref-counts since a ref might not be grabbed here.
|
||||
virtual auto NewTimer(TimeType timetype, TimerMedium length, bool repeat,
|
||||
const Object::Ref<Runnable>& runnable) -> int;
|
||||
virtual void DeleteTimer(TimeType timetype, int timer_id);
|
||||
|
||||
virtual auto GetTexture(const std::string& name) -> Object::Ref<Texture>;
|
||||
virtual auto GetSound(const std::string& name) -> Object::Ref<Sound>;
|
||||
virtual auto GetData(const std::string& name) -> Object::Ref<Data>;
|
||||
virtual auto GetModel(const std::string& name) -> Object::Ref<Model>;
|
||||
virtual auto GetCollideModel(const std::string& name)
|
||||
-> Object::Ref<CollideModel>;
|
||||
|
||||
// Return the current time of a given type in milliseconds.
|
||||
// Exceptions should be thrown for unsupported timetypes.
|
||||
// Default implementation throws a descriptive error so can be
|
||||
// useful to fall back on for unsupported cases
|
||||
virtual auto GetTime(TimeType timetype) -> millisecs_t;
|
||||
};
|
||||
|
||||
// Use this to push/pop a change to the current context
|
||||
class ScopedSetContext {
|
||||
public:
|
||||
explicit ScopedSetContext(const Object::Ref<ContextTarget>& context);
|
||||
explicit ScopedSetContext(ContextTarget* context);
|
||||
explicit ScopedSetContext(const Context& context);
|
||||
~ScopedSetContext();
|
||||
|
||||
private:
|
||||
BA_DISALLOW_CLASS_COPIES(ScopedSetContext);
|
||||
Context context_prev_;
|
||||
};
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // BALLISTICA_CORE_CONTEXT_H_
|
||||
83
src/ballistica/core/exception.cc
Normal file
83
src/ballistica/core/exception.cc
Normal file
@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2011-2020 Eric Froemling
|
||||
|
||||
#include "ballistica/core/exception.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "ballistica/ballistica.h"
|
||||
#include "ballistica/platform/platform.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
auto GetShortExceptionDescription(const std::exception& exc) -> const char* {
|
||||
if (auto b_exc = dynamic_cast<const Exception*>(&exc)) {
|
||||
return b_exc->message();
|
||||
}
|
||||
return exc.what();
|
||||
}
|
||||
|
||||
Exception::Exception(std::string message_in, PyExcType python_type)
|
||||
: message_(std::move(message_in)), python_type_(python_type) {
|
||||
thread_name_ = GetCurrentThreadName();
|
||||
|
||||
// Attempt to capture a stack-trace here we can print out later if desired.
|
||||
if (g_platform != nullptr) {
|
||||
stack_trace_ = g_platform->GetStackTrace();
|
||||
}
|
||||
}
|
||||
Exception::Exception(PyExcType python_type) : python_type_(python_type) {
|
||||
thread_name_ = GetCurrentThreadName();
|
||||
|
||||
// Attempt to capture a stack-trace here we can print out later if desired.
|
||||
if (g_platform != nullptr) {
|
||||
stack_trace_ = g_platform->GetStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// Copy constructor.
|
||||
Exception::Exception(const Exception& other) noexcept {
|
||||
try {
|
||||
thread_name_ = other.thread_name_;
|
||||
message_ = other.message_;
|
||||
full_description_ = other.full_description_;
|
||||
python_type_ = other.python_type_;
|
||||
if (other.stack_trace_) {
|
||||
stack_trace_ = other.stack_trace_->copy();
|
||||
}
|
||||
} catch (const std::exception&) {
|
||||
// Hmmm not sure what we should do if this happens;
|
||||
// for now we'll just wind up with some parts of our
|
||||
// shiny new exception copy potentially missing.
|
||||
// Better than crashing I suppose.
|
||||
}
|
||||
}
|
||||
|
||||
Exception::~Exception() { delete stack_trace_; }
|
||||
|
||||
auto Exception::what() const noexcept -> const char* {
|
||||
// Return a nice pretty stack trace and other relevant info.
|
||||
try {
|
||||
// This call is const so we're technically not supposed to modify ourself,
|
||||
// but a one-time flattening of our description into an internal buffer
|
||||
// should be fine.
|
||||
if (full_description_.empty()) {
|
||||
if (stack_trace_ != nullptr) {
|
||||
const_cast<Exception*>(this)->full_description_ =
|
||||
message_ + "\nThrown from " + thread_name_ + " thread:\n"
|
||||
+ stack_trace_->GetDescription();
|
||||
} else {
|
||||
const_cast<Exception*>(this)->full_description_ = message_;
|
||||
}
|
||||
}
|
||||
return full_description_.c_str();
|
||||
} catch (const std::exception&) {
|
||||
// Welp; we tried.
|
||||
return "Error generating ballistica::Exception::what(); oh dear.";
|
||||
}
|
||||
}
|
||||
|
||||
void Exception::SetPyError() const noexcept {
|
||||
SetPythonException(python_type_, GetShortExceptionDescription(*this));
|
||||
}
|
||||
|
||||
} // namespace ballistica
|
||||
82
src/ballistica/core/exception.h
Normal file
82
src/ballistica/core/exception.h
Normal file
@ -0,0 +1,82 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_CORE_EXCEPTION_H_
|
||||
#define BALLISTICA_CORE_EXCEPTION_H_
|
||||
#ifdef __cplusplus
|
||||
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "ballistica/core/types.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
// Notes on our C++ exception handling:
|
||||
//
|
||||
// std::exception in broken into two subclass categories, logic_error
|
||||
// and runtime_error. It is my understanding that logic_error should be used
|
||||
// as a sort of non-fatal assert() for things that the program is doing
|
||||
// incorrectly, while runtime_error applies to external things such as user
|
||||
// input (a user entering a name containing invalid characters, etc).
|
||||
//
|
||||
// In practice, we currently handle both sides identically, so the distinction
|
||||
// is not really important to us. We also translate C++ exceptions to and
|
||||
// from Python exceptions as their respective stacks unwind, so the distinction
|
||||
// tends to get lost anyway.
|
||||
//
|
||||
// So for the time being we have a simple single ballistica::Exception type
|
||||
// inheriting directly from std::exception that we use for pretty much anything
|
||||
// going wrong. It contains useful tidbits such as a stack trace to help
|
||||
// diagnose issues. We can expand on this or branch off into more particular
|
||||
// types if/when the need arises.
|
||||
//
|
||||
// Note that any sites *catching* exception should catch std::exception
|
||||
// (unless they have a particular need to catch a more specific type). This
|
||||
// preserves our freedom to add variants under std::logic_error or
|
||||
// std::runtime_error at a later time and also catches exceptions coming from
|
||||
// std itself.
|
||||
|
||||
class PlatformStackTrace;
|
||||
|
||||
/// Get a short description for an exception.
|
||||
/// By default, our Exception classes provide what() values that may include
|
||||
/// backtraces of the throw location or other extended info that can be useful
|
||||
/// to have printed in crash reports/etc. In some cases this extended info is
|
||||
/// not desired, however, such as when converting a C++ exception to a Python
|
||||
/// one (which will have its own backtrace and other context). This function
|
||||
/// will return the raw message only if passed one of our Exceptions, and
|
||||
/// simply what() in other cases.
|
||||
auto GetShortExceptionDescription(const std::exception& exc) -> const char*;
|
||||
|
||||
class Exception : public std::exception {
|
||||
public:
|
||||
// NOTE: When adding exception types here, add a corresponding
|
||||
// handler in Python::SetPythonException.
|
||||
|
||||
explicit Exception(std::string message = "",
|
||||
PyExcType python_type = PyExcType::kRuntime);
|
||||
explicit Exception(PyExcType python_type);
|
||||
Exception(const Exception& other) noexcept;
|
||||
~Exception() override;
|
||||
|
||||
/// Return the full description for this exception which may include
|
||||
/// backtraces/etc.
|
||||
auto what() const noexcept -> const char* override;
|
||||
|
||||
/// Return only the raw message passed to this exception on creation.
|
||||
auto message() const noexcept -> const char* { return message_.c_str(); }
|
||||
|
||||
void SetPyError() const noexcept;
|
||||
|
||||
private:
|
||||
std::string thread_name_;
|
||||
std::string message_;
|
||||
std::string full_description_;
|
||||
PyExcType python_type_;
|
||||
PlatformStackTrace* stack_trace_{};
|
||||
};
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // __cplusplus
|
||||
#endif // BALLISTICA_CORE_EXCEPTION_H_
|
||||
169
src/ballistica/core/fatal_error.cc
Normal file
169
src/ballistica/core/fatal_error.cc
Normal file
@ -0,0 +1,169 @@
|
||||
// Copyright (c) 2011-2020 Eric Froemling
|
||||
|
||||
#include "ballistica/core/fatal_error.h"
|
||||
|
||||
#include "ballistica/app/app.h"
|
||||
#include "ballistica/core/logging.h"
|
||||
#include "ballistica/platform/platform.h"
|
||||
|
||||
namespace ballistica {
|
||||
auto FatalError::ReportFatalError(const std::string& message,
|
||||
bool in_top_level_exception_handler) -> void {
|
||||
// We want to report the first fatal error that happens; if further ones
|
||||
// happen they are probably red herrings.
|
||||
static bool ran = false;
|
||||
if (ran) {
|
||||
return;
|
||||
}
|
||||
ran = true;
|
||||
|
||||
// Our main goal here varies based off whether we are an unmodified
|
||||
// blessed build. If we are, our main goal is to communicate as much info
|
||||
// about the error to the master server, and communicating to the user is
|
||||
// a stretch goal.
|
||||
// If we are unblessed or modified, the main goals are communicating the error
|
||||
// to the user and exiting the app cleanly (so we don't pollute our crash
|
||||
// records with results of user tinkering).
|
||||
|
||||
// Try to avoid crash reports if we're not a clean blessed build.
|
||||
// bool exit_cleanly = !IsUnmodifiedBlessedBuild();
|
||||
// printf("BLESSED %d\n", static_cast<int>(IsUnmodifiedBlessedBuild()));
|
||||
|
||||
// Give the platform the opportunity to completely override our handling.
|
||||
if (g_platform) {
|
||||
auto handled =
|
||||
g_platform->ReportFatalError(message, in_top_level_exception_handler);
|
||||
if (handled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::string dialog_msg = message;
|
||||
if (!dialog_msg.empty()) {
|
||||
dialog_msg += "\n";
|
||||
}
|
||||
// (No longer adding this note; individual errors to which the log is
|
||||
// relevant can do to themselves).
|
||||
// dialog_msg += "See BallisticaCore log for details.";
|
||||
|
||||
auto starttime = time(nullptr);
|
||||
|
||||
// Launch a thread and give it a chance to directly send our logs to the
|
||||
// master-server. The standard mechanism probably won't get the job done
|
||||
// since it relies on the game thread loop and we're likely blocking that.
|
||||
// But generally we want to stay in this function and call abort() or whatnot
|
||||
// from here so that our stack trace makes it into platform logs.
|
||||
int result{};
|
||||
|
||||
std::string logmsg =
|
||||
std::string("FATAL ERROR:") + (!message.empty() ? " " : "") + message;
|
||||
|
||||
// Try to include a stack trace if we're being called from outside of a
|
||||
// top-level exception handler. Otherwise the trace isn't really useful
|
||||
// since we know where those are anyway.
|
||||
if (!in_top_level_exception_handler) {
|
||||
if (g_platform) {
|
||||
PlatformStackTrace* trace{g_platform->GetStackTrace()};
|
||||
if (trace) {
|
||||
std::string tracestr = trace->GetDescription();
|
||||
if (!tracestr.empty()) {
|
||||
logmsg += ("\nSTACK-TRACE-BEGIN:\n" + tracestr + "\nSTACK-TRACE-END");
|
||||
}
|
||||
delete trace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent the early-log insta-send mechanism from firing since we do
|
||||
// basically the same thing ourself here (avoid sending the same logs twice).
|
||||
g_early_log_writes = 0;
|
||||
|
||||
Logging::Log(logmsg);
|
||||
|
||||
std::string prefix = "FATAL-ERROR-LOG:";
|
||||
std::string suffix;
|
||||
|
||||
// If we have no globals yet, include this message explicitly
|
||||
// since it won't be part of the standard log.
|
||||
if (g_app_globals == nullptr) {
|
||||
suffix = logmsg;
|
||||
}
|
||||
Logging::DirectSendLogs(prefix, suffix, true, &result);
|
||||
|
||||
// If we're able to show a fatal-error dialog synchronously, do so.
|
||||
if (g_platform && g_platform->CanShowBlockingFatalErrorDialog()) {
|
||||
DoBlockingFatalErrorDialog(dialog_msg);
|
||||
}
|
||||
|
||||
// Wait until the log submit has finished or a bit of time has passed..
|
||||
while (time(nullptr) - starttime < 10) {
|
||||
if (result != 0) {
|
||||
break;
|
||||
}
|
||||
Platform::SleepMS(100);
|
||||
}
|
||||
}
|
||||
|
||||
auto FatalError::DoBlockingFatalErrorDialog(const std::string& message)
|
||||
-> void {
|
||||
// If we're in the main thread; just fire off the dialog directly.
|
||||
// Otherwise tell the main thread to do it and wait around until it's done.
|
||||
if (InMainThread()) {
|
||||
g_platform->BlockingFatalErrorDialog(message);
|
||||
} else {
|
||||
bool started{};
|
||||
bool finished{};
|
||||
bool* startedptr{&started};
|
||||
bool* finishedptr{&finished};
|
||||
g_app->PushCall([message, startedptr, finishedptr] {
|
||||
*startedptr = true;
|
||||
g_platform->BlockingFatalErrorDialog(message);
|
||||
*finishedptr = true;
|
||||
});
|
||||
|
||||
// Wait a short amount of time for the main thread to take action.
|
||||
// There's a chance that it can't (if threads are paused, if it is
|
||||
// blocked on a synchronous call to another thread, etc.) so if we don't
|
||||
// see something happening soon, just give up on showing a dialog.
|
||||
auto starttime = Platform::GetCurrentMilliseconds();
|
||||
while (!started) {
|
||||
if (Platform::GetCurrentMilliseconds() - starttime > 1000) {
|
||||
return;
|
||||
}
|
||||
Platform::SleepMS(10);
|
||||
}
|
||||
while (!finished) {
|
||||
Platform::SleepMS(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto FatalError::HandleFatalError(bool exit_cleanly,
|
||||
bool in_top_level_exception_handler) -> bool {
|
||||
// Give the platform the opportunity to completely override our handling.
|
||||
if (g_platform) {
|
||||
auto handled = g_platform->HandleFatalError(exit_cleanly,
|
||||
in_top_level_exception_handler);
|
||||
if (handled) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're not being called as part of a top-level exception handler,
|
||||
// bring the app down ourself.
|
||||
if (!in_top_level_exception_handler) {
|
||||
if (exit_cleanly) {
|
||||
Log("Calling exit(1)...");
|
||||
exit(1);
|
||||
} else {
|
||||
Log("Calling abort()...");
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise its up to who called us
|
||||
// (they might let the caught exception bubble up)
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace ballistica
|
||||
33
src/ballistica/core/fatal_error.h
Normal file
33
src/ballistica/core/fatal_error.h
Normal file
@ -0,0 +1,33 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_CORE_FATAL_ERROR_H_
|
||||
#define BALLISTICA_CORE_FATAL_ERROR_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
class FatalError {
|
||||
public:
|
||||
/// Report a fatal error to the master-server/user/etc. Note that reporting
|
||||
/// only happens for the first invocation of this call; additional calls
|
||||
/// are no-ops.
|
||||
static auto ReportFatalError(const std::string& message,
|
||||
bool in_top_level_exception_handler) -> void;
|
||||
|
||||
/// Handle a fatal error. This can involve calling exit(), abort(), setting
|
||||
/// up an asynchronous quit, etc. Returns true if the fatal-error has been
|
||||
/// handled; otherwise it is up to the caller (this should only be the case
|
||||
/// when in_top_level_exception_handler is true).
|
||||
/// Unlike ReportFatalError, the logic in this call can be invoked repeatedly
|
||||
/// and should be prepared for that possibility in the case of recursive
|
||||
/// fatal errors/etc.
|
||||
static auto HandleFatalError(bool clean_exit,
|
||||
bool in_top_level_exception_handler) -> bool;
|
||||
|
||||
private:
|
||||
static auto DoBlockingFatalErrorDialog(const std::string& message) -> void;
|
||||
};
|
||||
|
||||
} // namespace ballistica
|
||||
#endif // BALLISTICA_CORE_FATAL_ERROR_H_
|
||||
11
src/ballistica/core/inline.cc
Normal file
11
src/ballistica/core/inline.cc
Normal file
@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2011-2020 Eric Froemling
|
||||
|
||||
#if 0 // Satisfy both CppLint and CLang..
|
||||
#include "ballistica/core/inline.h"
|
||||
#endif
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
auto InlineDebugExplicitBool(bool val) -> bool { return val; }
|
||||
|
||||
} // namespace ballistica
|
||||
143
src/ballistica/core/inline.h
Normal file
143
src/ballistica/core/inline.h
Normal file
@ -0,0 +1,143 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_CORE_INLINE_H_
|
||||
#define BALLISTICA_CORE_INLINE_H_
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
// Bits of functionality that are useful enough to include fully as
|
||||
// inlines/templates in our top level namespace.
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
// Support functions we declare in our .cc file; not for public use.
|
||||
// auto InlineDebugExplicitBool(bool val) -> bool;
|
||||
|
||||
/// Return the same bool value passed in, but obfuscated enough in debug mode
|
||||
/// that no 'value is always true/false', 'code will never run', type warnings
|
||||
/// should appear. In release builds it should optimize away to a no-op.
|
||||
inline auto explicit_bool(bool val) -> bool {
|
||||
#if BA_DEBUG_BUILD
|
||||
return InlineDebugExplicitBool(val);
|
||||
#else
|
||||
return val;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Simply a static_cast, but in debug builds casts the results back to ensure
|
||||
/// the value fits into the receiver unchanged. Handy as a sanity check when
|
||||
/// stuffing a 32 bit value into a 16 bit container, etc.
|
||||
template <typename OUT_TYPE, typename IN_TYPE>
|
||||
auto static_cast_check_fit(IN_TYPE in) -> OUT_TYPE {
|
||||
// Make sure we don't try to use this when casting to or from floats or
|
||||
// doubles. We don't expect to always get the same value back
|
||||
// on casting back in that case.
|
||||
static_assert(!std::is_same<IN_TYPE, float>::value
|
||||
&& !std::is_same<IN_TYPE, double>::value
|
||||
&& !std::is_same<IN_TYPE, const float>::value
|
||||
&& !std::is_same<IN_TYPE, const double>::value
|
||||
&& !std::is_same<OUT_TYPE, float>::value
|
||||
&& !std::is_same<OUT_TYPE, double>::value
|
||||
&& !std::is_same<OUT_TYPE, const float>::value
|
||||
&& !std::is_same<OUT_TYPE, const double>::value,
|
||||
"static_cast_check_fit cannot be used with floats or doubles.");
|
||||
#if BA_DEBUG_BUILD
|
||||
assert(static_cast<IN_TYPE>(static_cast<OUT_TYPE>(in)) == in);
|
||||
#endif
|
||||
return static_cast<OUT_TYPE>(in);
|
||||
}
|
||||
|
||||
/// Like static_cast_check_fit, but runs checks even in release builds and
|
||||
/// throws an Exception on failure.
|
||||
template <typename OUT_TYPE, typename IN_TYPE>
|
||||
auto static_cast_check_fit_always(IN_TYPE in) -> OUT_TYPE {
|
||||
// Make sure we don't try to use this when casting to or from floats or
|
||||
// doubles. We don't expect to always get the same value back
|
||||
// on casting back in that case.
|
||||
static_assert(
|
||||
!std::is_same<IN_TYPE, float>::value
|
||||
&& !std::is_same<IN_TYPE, double>::value
|
||||
&& !std::is_same<IN_TYPE, const float>::value
|
||||
&& !std::is_same<IN_TYPE, const double>::value
|
||||
&& !std::is_same<OUT_TYPE, float>::value
|
||||
&& !std::is_same<OUT_TYPE, double>::value
|
||||
&& !std::is_same<OUT_TYPE, const float>::value
|
||||
&& !std::is_same<OUT_TYPE, const double>::value,
|
||||
"static_cast_always_checked cannot be used with floats or doubles.");
|
||||
auto out = static_cast<OUT_TYPE>(in);
|
||||
if (static_cast<IN_TYPE>(out) != in) {
|
||||
throw Exception("static_cast_check_fit_always failed for value "
|
||||
+ std::to_string(in) + ".");
|
||||
}
|
||||
return static_cast<OUT_TYPE>(in);
|
||||
}
|
||||
|
||||
/// Simply a static_cast, but in debug builds also runs a dynamic cast to
|
||||
/// ensure the results would have been the same. Handy for keeping casts
|
||||
/// lightweight when types are known while still having a sanity check.
|
||||
template <typename OUT_TYPE, typename IN_TYPE>
|
||||
auto static_cast_check_type(IN_TYPE in) -> OUT_TYPE {
|
||||
auto out_static = static_cast<OUT_TYPE>(in);
|
||||
#if BA_DEBUG_BUILD
|
||||
auto out_dynamic = dynamic_cast<OUT_TYPE>(in);
|
||||
assert(out_static == out_dynamic);
|
||||
#endif
|
||||
return out_static;
|
||||
}
|
||||
|
||||
// This call hijacks compile-type pretty-function-printing functionality
|
||||
// to give human-readable strings for arbitrary types. Note that these
|
||||
// will not be consistent across platforms and should only be used for
|
||||
// logging/debugging. Also note that this code is dependent on very specific
|
||||
// compiler output which could change at any time; to watch out for this
|
||||
// it is recommended to add static_assert()s somewhere to ensure that
|
||||
// output for a few given types matches expected result(s).
|
||||
template <typename T>
|
||||
constexpr auto static_type_name_constexpr(bool debug_full = false)
|
||||
-> std::string_view {
|
||||
std::string_view name, prefix, suffix;
|
||||
#ifdef __clang__
|
||||
name = __PRETTY_FUNCTION__;
|
||||
prefix =
|
||||
"std::string_view ballistica::"
|
||||
"static_type_name_constexpr(bool) [T = ";
|
||||
suffix = "]";
|
||||
#elif defined(__GNUC__)
|
||||
name = __PRETTY_FUNCTION__;
|
||||
prefix =
|
||||
"constexpr std::string_view "
|
||||
"ballistica::static_type_name_constexpr(bool) "
|
||||
"[with T = ";
|
||||
suffix = "; std::string_view = std::basic_string_view<char>]";
|
||||
#elif defined(_MSC_VER)
|
||||
name = __FUNCSIG__;
|
||||
prefix =
|
||||
"class std::basic_string_view<char,struct std::char_traits<char> > "
|
||||
"__cdecl ballistica::static_type_name_constexpr<";
|
||||
suffix = ">(bool)";
|
||||
#else
|
||||
#error unimplemented
|
||||
#endif
|
||||
if (debug_full) {
|
||||
return name;
|
||||
}
|
||||
name.remove_prefix(prefix.size());
|
||||
name.remove_suffix(suffix.size());
|
||||
return name;
|
||||
}
|
||||
|
||||
/// Return a human-readable string for the template type.
|
||||
template <typename T>
|
||||
static auto static_type_name(bool debug_full = false) -> std::string {
|
||||
return std::string(static_type_name_constexpr<T>(debug_full));
|
||||
}
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // __cplusplus
|
||||
|
||||
#endif // BALLISTICA_CORE_INLINE_H_
|
||||
214
src/ballistica/core/logging.cc
Normal file
214
src/ballistica/core/logging.cc
Normal file
@ -0,0 +1,214 @@
|
||||
// Copyright (c) 2011-2020 Eric Froemling
|
||||
|
||||
#include "ballistica/core/logging.h"
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "ballistica/app/app_globals.h"
|
||||
#include "ballistica/ballistica.h"
|
||||
#include "ballistica/game/game.h"
|
||||
#include "ballistica/networking/networking.h"
|
||||
#include "ballistica/networking/telnet_server.h"
|
||||
#include "ballistica/platform/platform.h"
|
||||
#include "ballistica/python/python.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
static void PrintCommon(const std::string& s) {
|
||||
// Print to in-game console.
|
||||
{
|
||||
if (g_game != nullptr) {
|
||||
g_game->PushConsolePrintCall(s);
|
||||
} else {
|
||||
if (g_platform != nullptr) {
|
||||
g_platform->HandleLog(
|
||||
"Warning: Log() called before game-thread setup; "
|
||||
"will not appear on in-game console.\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Print to any telnet clients.
|
||||
if (g_app_globals && g_app_globals->telnet_server) {
|
||||
g_app_globals->telnet_server->PushPrint(s);
|
||||
}
|
||||
}
|
||||
|
||||
void Logging::PrintStdout(const std::string& s, bool flush) {
|
||||
fprintf(stdout, "%s", s.c_str());
|
||||
if (flush) {
|
||||
fflush(stdout);
|
||||
}
|
||||
PrintCommon(s);
|
||||
}
|
||||
|
||||
void Logging::PrintStderr(const std::string& s, bool flush) {
|
||||
fprintf(stderr, "%s", s.c_str());
|
||||
if (flush) {
|
||||
fflush(stderr);
|
||||
}
|
||||
PrintCommon(s);
|
||||
}
|
||||
|
||||
void Logging::Log(const std::string& msg, bool to_stdout, bool to_server) {
|
||||
if (to_stdout) {
|
||||
PrintStdout(msg + "\n", true);
|
||||
}
|
||||
|
||||
// Ship to the platform logging mechanism (android-log, stderr, etc.)
|
||||
// if that's available yet.
|
||||
if (g_platform != nullptr) {
|
||||
g_platform->HandleLog(msg);
|
||||
}
|
||||
|
||||
// Ship to master-server/etc.
|
||||
if (to_server) {
|
||||
// Route through platform-specific loggers if present.
|
||||
// (things like Crashlytics crash-logging)
|
||||
if (g_platform) {
|
||||
Platform::DebugLog(msg);
|
||||
}
|
||||
|
||||
// Add to our complete log.
|
||||
if (g_app_globals != nullptr) {
|
||||
std::lock_guard<std::mutex> lock(g_app_globals->log_mutex);
|
||||
if (!g_app_globals->log_full) {
|
||||
(g_app_globals->log) += (msg + "\n");
|
||||
if ((g_app_globals->log).size() > 10000) {
|
||||
// Allow some reasonable overflow for last statement.
|
||||
if ((g_app_globals->log).size() > 100000) {
|
||||
// FIXME: This could potentially chop up utf-8 chars.
|
||||
(g_app_globals->log).resize(100000);
|
||||
}
|
||||
g_app_globals->log += "\n<max log size reached>\n";
|
||||
g_app_globals->log_full = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the game is fully bootstrapped, let the Python layer handle logs.
|
||||
// It will group log messages intelligently and ship them to the
|
||||
// master server with various other context info included.
|
||||
if (g_app_globals && g_app_globals->is_bootstrapped) {
|
||||
assert(g_python != nullptr);
|
||||
g_python->PushObjCall(Python::ObjID::kHandleLogCall);
|
||||
} else {
|
||||
// For log messages during bootstrapping we ship them immediately since
|
||||
// we don't know if the Python layer is (or will be) able to.
|
||||
if (g_early_log_writes > 0) {
|
||||
g_early_log_writes -= 1;
|
||||
std::string logprefix = "EARLY-LOG:";
|
||||
std::string logsuffix;
|
||||
|
||||
// If we're an early enough error, our global log isn't even available,
|
||||
// so include this specific message as a suffix instead.
|
||||
if (g_app_globals == nullptr) {
|
||||
logsuffix = msg;
|
||||
}
|
||||
DirectSendLogs(logprefix, logsuffix, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Logging::DirectSendLogs(const std::string& prefix,
|
||||
const std::string& suffix, bool instant,
|
||||
int* result) -> void {
|
||||
// Use a rough mechanism to restrict log uploads to 1 send per second.
|
||||
static time_t last_non_instant_send_time{-1};
|
||||
if (!instant) {
|
||||
auto curtime = Platform::GetCurrentSeconds();
|
||||
if (curtime == last_non_instant_send_time) {
|
||||
return;
|
||||
}
|
||||
last_non_instant_send_time = curtime;
|
||||
}
|
||||
|
||||
std::thread t([prefix, suffix, instant, result]() {
|
||||
// For non-instant sends, sleep for 2 seconds before sending logs;
|
||||
// this should capture the just-added log as well as any more that
|
||||
// got added in the subsequent second when we were not launching new
|
||||
// send threads.
|
||||
if (!instant) {
|
||||
Platform::SleepMS(2000);
|
||||
}
|
||||
std::string log;
|
||||
|
||||
// Send our blessing hash only after we've calculated it; don't use our
|
||||
// internal one. This means that we'll get false-negatives on whether
|
||||
// direct-sent logs are blessed, but I think that's better than false
|
||||
// positives.
|
||||
std::string calced_blessing_hash;
|
||||
if (g_app_globals) {
|
||||
std::lock_guard<std::mutex> lock(g_app_globals->log_mutex);
|
||||
log = g_app_globals->log;
|
||||
calced_blessing_hash = g_app_globals->calced_blessing_hash;
|
||||
} else {
|
||||
log = "(g_app_globals not yet inited; no global log available)";
|
||||
}
|
||||
if (!prefix.empty()) {
|
||||
log = prefix + "\n" + log;
|
||||
}
|
||||
if (!suffix.empty()) {
|
||||
log = log + "\n" + suffix;
|
||||
}
|
||||
|
||||
// Also send our blessing-calculation state; we may want to distinguish
|
||||
// between blessing not being calced yet and being confirmed as un-blessed.
|
||||
// FIXME: should probably do this in python layer log submits too.
|
||||
std::string bless_calc_state;
|
||||
if (kBlessingHash == nullptr) {
|
||||
bless_calc_state = "nointhash";
|
||||
} else if (g_app_globals == nullptr) {
|
||||
bless_calc_state = "noglobs";
|
||||
} else if (g_app_globals->calced_blessing_hash.empty()) {
|
||||
// Mention we're calculating, but also mention if it is likely that
|
||||
// the user is mucking with stuff.
|
||||
if (g_app_globals->user_ran_commands
|
||||
|| g_platform->using_custom_app_python_dir()) {
|
||||
bless_calc_state = "calcing_likely_modded";
|
||||
} else {
|
||||
bless_calc_state = "calcing_not_modded";
|
||||
}
|
||||
} else {
|
||||
bless_calc_state = "done";
|
||||
}
|
||||
|
||||
std::string path{"/bsLog"};
|
||||
std::map<std::string, std::string> params{
|
||||
{"log", log},
|
||||
{"time", "-1"},
|
||||
{"userAgentString", g_app_globals ? g_app_globals->user_agent_string
|
||||
: "(no g_app_globals)"},
|
||||
{"newsShow", calced_blessing_hash.c_str()},
|
||||
{"bcs", bless_calc_state.c_str()},
|
||||
{"build", std::to_string(kAppBuildNumber)}};
|
||||
try {
|
||||
Networking::MasterServerPost(path, params);
|
||||
if (result) {
|
||||
*result = 1; // SUCCESS!
|
||||
}
|
||||
} catch (const std::exception&) {
|
||||
// Try our fallback master-server address if that didn't work.
|
||||
try {
|
||||
params["log"] = prefix + "(FALLBACK-ADDR):\n" + log;
|
||||
Networking::MasterServerPost(path, params, true);
|
||||
if (result) {
|
||||
*result = 1; // SUCCESS!
|
||||
}
|
||||
} catch (const std::exception& exc) {
|
||||
// Well, we tried; make a note to platform log if available
|
||||
// that we failed.
|
||||
if (g_platform != nullptr) {
|
||||
g_platform->HandleLog(std::string("Early log-to-server failed: ")
|
||||
+ exc.what());
|
||||
}
|
||||
if (result) {
|
||||
*result = -1; // FAIL!!
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
t.detach();
|
||||
}
|
||||
|
||||
} // namespace ballistica
|
||||
35
src/ballistica/core/logging.h
Normal file
35
src/ballistica/core/logging.h
Normal file
@ -0,0 +1,35 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_CORE_LOGGING_H_
|
||||
#define BALLISTICA_CORE_LOGGING_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
class Logging {
|
||||
public:
|
||||
/// Print a string directly to stdout as well as the in-game console
|
||||
/// and any connected telnet consoles.
|
||||
static auto PrintStdout(const std::string& s, bool flush = false) -> void;
|
||||
|
||||
/// Print a string directly to stderr as well as the in-game console
|
||||
/// and any connected telnet consoles.
|
||||
static auto PrintStderr(const std::string& s, bool flush = false) -> void;
|
||||
|
||||
/// Write a string to the debug log.
|
||||
/// This will go to stdout, windows debug log, android log, etc. depending
|
||||
/// on the platform.
|
||||
static auto Log(const std::string& msg, bool to_stdout = true,
|
||||
bool to_server = true) -> void;
|
||||
|
||||
/// Ship logs to the master-server in a bg thread. If result is passed,
|
||||
/// it will be set to 1 on success and -1 on error.
|
||||
static auto DirectSendLogs(const std::string& prefix,
|
||||
const std::string& suffix, bool instant,
|
||||
int* result = nullptr) -> void;
|
||||
};
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // BALLISTICA_CORE_LOGGING_H_
|
||||
106
src/ballistica/core/macros.cc
Normal file
106
src/ballistica/core/macros.cc
Normal file
@ -0,0 +1,106 @@
|
||||
// Copyright (c) 2011-2020 Eric Froemling
|
||||
|
||||
#include "ballistica/core/macros.h"
|
||||
|
||||
#include "ballistica/platform/platform.h"
|
||||
#include "ballistica/python/python.h"
|
||||
|
||||
// Snippets of compiled functionality used by our evil macros.
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
void MacroFunctionTimerEnd(millisecs_t starttime, millisecs_t time,
|
||||
const char* funcname) {
|
||||
// Currently disabling this for test builds; not really useful for
|
||||
// the general public.
|
||||
if (g_buildconfig.test_build()) {
|
||||
return;
|
||||
}
|
||||
millisecs_t endtime = g_platform->GetTicks();
|
||||
if (endtime - starttime > time) {
|
||||
Log("Warning: " + std::to_string(endtime - starttime)
|
||||
+ " milliseconds spent in " + funcname);
|
||||
}
|
||||
}
|
||||
|
||||
void MacroFunctionTimerEndThread(millisecs_t starttime, millisecs_t time,
|
||||
const char* funcname) {
|
||||
// Currently disabling this for test builds; not really useful for
|
||||
// the general public.
|
||||
if (g_buildconfig.test_build()) {
|
||||
return;
|
||||
}
|
||||
millisecs_t endtime = g_platform->GetTicks();
|
||||
if (endtime - starttime > time) {
|
||||
Log("Warning: " + std::to_string(endtime - starttime)
|
||||
+ " milliseconds spent by " + ballistica::GetCurrentThreadName()
|
||||
+ " thread in " + funcname);
|
||||
}
|
||||
}
|
||||
|
||||
void MacroFunctionTimerEndEx(millisecs_t starttime, millisecs_t time,
|
||||
const char* funcname, const std::string& what) {
|
||||
// Currently disabling this for test builds; not really useful for
|
||||
// the general public.
|
||||
if (g_buildconfig.test_build()) {
|
||||
return;
|
||||
}
|
||||
millisecs_t endtime = g_platform->GetTicks();
|
||||
if (endtime - starttime > time) {
|
||||
Log("Warning: " + std::to_string(endtime - starttime)
|
||||
+ " milliseconds spent in " + funcname + " for " + what);
|
||||
}
|
||||
}
|
||||
|
||||
void MacroFunctionTimerEndThreadEx(millisecs_t starttime, millisecs_t time,
|
||||
const char* funcname,
|
||||
const std::string& what) {
|
||||
// Currently disabling this for test builds; not really useful for
|
||||
// the general public.
|
||||
if (g_buildconfig.test_build()) {
|
||||
return;
|
||||
}
|
||||
millisecs_t endtime = g_platform->GetTicks();
|
||||
if (endtime - starttime > time) {
|
||||
Log("Warning: " + std::to_string(endtime - starttime)
|
||||
+ " milliseconds spent by " + ballistica::GetCurrentThreadName()
|
||||
+ " thread in " + funcname + " for " + what);
|
||||
}
|
||||
}
|
||||
|
||||
void MacroTimeCheckEnd(millisecs_t starttime, millisecs_t time,
|
||||
const char* name, const char* file, int line) {
|
||||
// Currently disabling this for test builds; not really useful for
|
||||
// the general public.
|
||||
if (g_buildconfig.test_build()) {
|
||||
return;
|
||||
}
|
||||
millisecs_t e = g_platform->GetTicks();
|
||||
if (e - starttime > time) {
|
||||
Log(std::string("Warning: ") + name + " took "
|
||||
+ std::to_string(e - starttime) + " milliseconds; " + file + " line "
|
||||
+ std::to_string(line));
|
||||
}
|
||||
}
|
||||
|
||||
void MacroLogErrorTrace(const std::string& msg, const char* fname, int line) {
|
||||
char buffer[2048];
|
||||
snprintf(buffer, sizeof(buffer), "%s:%d:", fname, line);
|
||||
buffer[sizeof(buffer) - 1] = 0;
|
||||
Python::PrintStackTrace();
|
||||
Log(std::string(buffer) + " error: " + msg);
|
||||
}
|
||||
|
||||
void MacroLogError(const std::string& msg, const char* fname, int line) {
|
||||
char e_buffer[2048];
|
||||
snprintf(e_buffer, sizeof(e_buffer), "%s:%d:", fname, line);
|
||||
e_buffer[sizeof(e_buffer) - 1] = 0;
|
||||
ballistica::Log(std::string(e_buffer) + " error: " + msg);
|
||||
}
|
||||
|
||||
void MacroLogPythonTrace(const std::string& msg) {
|
||||
Python::PrintStackTrace();
|
||||
Log(msg);
|
||||
}
|
||||
|
||||
} // namespace ballistica
|
||||
166
src/ballistica/core/macros.h
Normal file
166
src/ballistica/core/macros.h
Normal file
@ -0,0 +1,166 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_CORE_MACROS_H_
|
||||
#define BALLISTICA_CORE_MACROS_H_
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#endif
|
||||
|
||||
#include "ballistica/core/types.h"
|
||||
|
||||
// Various utility macros and related support calls.
|
||||
// Trying to contain the evil in this one place.
|
||||
|
||||
// Trailing-semicolon note:
|
||||
// Some macros contain a ((void*) at the end. This is so the macro can be
|
||||
// followed by a semicolon without triggering an 'empty statement' warning.
|
||||
// I find standalone function-style macro invocations without semicolons
|
||||
// tends to confuse code formatters.
|
||||
|
||||
#define BA_STRINGIFY(x) #x
|
||||
|
||||
#define BA_BUILD_COMMAND_FILENAME \
|
||||
"<string: " __FILE__ " line " BA_STRINGIFY(__LINE__) ">"
|
||||
#define BA_BCFN BA_BUILD_COMMAND_FILENAME
|
||||
|
||||
#if BA_OSTYPE_WINDOWS
|
||||
#define BA_DIRSLASH "\\"
|
||||
#else
|
||||
#define BA_DIRSLASH "/"
|
||||
#endif
|
||||
|
||||
#if BA_DEBUG_BUILD
|
||||
#define BA_IFDEBUG(a) a
|
||||
#else
|
||||
#define BA_IFDEBUG(a) ((void)0)
|
||||
#endif
|
||||
|
||||
// Useful for finding hitches.
|
||||
// Call begin, followed at some point by any of the end versions.
|
||||
// FIXME: Turn these into C++ classes.
|
||||
#if BA_DEBUG_BUILD
|
||||
#define BA_DEBUG_FUNCTION_TIMER_BEGIN() \
|
||||
millisecs_t _dfts = g_platform->GetTicks()
|
||||
#define BA_DEBUG_FUNCTION_TIMER_END(time) \
|
||||
ballistica::MacroFunctionTimerEnd(_dfts, time, __PRETTY_FUNCTION__)
|
||||
#define BA_DEBUG_FUNCTION_TIMER_END_THREAD(time) \
|
||||
ballistica::MacroFunctionTimerEndThread(_dfts, time, __PRETTY_FUNCTION__)
|
||||
#define BA_DEBUG_FUNCTION_TIMER_END_EX(time, what) \
|
||||
MacroFunctionTimerEndEx(_dfts, time, __PRETTY_FUNCTION__, what)
|
||||
#define BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(time, what) \
|
||||
ballistica::MacroFunctionTimerEndThreadEx(_dfts, time, __PRETTY_FUNCTION__, \
|
||||
what)
|
||||
#define BA_DEBUG_TIME_CHECK_BEGIN(name) \
|
||||
millisecs_t name##_ts = g_platform->GetTicks()
|
||||
#define BA_DEBUG_TIME_CHECK_END(name, time) \
|
||||
ballistica::MacroTimeCheckEnd(name##_ts, time, #name, __FILE__, __LINE__)
|
||||
#else
|
||||
#define BA_DEBUG_FUNCTION_TIMER_BEGIN() ((void)0)
|
||||
#define BA_DEBUG_FUNCTION_TIMER_END(time) ((void)0)
|
||||
#define BA_DEBUG_FUNCTION_TIMER_END_THREAD(time) ((void)0)
|
||||
#define BA_DEBUG_FUNCTION_TIMER_END_EX(time, what) ((void)0)
|
||||
#define BA_DEBUG_FUNCTION_TIMER_END_THREAD_EX(time, what) ((void)0)
|
||||
#define BA_DEBUG_TIME_CHECK_BEGIN(name) ((void)0)
|
||||
#define BA_DEBUG_TIME_CHECK_END(name, time) ((void)0)
|
||||
#endif
|
||||
|
||||
// Disallow copying for a class.
|
||||
#define BA_DISALLOW_CLASS_COPIES(type) \
|
||||
type(const type& foo) = delete; \
|
||||
type& operator=(const type& src) = delete; /* NOLINT (macro parens) */
|
||||
|
||||
// Call this for errors which are non-fatal but should be noted so they can be
|
||||
// fixed.
|
||||
#define BA_LOG_ERROR_TRACE(msg) \
|
||||
ballistica::MacroLogErrorTrace(msg, __FILE__, __LINE__)
|
||||
|
||||
#define BA_LOG_ERROR_TRACE_ONCE(msg) \
|
||||
{ \
|
||||
static bool did_log_error_trace_here = false; \
|
||||
if (!did_log_error_trace_here) { \
|
||||
ballistica::MacroLogErrorTrace(msg, __FILE__, __LINE__); \
|
||||
did_log_error_trace_here = true; \
|
||||
} \
|
||||
} \
|
||||
((void)0) // (see 'Trailing-semicolon note' at top)
|
||||
|
||||
#define BA_LOG_ONCE(msg) \
|
||||
{ \
|
||||
static bool did_log_here = false; \
|
||||
if (!did_log_here) { \
|
||||
ballistica::Log(msg); \
|
||||
did_log_here = true; \
|
||||
} \
|
||||
} \
|
||||
((void)0) // (see 'Trailing-semicolon note' at top)
|
||||
|
||||
#define BA_LOG_PYTHON_TRACE(msg) ballistica::MacroLogPythonTrace(msg)
|
||||
|
||||
#define BA_LOG_PYTHON_TRACE_ONCE(msg) \
|
||||
{ \
|
||||
static bool did_log_python_trace_here = false; \
|
||||
if (!did_log_python_trace_here) { \
|
||||
ballistica::MacroLogPythonTrace(msg); \
|
||||
did_log_python_trace_here = true; \
|
||||
} \
|
||||
} \
|
||||
((void)0) // (see 'Trailing-semicolon note' at top)
|
||||
|
||||
/// Test a condition and throw an exception if it fails (on both debug and
|
||||
/// release builds)
|
||||
#define BA_PRECONDITION(b) \
|
||||
{ \
|
||||
if (!(b)) { \
|
||||
throw ballistica::Exception("Precondition failed: " #b); \
|
||||
} \
|
||||
} \
|
||||
((void)0) // (see 'Trailing-semicolon note' at top)
|
||||
|
||||
/// Test a condition and simply print a log message if it fails (on both debug
|
||||
/// and release builds)
|
||||
#define BA_PRECONDITION_LOG(b) \
|
||||
{ \
|
||||
if (!(b)) { \
|
||||
Log("Precondition failed: " #b); \
|
||||
} \
|
||||
} \
|
||||
((void)0) // (see 'Trailing-semicolon note' at top)
|
||||
|
||||
/// Test a condition and abort the program if it fails (on both debug
|
||||
/// and release builds)
|
||||
#define BA_PRECONDITION_FATAL(b) \
|
||||
{ \
|
||||
if (!(b)) { \
|
||||
FatalError("Precondition failed: " #b); \
|
||||
} \
|
||||
} \
|
||||
((void)0) // (see 'Trailing-semicolon note' at top)
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
// Support functions used by some of our macros; not intended to be used
|
||||
// directly.
|
||||
void MacroFunctionTimerEnd(millisecs_t starttime, millisecs_t time,
|
||||
const char* funcname);
|
||||
void MacroFunctionTimerEndThread(millisecs_t starttime, millisecs_t time,
|
||||
const char* funcname);
|
||||
void MacroFunctionTimerEndEx(millisecs_t starttime, millisecs_t time,
|
||||
const char* funcname, const std::string& what);
|
||||
void MacroFunctionTimerEndThreadEx(millisecs_t starttime, millisecs_t time,
|
||||
const char* funcname,
|
||||
const std::string& what);
|
||||
void MacroTimeCheckEnd(millisecs_t starttime, millisecs_t time,
|
||||
const char* name, const char* file, int line);
|
||||
void MacroLogErrorTrace(const std::string& msg, const char* fname, int line);
|
||||
void MacroLogError(const std::string& msg, const char* fname, int line);
|
||||
void MacroLogPythonTrace(const std::string& msg);
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // __cplusplus
|
||||
|
||||
#endif // BALLISTICA_CORE_MACROS_H_
|
||||
50
src/ballistica/core/module.cc
Normal file
50
src/ballistica/core/module.cc
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2011-2020 Eric Froemling
|
||||
|
||||
#include "ballistica/core/module.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "ballistica/core/thread.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
void Module::PushLocalRunnable(Runnable* runnable) {
|
||||
assert(std::this_thread::get_id() == thread()->thread_id());
|
||||
runnables_.push_back(runnable);
|
||||
}
|
||||
|
||||
void Module::PushRunnable(Runnable* runnable) {
|
||||
// If we're being called from the module's thread, just drop it in the list.
|
||||
// otherwise send it as a message to the other thread.
|
||||
if (std::this_thread::get_id() == thread()->thread_id()) {
|
||||
PushLocalRunnable(runnable);
|
||||
} else {
|
||||
thread_->PushModuleRunnable(runnable, id_);
|
||||
}
|
||||
}
|
||||
|
||||
Module::Module(std::string name_in, Thread* thread_in)
|
||||
: thread_(thread_in), name_(std::move(name_in)) {
|
||||
id_ = thread_->RegisterModule(name_, this);
|
||||
}
|
||||
|
||||
Module::~Module() = default;
|
||||
|
||||
auto Module::NewThreadTimer(millisecs_t length, bool repeat,
|
||||
const Object::Ref<Runnable>& runnable) -> Timer* {
|
||||
return thread_->NewTimer(length, repeat, runnable);
|
||||
}
|
||||
|
||||
void Module::RunPendingRunnables() {
|
||||
// Pull all runnables off the list first (its possible for one of these
|
||||
// runnables to add more) and then process them.
|
||||
assert(std::this_thread::get_id() == thread()->thread_id());
|
||||
std::list<Runnable*> runnables;
|
||||
runnables_.swap(runnables);
|
||||
for (Runnable* i : runnables) {
|
||||
i->Run();
|
||||
delete i;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ballistica
|
||||
70
src/ballistica/core/module.h
Normal file
70
src/ballistica/core/module.h
Normal file
@ -0,0 +1,70 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_CORE_MODULE_H_
|
||||
#define BALLISTICA_CORE_MODULE_H_
|
||||
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "ballistica/generic/lambda_runnable.h"
|
||||
#include "ballistica/generic/runnable.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
/// A logical entity that can be added to a thread and make use of its
|
||||
/// event loop.
|
||||
class Module {
|
||||
public:
|
||||
/// Add a runnable to this module's queue.
|
||||
/// Pass a Runnable that has been allocated with new().
|
||||
/// There must be no existing strong refs to it.
|
||||
/// It will be owned and disposed of by the module from this point.
|
||||
void PushRunnable(Runnable* runnable);
|
||||
|
||||
/// Convenience function to push a lambda as a runnable.
|
||||
template <typename F>
|
||||
void PushCall(const F& lambda) {
|
||||
PushRunnable(NewLambdaRunnableRaw(lambda));
|
||||
}
|
||||
|
||||
/// Return the thread this module is running on.
|
||||
auto thread() const -> Thread* { return thread_; }
|
||||
|
||||
virtual ~Module();
|
||||
|
||||
/// Push a runnable from the same thread as the module.
|
||||
void PushLocalRunnable(Runnable* runnable);
|
||||
|
||||
/// Called for each module when its thread is about to be suspended
|
||||
/// (on platforms such as mobile).
|
||||
virtual void HandleThreadPause() {}
|
||||
|
||||
/// Called for each module when its thread is about to be resumed
|
||||
/// (on platforms such as mobile).
|
||||
virtual void HandleThreadResume() {}
|
||||
|
||||
/// Whether this module has pending runnables.
|
||||
auto has_pending_runnables() const -> bool { return !runnables_.empty(); }
|
||||
|
||||
/// Used by the module's owner thread to let it do its thing.
|
||||
void RunPendingRunnables();
|
||||
|
||||
auto name() const -> const std::string& { return name_; }
|
||||
|
||||
protected:
|
||||
Module(std::string name, Thread* thread);
|
||||
auto NewThreadTimer(millisecs_t length, bool repeat,
|
||||
const Object::Ref<Runnable>& runnable) -> Timer*;
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
int id_{};
|
||||
std::list<Runnable*> runnables_;
|
||||
Thread* thread_{};
|
||||
};
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // BALLISTICA_CORE_MODULE_H_
|
||||
233
src/ballistica/core/object.cc
Normal file
233
src/ballistica/core/object.cc
Normal file
@ -0,0 +1,233 @@
|
||||
// Copyright (c) 2011-2020 Eric Froemling
|
||||
|
||||
#include "ballistica/core/object.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "ballistica/app/app_globals.h"
|
||||
#include "ballistica/generic/utils.h"
|
||||
#include "ballistica/platform/min_sdl.h"
|
||||
#include "ballistica/platform/platform.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
void Object::PrintObjects() {
|
||||
#if BA_DEBUG_BUILD
|
||||
std::string s;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_app_globals->object_list_mutex);
|
||||
s = std::to_string(g_app_globals->object_count) + " Objects at time "
|
||||
+ std::to_string(GetRealTime()) + ";";
|
||||
|
||||
if (explicit_bool(true)) {
|
||||
std::map<std::string, int> obj_map;
|
||||
|
||||
// Tally up counts for all types.
|
||||
int count = 0;
|
||||
for (Object* o = g_app_globals->object_list_first; o != nullptr;
|
||||
o = o->object_next_) {
|
||||
count++;
|
||||
std::string obj_name = o->GetObjectTypeName();
|
||||
auto i = obj_map.find(obj_name);
|
||||
if (i == obj_map.end()) {
|
||||
obj_map[obj_name] = 1;
|
||||
} else {
|
||||
// Getting complaints that 'second' is unused, but we sort and print
|
||||
// using this value like 10 lines down. Hmmm.
|
||||
#pragma clang diagnostic push
|
||||
#pragma ide diagnostic ignored "UnusedValue"
|
||||
i->second++;
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
// Now sort them by count and print.
|
||||
std::vector<std::pair<int, std::string> > sorted;
|
||||
sorted.reserve(obj_map.size());
|
||||
for (auto&& i : obj_map) {
|
||||
sorted.emplace_back(i.second, i.first);
|
||||
}
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
for (auto&& i : sorted) {
|
||||
s += "\n " + std::to_string(i.first) + ": " + i.second;
|
||||
}
|
||||
assert(count == g_app_globals->object_count);
|
||||
}
|
||||
}
|
||||
Log(s);
|
||||
#else
|
||||
Log("PrintObjects() only functions in debug builds.");
|
||||
#endif // BA_DEBUG_BUILD
|
||||
}
|
||||
|
||||
Object::Object() {
|
||||
#if BA_DEBUG_BUILD
|
||||
// Mark when we were born.
|
||||
object_birth_time_ = GetRealTime();
|
||||
|
||||
// Add ourself to the global object list.
|
||||
std::lock_guard<std::mutex> lock(g_app_globals->object_list_mutex);
|
||||
object_prev_ = nullptr;
|
||||
object_next_ = g_app_globals->object_list_first;
|
||||
g_app_globals->object_list_first = this;
|
||||
if (object_next_) {
|
||||
object_next_->object_prev_ = this;
|
||||
}
|
||||
g_app_globals->object_count++;
|
||||
#endif // BA_DEBUG_BUILD
|
||||
}
|
||||
|
||||
Object::~Object() {
|
||||
#if BA_DEBUG_BUILD
|
||||
// Pull ourself from the global obj list.
|
||||
std::lock_guard<std::mutex> lock(g_app_globals->object_list_mutex);
|
||||
if (object_next_) {
|
||||
object_next_->object_prev_ = object_prev_;
|
||||
}
|
||||
if (object_prev_) {
|
||||
object_prev_->object_next_ = object_next_;
|
||||
} else {
|
||||
g_app_globals->object_list_first = object_next_;
|
||||
}
|
||||
g_app_globals->object_count--;
|
||||
|
||||
// More sanity checks.
|
||||
if (object_strong_ref_count_ != 0) {
|
||||
// Avoiding Log for these low level errors; can lead to deadlock.
|
||||
printf(
|
||||
"Warning: Object is dying with non-zero ref-count; this is bad. "
|
||||
"(this "
|
||||
"might mean the object raised an exception in its constructor after "
|
||||
"being strong-referenced first).\n");
|
||||
}
|
||||
|
||||
#endif // BA_DEBUG_BUILD
|
||||
|
||||
// Invalidate all our weak refs.
|
||||
// We could call Release() on each but we'd have to deactivate the
|
||||
// thread-check since virtual functions won't work right in a destructor.
|
||||
// Also we can take a few shortcuts here since we know we're deleting the
|
||||
// entire list, not just one object.
|
||||
while (object_weak_refs_) {
|
||||
auto tmp = object_weak_refs_;
|
||||
object_weak_refs_ = tmp->next_;
|
||||
tmp->prev_ = nullptr;
|
||||
tmp->next_ = nullptr;
|
||||
tmp->obj_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
auto Object::GetObjectTypeName() const -> std::string {
|
||||
// Default implementation just returns type name.
|
||||
return g_platform->DemangleCXXSymbol(typeid(*this).name());
|
||||
}
|
||||
|
||||
auto Object::GetObjectDescription() const -> std::string {
|
||||
return "<" + GetObjectTypeName() + " object at " + Utils::PtrToString(this)
|
||||
+ ">";
|
||||
}
|
||||
|
||||
auto Object::GetThreadOwnership() const -> Object::ThreadOwnership {
|
||||
#if BA_DEBUG_BUILD
|
||||
return thread_ownership_;
|
||||
#else
|
||||
// Not used in release build so doesn't matter.
|
||||
return ThreadOwnership::kAny;
|
||||
#endif
|
||||
}
|
||||
|
||||
auto Object::GetDefaultOwnerThread() const -> ThreadIdentifier {
|
||||
return ThreadIdentifier::kGame;
|
||||
}
|
||||
|
||||
#if BA_DEBUG_BUILD
|
||||
|
||||
static auto GetCurrentThreadIdentifier() -> ThreadIdentifier {
|
||||
if (InMainThread()) {
|
||||
return ThreadIdentifier::kMain;
|
||||
} else if (InGameThread()) {
|
||||
return ThreadIdentifier::kGame;
|
||||
} else if (InAudioThread()) {
|
||||
return ThreadIdentifier::kAudio;
|
||||
} else if (InNetworkWriteThread()) {
|
||||
return ThreadIdentifier::kNetworkWrite;
|
||||
} else if (InMediaThread()) {
|
||||
return ThreadIdentifier::kMedia;
|
||||
} else if (InBGDynamicsThread()) {
|
||||
return ThreadIdentifier::kBGDynamics;
|
||||
} else {
|
||||
throw Exception(std::string("unrecognized thread: ")
|
||||
+ GetCurrentThreadName());
|
||||
}
|
||||
}
|
||||
|
||||
void Object::ObjectThreadCheck() {
|
||||
if (!thread_checks_enabled_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadOwnership thread_ownership = GetThreadOwnership();
|
||||
if (thread_ownership == ThreadOwnership::kAny) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're set to use the next-referencing thread
|
||||
// and haven't set that yet, do so.
|
||||
if (thread_ownership == ThreadOwnership::kNextReferencing
|
||||
&& owner_thread_ == ThreadIdentifier::kInvalid) {
|
||||
owner_thread_ = GetCurrentThreadIdentifier();
|
||||
}
|
||||
|
||||
ThreadIdentifier t;
|
||||
if (thread_ownership == ThreadOwnership::kClassDefault) {
|
||||
t = GetDefaultOwnerThread();
|
||||
} else {
|
||||
t = owner_thread_;
|
||||
}
|
||||
#define DO_FAIL(THREADNAME) \
|
||||
throw Exception("ObjectThreadCheck failed for " + GetObjectDescription() \
|
||||
+ "; expected " THREADNAME " thread; got " \
|
||||
+ GetCurrentThreadName())
|
||||
switch (t) {
|
||||
case ThreadIdentifier::kMain:
|
||||
if (!InMainThread()) {
|
||||
DO_FAIL("Main");
|
||||
}
|
||||
break;
|
||||
case ThreadIdentifier::kGame:
|
||||
if (!InGameThread()) {
|
||||
DO_FAIL("Game");
|
||||
}
|
||||
break;
|
||||
case ThreadIdentifier::kAudio:
|
||||
if (!InAudioThread()) {
|
||||
DO_FAIL("Audio");
|
||||
}
|
||||
break;
|
||||
case ThreadIdentifier::kNetworkWrite:
|
||||
if (!InNetworkWriteThread()) {
|
||||
DO_FAIL("NetworkWrite");
|
||||
}
|
||||
break;
|
||||
case ThreadIdentifier::kMedia:
|
||||
if (!InMediaThread()) {
|
||||
DO_FAIL("Media");
|
||||
}
|
||||
break;
|
||||
case ThreadIdentifier::kBGDynamics:
|
||||
if (!InBGDynamicsThread()) {
|
||||
DO_FAIL("BGDynamics");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw Exception();
|
||||
}
|
||||
#undef DO_FAIL
|
||||
}
|
||||
#endif // BA_DEBUG_BUILD
|
||||
|
||||
} // namespace ballistica
|
||||
671
src/ballistica/core/object.h
Normal file
671
src/ballistica/core/object.h
Normal file
@ -0,0 +1,671 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_CORE_OBJECT_H_
|
||||
#define BALLISTICA_CORE_OBJECT_H_
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ballistica/ballistica.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
/// Objects supporting strong and weak referencing and thread enforcement.
|
||||
/// A rule or two for for Objects:
|
||||
/// Don't throw exceptions out of object destructors;
|
||||
/// This will break references to that object and lead to crashes if/when they
|
||||
/// are used.
|
||||
class Object {
|
||||
public:
|
||||
Object();
|
||||
virtual ~Object();
|
||||
|
||||
/// Prints a tally of object types and counts (debug build only).
|
||||
static void PrintObjects();
|
||||
|
||||
// Object classes can provide descriptive names for themselves;
|
||||
// these are used for debugging and other purposes.
|
||||
// The default is to use the C++ symbol name, demangling it when possible.
|
||||
// IMPORTANT: Do not rely on this being consistent across builds/platforms.
|
||||
virtual auto GetObjectTypeName() const -> std::string;
|
||||
|
||||
// Provide a brief description of this particular object; by default returns
|
||||
// type-name plus address.
|
||||
virtual auto GetObjectDescription() const -> std::string;
|
||||
|
||||
// This is called when adding or removing a reference to an Object;
|
||||
// it can perform sanity-tests to make sure references are not being
|
||||
// added at incorrect times or from incorrect threads.
|
||||
// The default implementation uses the per-object
|
||||
// ThreadOwnership/ThreadIdentifier values accessible below. NOTE: this
|
||||
// check runs only in the debug build so don't add any logical side-effects!
|
||||
#if BA_DEBUG_BUILD
|
||||
virtual void ObjectThreadCheck();
|
||||
#endif
|
||||
|
||||
enum class ThreadOwnership {
|
||||
kClassDefault, // Uses class' GetDefaultOwnerThread() call.
|
||||
kNextReferencing, // Uses whichever thread next acquires/accesses a ref.
|
||||
kCustom, // Always use a specific thread.
|
||||
kAny // Any thread is fine.
|
||||
};
|
||||
|
||||
/// Called by the default ObjectThreadCheck() to determine ThreadOwnership
|
||||
/// for an Object. The default uses the object's individual value
|
||||
/// (which defaults to ThreadOwnership::kClassDefault and can be set via
|
||||
/// SetThreadOwnership())
|
||||
virtual auto GetThreadOwnership() const -> ThreadOwnership;
|
||||
|
||||
/// Return the exact thread to check for with ThreadOwnership::kClassDefault
|
||||
/// (in the default ObjectThreadCheck implementation at least).
|
||||
/// Default returns ThreadIdentifier::kGame
|
||||
virtual auto GetDefaultOwnerThread() const -> ThreadIdentifier;
|
||||
|
||||
/// Set thread ownership values for an individual object.
|
||||
/// Note that these values may be ignored if ObjectThreadCheck() is
|
||||
/// overridden, and thread_identifier is only relevant when ownership is
|
||||
/// ThreadOwnership::kCustom.
|
||||
/// UPDATE: turning off per-object controls; gonna see if we can get by
|
||||
/// with just set_thread_checks_enabled() for temp special cases...
|
||||
void SetThreadOwnership(
|
||||
ThreadOwnership ownership,
|
||||
ThreadIdentifier thread_identifier = ThreadIdentifier::kGame) {
|
||||
#if BA_DEBUG_BUILD
|
||||
thread_ownership_ = ownership;
|
||||
if (thread_ownership_ == ThreadOwnership::kNextReferencing) {
|
||||
owner_thread_ = ThreadIdentifier::kInvalid;
|
||||
} else {
|
||||
owner_thread_ = thread_identifier;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Return true if the object is ref-counted and has at least 1 strong ref.
|
||||
// This is generally a good thing for calls accepting object ptrs to check.
|
||||
// Note that this can return false positives in release builds so should
|
||||
// mainly be used as a debug sanity check (erroring if false)
|
||||
auto is_valid_refcounted_object() const -> bool {
|
||||
#if BA_DEBUG_BUILD
|
||||
if (object_is_dead_) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
return (object_strong_ref_count_ > 0);
|
||||
}
|
||||
|
||||
auto object_strong_ref_count() const -> int {
|
||||
return object_strong_ref_count_;
|
||||
}
|
||||
template <typename T = Object>
|
||||
class Ref;
|
||||
template <typename T = Object>
|
||||
class WeakRef;
|
||||
|
||||
class WeakRefBase {
|
||||
public:
|
||||
WeakRefBase() = default;
|
||||
~WeakRefBase() { Release(); }
|
||||
|
||||
void Release() {
|
||||
if (obj_) {
|
||||
#if BA_DEBUG_BUILD
|
||||
obj_->ObjectThreadCheck();
|
||||
#endif
|
||||
if (next_) {
|
||||
next_->prev_ = prev_;
|
||||
}
|
||||
if (prev_) {
|
||||
prev_->next_ = next_;
|
||||
} else {
|
||||
obj_->object_weak_refs_ = next_;
|
||||
}
|
||||
obj_ = nullptr;
|
||||
next_ = prev_ = nullptr;
|
||||
} else {
|
||||
assert(next_ == nullptr && prev_ == nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Object* obj_ = nullptr;
|
||||
WeakRefBase* prev_ = nullptr;
|
||||
WeakRefBase* next_ = nullptr;
|
||||
friend class Object;
|
||||
}; // WeakRefBase
|
||||
|
||||
/// Weak-reference to an instance of a specific Object subclass.
|
||||
template <typename T>
|
||||
class WeakRef : public WeakRefBase {
|
||||
public:
|
||||
auto exists() const -> bool { return (obj_ != nullptr); }
|
||||
|
||||
void Clear() { Release(); }
|
||||
|
||||
// Return a pointer or nullptr.
|
||||
auto get() const -> T* {
|
||||
// Yes, reinterpret_cast is evil, but we make sure
|
||||
// we only operate on cases where this is valid
|
||||
// (see Acquire()).
|
||||
return reinterpret_cast<T*>(obj_);
|
||||
}
|
||||
|
||||
// These operators throw exceptions if the object is dead.
|
||||
auto operator*() const -> T& {
|
||||
if (!obj_) {
|
||||
throw Exception("Invalid dereference of " + static_type_name<T>());
|
||||
}
|
||||
|
||||
// Yes, reinterpret_cast is evil, but we make sure
|
||||
// we only operate on cases where this is valid
|
||||
// (see Acquire()).
|
||||
return *reinterpret_cast<T*>(obj_);
|
||||
}
|
||||
auto operator->() const -> T* {
|
||||
if (!obj_) {
|
||||
throw Exception("Invalid dereference of " + static_type_name<T>());
|
||||
}
|
||||
|
||||
// Yes, reinterpret_cast is evil, but we make sure
|
||||
// we only operate on cases where this is valid
|
||||
// (see Acquire()).
|
||||
return reinterpret_cast<T*>(obj_);
|
||||
}
|
||||
|
||||
// Assign/compare with any compatible pointer.
|
||||
template <typename U>
|
||||
auto operator=(U* ptr) -> WeakRef<T>& {
|
||||
Release();
|
||||
|
||||
// Go through our template type instead of assigning directly
|
||||
// to our Object* so we catch invalid assigns at compile-time.
|
||||
T* tmp = ptr;
|
||||
if (tmp) Acquire(tmp);
|
||||
|
||||
// More debug sanity checks.
|
||||
assert(reinterpret_cast<T*>(obj_) == ptr);
|
||||
assert(static_cast<T*>(obj_) == ptr);
|
||||
assert(dynamic_cast<T*>(obj_) == ptr);
|
||||
return *this;
|
||||
}
|
||||
template <typename U>
|
||||
auto operator==(U* ptr) -> bool {
|
||||
return (get() == ptr);
|
||||
}
|
||||
template <typename U>
|
||||
auto operator!=(U* ptr) -> bool {
|
||||
return (get() != ptr);
|
||||
}
|
||||
|
||||
// Assign/compare with same type ref (apparently the template below doesn't
|
||||
// cover this case?).
|
||||
auto operator=(const WeakRef<T>& ref) -> WeakRef<T>& {
|
||||
*this = ref.get();
|
||||
return *this;
|
||||
}
|
||||
auto operator==(const WeakRef<T>& ref) -> bool {
|
||||
return (get() == ref.get());
|
||||
}
|
||||
auto operator!=(const WeakRef<T>& ref) -> bool {
|
||||
return (get() != ref.get());
|
||||
}
|
||||
|
||||
// Assign/compare with any compatible strong-ref.
|
||||
template <typename U>
|
||||
auto operator=(const Ref<U>& ref) -> WeakRef<T>& {
|
||||
*this = ref.get();
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename U>
|
||||
auto operator==(const Ref<U>& ref) -> bool {
|
||||
return (get() == ref.get());
|
||||
}
|
||||
|
||||
template <typename U>
|
||||
auto operator!=(const Ref<U>& ref) -> bool {
|
||||
return (get() != ref.get());
|
||||
}
|
||||
|
||||
// Assign/compare with any compatible weak-ref.
|
||||
template <typename U>
|
||||
auto operator=(const WeakRef<U>& ref) -> WeakRef<T>& {
|
||||
*this = ref.get();
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename U>
|
||||
auto operator==(const WeakRef<U>& ref) -> bool {
|
||||
return (get() == ref.get());
|
||||
}
|
||||
|
||||
template <typename U>
|
||||
auto operator!=(const WeakRef<U>& ref) -> bool {
|
||||
return (get() != ref.get());
|
||||
}
|
||||
|
||||
// Various constructors:
|
||||
|
||||
// Empty.
|
||||
WeakRef() = default;
|
||||
|
||||
// From our type pointer.
|
||||
explicit WeakRef(T* obj) { *this = obj; }
|
||||
|
||||
// Copy constructor (only non-explicit one).
|
||||
WeakRef(const WeakRef<T>& ref) { *this = ref.get(); }
|
||||
|
||||
// From a compatible pointer.
|
||||
template <typename U>
|
||||
explicit WeakRef(U* ptr) {
|
||||
*this = ptr;
|
||||
}
|
||||
|
||||
// From a compatible strong ref.
|
||||
template <typename U>
|
||||
explicit WeakRef(const Ref<U>& ref) {
|
||||
*this = ref;
|
||||
}
|
||||
|
||||
// From a compatible weak ref.
|
||||
template <typename U>
|
||||
explicit WeakRef(const WeakRef<U>& ref) {
|
||||
*this = ref;
|
||||
}
|
||||
|
||||
private:
|
||||
void Acquire(T* obj) {
|
||||
if (obj == nullptr) {
|
||||
throw Exception("Acquiring invalid ptr of " + static_type_name<T>());
|
||||
}
|
||||
#if BA_DEBUG_BUILD
|
||||
|
||||
// Seems like it'd be a good idea to prevent creation of weak-refs to
|
||||
// objects in their destructors, but it turns out we're currently
|
||||
// doing this (session points contexts at itself as it dies, etc.)
|
||||
// Perhaps later can untangle this and change the behavior.
|
||||
obj->ObjectThreadCheck();
|
||||
assert(obj_ == nullptr && next_ == nullptr && prev_ == nullptr);
|
||||
#endif
|
||||
if (obj->object_weak_refs_) {
|
||||
obj->object_weak_refs_->prev_ = this;
|
||||
next_ = obj->object_weak_refs_;
|
||||
}
|
||||
obj->object_weak_refs_ = this;
|
||||
|
||||
// Sanity checking: We make the assumption that static-casting our pointer
|
||||
// to/from Object gives the same results as reinterpret-casting it; let's
|
||||
// be certain that's the case. In some cases involving multiple
|
||||
// inheritance this might not be true, but we avoid those cases in our
|
||||
// object hierarchy. (the one type of multiple inheritance we allow is
|
||||
// pure virtual 'interfaces' which should not affect pointer offsets)
|
||||
assert(static_cast<Object*>(obj) == reinterpret_cast<Object*>(obj));
|
||||
|
||||
// More random sanity checking.
|
||||
assert(dynamic_cast<T*>(reinterpret_cast<Object*>(obj)) == obj);
|
||||
obj_ = obj;
|
||||
}
|
||||
}; // WeakRef
|
||||
|
||||
// Strong-ref.
|
||||
template <typename T>
|
||||
class Ref {
|
||||
public:
|
||||
~Ref() { Release(); }
|
||||
auto get() const -> T* { return obj_; }
|
||||
|
||||
// These operators throw an Exception if the object is dead.
|
||||
auto operator*() const -> T& {
|
||||
if (!obj_) {
|
||||
throw Exception("Invalid dereference of " + static_type_name<T>());
|
||||
}
|
||||
return *obj_;
|
||||
}
|
||||
auto operator->() const -> T* {
|
||||
if (!obj_) {
|
||||
throw Exception("Invalid dereference of " + static_type_name<T>());
|
||||
}
|
||||
return obj_;
|
||||
}
|
||||
auto exists() const -> bool { return (obj_ != nullptr); }
|
||||
void Clear() { Release(); }
|
||||
|
||||
// Assign/compare with any compatible pointer.
|
||||
template <typename U>
|
||||
auto operator=(U* ptr) -> Ref<T>& {
|
||||
Release();
|
||||
if (ptr) {
|
||||
Acquire(ptr);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
template <typename U>
|
||||
auto operator==(U* ptr) -> bool {
|
||||
return (get() == ptr);
|
||||
}
|
||||
template <typename U>
|
||||
auto operator!=(U* ptr) -> bool {
|
||||
return (get() != ptr);
|
||||
}
|
||||
|
||||
auto operator==(const Ref<T>& ref) -> bool { return (get() == ref.get()); }
|
||||
auto operator!=(const Ref<T>& ref) -> bool { return (get() != ref.get()); }
|
||||
|
||||
// Assign/compare with same type ref (apparently the generic template below
|
||||
// doesn't cover that case?..)
|
||||
// DANGER: Seems to still compile if we comment this out, but crashes.
|
||||
// Should get to the bottom of that.
|
||||
auto operator=(const Ref<T>& ref) -> Ref<T>& {
|
||||
assert(this != &ref); // Shouldn't be self-assigning.
|
||||
*this = ref.get();
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Assign/compare with any compatible strong-ref.
|
||||
template <typename U>
|
||||
auto operator=(const Ref<U>& ref) -> Ref<T>& {
|
||||
*this = ref.get();
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename U>
|
||||
auto operator==(const Ref<U>& ref) -> bool {
|
||||
return (get() == ref.get());
|
||||
}
|
||||
|
||||
template <typename U>
|
||||
auto operator!=(const Ref<U>& ref) -> bool {
|
||||
return (get() != ref.get());
|
||||
}
|
||||
|
||||
// Assign/compare from any compatible weak-ref.
|
||||
template <typename U>
|
||||
auto operator=(const WeakRef<U>& ref) -> Ref<T>& {
|
||||
*this = ref.get();
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename U>
|
||||
auto operator==(const WeakRef<U>& ref) -> bool {
|
||||
return (get() == ref.get());
|
||||
}
|
||||
|
||||
template <typename U>
|
||||
auto operator!=(const WeakRef<U>& ref) -> bool {
|
||||
return (get() != ref.get());
|
||||
}
|
||||
|
||||
// Various constructors:
|
||||
|
||||
// Empty.
|
||||
Ref() = default;
|
||||
|
||||
// From our type pointer.
|
||||
explicit Ref(T* obj) { *this = obj; }
|
||||
|
||||
// Copy constructor (only non-explicit one).
|
||||
Ref(const Ref<T>& ref) { *this = ref.get(); }
|
||||
|
||||
// From a compatible pointer.
|
||||
template <typename U>
|
||||
explicit Ref(U* ptr) {
|
||||
*this = ptr;
|
||||
}
|
||||
|
||||
// From a compatible strong ref.
|
||||
template <typename U>
|
||||
explicit Ref(const Ref<U>& ref) {
|
||||
*this = ref;
|
||||
}
|
||||
|
||||
// From a compatible weak ref.
|
||||
template <typename U>
|
||||
explicit Ref(const WeakRef<U>& ref) {
|
||||
*this = ref;
|
||||
}
|
||||
|
||||
private:
|
||||
void Acquire(T* obj) {
|
||||
if (obj == nullptr) {
|
||||
throw Exception("Acquiring invalid ptr of " + static_type_name<T>());
|
||||
}
|
||||
|
||||
#if BA_DEBUG_BUILD
|
||||
obj->ObjectThreadCheck();
|
||||
|
||||
// Obvs shouldn't be referencing dead stuff.
|
||||
assert(!obj->object_is_dead_);
|
||||
|
||||
// Complain if creating an initial strong-ref to something
|
||||
// not marked as ref-counted.
|
||||
// (should make this an error once we know these are out of the system)
|
||||
if (!obj->object_has_strong_ref_
|
||||
&& !obj->object_creating_strong_reffed_) {
|
||||
// Log only to system log for these low-level errors;
|
||||
// console or server can cause deadlock due to recursive
|
||||
// ref-list locks.
|
||||
printf(
|
||||
"Incorrectly creating initial strong-ref to %s; use "
|
||||
"New() or MakeRefCounted()\n",
|
||||
obj->GetObjectDescription().c_str());
|
||||
}
|
||||
obj->object_has_strong_ref_ = true;
|
||||
#endif // BA_DEBUG_BUILD
|
||||
|
||||
obj->object_strong_ref_count_++;
|
||||
obj_ = obj;
|
||||
}
|
||||
void Release() {
|
||||
if (obj_ != nullptr) {
|
||||
#if BA_DEBUG_BUILD
|
||||
obj_->ObjectThreadCheck();
|
||||
#endif
|
||||
assert(obj_->object_strong_ref_count_ > 0);
|
||||
obj_->object_strong_ref_count_--;
|
||||
T* tmp = obj_;
|
||||
|
||||
// Invalidate ref *before* delete to avoid potential double-release.
|
||||
obj_ = nullptr;
|
||||
if (tmp->object_strong_ref_count_ == 0) {
|
||||
#if BA_DEBUG_BUILD
|
||||
tmp->object_is_dead_ = true;
|
||||
#endif
|
||||
delete tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
T* obj_ = nullptr;
|
||||
};
|
||||
|
||||
/// Object::New<Type>(): The preferred way to create ref-counted Objects.
|
||||
/// Allocates a new Object with the provided args and returns a strong
|
||||
/// reference to it.
|
||||
/// Generally you pass a single type to be instantiated and returned,
|
||||
/// but you can optionally specify the two separately.
|
||||
/// (for instance you may want to create a Button but return
|
||||
/// a Ref to a Widget)
|
||||
template <typename TRETURN, typename TALLOC = TRETURN, typename... ARGS>
|
||||
[[nodiscard]] static auto New(ARGS&&... args) -> Object::Ref<TRETURN> {
|
||||
auto* ptr = new TALLOC(std::forward<ARGS>(args)...);
|
||||
#if BA_DEBUG_BUILD
|
||||
if (ptr->object_creating_strong_reffed_) {
|
||||
// Avoiding Log for these low level errors; can lead to deadlock.
|
||||
printf("Object already set up as reffed in New: %s\n",
|
||||
ptr->GetObjectDescription().c_str());
|
||||
}
|
||||
if (ptr->object_strong_ref_count_ > 0) {
|
||||
// TODO(ericf): make this an error once its cleared out
|
||||
printf("Obj strong-ref in constructor: %s\n",
|
||||
ptr->GetObjectDescription().c_str());
|
||||
}
|
||||
ptr->object_in_constructor_ = false;
|
||||
ptr->object_creating_strong_reffed_ = true;
|
||||
#endif // BA_DEBUG_BUILD
|
||||
return Object::Ref<TRETURN>(ptr);
|
||||
}
|
||||
|
||||
/// In some cases it may be handy to allocate an object for ref-counting
|
||||
/// but not actually create references yet. (Such as when creating an object
|
||||
/// in one thread to be passed to another which will own said object)
|
||||
/// For such cases, allocate using NewDeferred() and then use MakeRefCounted()
|
||||
/// on the raw Object* to create its initial reference.
|
||||
/// Note that in debug builds this will run checks to make sure the object
|
||||
/// wound up being ref-counted. To allocate an object for manual
|
||||
/// deallocation, use NewUnmanaged()
|
||||
template <typename T, typename... ARGS>
|
||||
[[nodiscard]] static auto NewDeferred(ARGS&&... args) -> T* {
|
||||
T* ptr = new T(std::forward<ARGS>(args)...);
|
||||
#if BA_DEBUG_BUILD
|
||||
if (ptr->object_strong_ref_count_ > 0) {
|
||||
printf("Obj strong-ref in constructor: %s\n",
|
||||
ptr->GetObjectDescription().c_str());
|
||||
}
|
||||
ptr->object_in_constructor_ = false;
|
||||
#endif
|
||||
return ptr;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static auto MakeRefCounted(T* ptr) -> Object::Ref<T> {
|
||||
#if BA_DEBUG_BUILD
|
||||
// Make sure we're operating on a fresh object.
|
||||
assert(ptr->object_strong_ref_count_ == 0);
|
||||
if (ptr->object_creating_strong_reffed_) {
|
||||
// Avoiding Log for these low level errors; can lead to deadlock.
|
||||
printf("Object already set up as reffed in MakeRefCounted: %s\n",
|
||||
ptr->GetObjectDescription().c_str());
|
||||
}
|
||||
ptr->object_creating_strong_reffed_ = true;
|
||||
#endif
|
||||
return Object::Ref<T>(ptr);
|
||||
}
|
||||
|
||||
/// Allocate an Object with no ref-counting; for use when an object
|
||||
/// will be manually managed/deleted.
|
||||
/// In debug builds, these objects will complain if attempts are made to
|
||||
/// create strong references to them.
|
||||
template <typename T, typename... ARGS>
|
||||
[[nodiscard]] static auto NewUnmanaged(ARGS&&... args) -> T* {
|
||||
T* ptr = new T(std::forward<ARGS>(args)...);
|
||||
#if BA_DEBUG_BUILD
|
||||
ptr->object_in_constructor_ = false;
|
||||
#endif
|
||||
return ptr;
|
||||
}
|
||||
|
||||
private:
|
||||
// Making operator new private here to help ensure all of our dynamic
|
||||
// allocation/deallocation goes through our special functions (New(),
|
||||
// NewDeferred(), etc.). However, sticking with original new for release
|
||||
// builds since it may handle corner cases this does not.
|
||||
#if BA_DEBUG_BUILD
|
||||
auto operator new(size_t size) -> void* { return new char[size]; }
|
||||
#endif
|
||||
|
||||
#if BA_DEBUG_BUILD
|
||||
bool object_has_strong_ref_{};
|
||||
bool object_creating_strong_reffed_{};
|
||||
bool object_is_dead_{};
|
||||
bool object_in_constructor_{true};
|
||||
Object* object_next_{};
|
||||
Object* object_prev_{};
|
||||
ThreadOwnership thread_ownership_{ThreadOwnership::kClassDefault};
|
||||
ThreadIdentifier owner_thread_{ThreadIdentifier::kInvalid};
|
||||
bool thread_checks_enabled_{true};
|
||||
millisecs_t object_birth_time_{};
|
||||
bool object_printed_warning_{};
|
||||
#endif
|
||||
int object_strong_ref_count_{};
|
||||
WeakRefBase* object_weak_refs_{};
|
||||
BA_DISALLOW_CLASS_COPIES(Object);
|
||||
}; // Object
|
||||
|
||||
/// Convert a vector of ptrs into a vector of refs.
|
||||
template <typename T>
|
||||
auto PointersToRefs(const std::vector<T*>& ptrs)
|
||||
-> std::vector<Object::Ref<T> > {
|
||||
std::vector<Object::Ref<T> > refs;
|
||||
refs.reserve(ptrs.size());
|
||||
for (typename std::vector<T*>::const_iterator i = ptrs.begin();
|
||||
i != ptrs.end(); i++) {
|
||||
refs.push_back(Object::Ref<T>(*i));
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
/// Convert a vector of ptrs into a vector of refs.
|
||||
template <typename T>
|
||||
auto PointersToWeakRefs(const std::vector<T*>& ptrs)
|
||||
-> std::vector<Object::WeakRef<T> > {
|
||||
std::vector<Object::WeakRef<T> > refs;
|
||||
refs.reserve(ptrs.size());
|
||||
for (typename std::vector<T*>::const_iterator i = ptrs.begin();
|
||||
i != ptrs.end(); i++) {
|
||||
refs.push_back(Object::WeakRef<T>(*i));
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
/// Convert a vector of refs to a vector of ptrs.
|
||||
template <typename T>
|
||||
auto RefsToPointers(const std::vector<Object::Ref<T> >& refs)
|
||||
-> std::vector<T*> {
|
||||
std::vector<T*> ptrs;
|
||||
auto refs_size = refs.size();
|
||||
if (refs_size > 0) {
|
||||
ptrs.resize(refs_size);
|
||||
|
||||
// Let's just access the memory directly; potentially faster?
|
||||
T** p = &(ptrs[0]);
|
||||
for (size_t i = 0; i < refs_size; i++) {
|
||||
p[i] = refs[i].get();
|
||||
}
|
||||
}
|
||||
return ptrs;
|
||||
}
|
||||
|
||||
/// Prune dead refs out of a vector/list.
|
||||
template <typename T>
|
||||
void PruneDeadRefs(T* list) {
|
||||
for (typename T::iterator i = list->begin(); i != list->end();) {
|
||||
if (!i->exists()) {
|
||||
i = list->erase(i);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prune dead refs out of a map/etc.
|
||||
template <typename T>
|
||||
void PruneDeadMapRefs(T* map) {
|
||||
for (typename T::iterator i = map->begin(); i != map->end();) {
|
||||
if (!i->second.exists()) {
|
||||
typename T::iterator i_next = i;
|
||||
i_next++;
|
||||
map->erase(i);
|
||||
i = i_next;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Print an Object (handles nullptr too).
|
||||
inline auto ObjToString(Object* obj) -> std::string {
|
||||
return obj ? obj->GetObjectDescription() : "<nullptr>";
|
||||
}
|
||||
|
||||
// A handy utility which creates a weak-ref in debug mode
|
||||
// and a simple pointer in release mode.
|
||||
// This can be used when a pointer *should* always be valid
|
||||
// but its nice to be sure when the cpu cycles don't matter
|
||||
#if BA_DEBUG_BUILD
|
||||
#define BA_DEBUG_PTR(TYPE) Object::WeakRef<TYPE>
|
||||
#else
|
||||
#define BA_DEBUG_PTR(TYPE) TYPE*
|
||||
#endif
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // BALLISTICA_CORE_OBJECT_H_
|
||||
630
src/ballistica/core/thread.cc
Normal file
630
src/ballistica/core/thread.cc
Normal file
@ -0,0 +1,630 @@
|
||||
// Copyright (c) 2011-2020 Eric Froemling
|
||||
|
||||
#include "ballistica/core/thread.h"
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "ballistica/app/app.h"
|
||||
#include "ballistica/core/fatal_error.h"
|
||||
#include "ballistica/core/module.h"
|
||||
#include "ballistica/platform/platform.h"
|
||||
#include "ballistica/python/python.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
bool Thread::threads_paused_ = false;
|
||||
|
||||
void Thread::AddCurrentThreadName(const std::string& name) {
|
||||
std::lock_guard<std::mutex> lock(g_app_globals->thread_name_map_mutex);
|
||||
std::thread::id thread_id = std::this_thread::get_id();
|
||||
auto i = g_app_globals->thread_name_map.find(thread_id);
|
||||
std::string s;
|
||||
if (i != g_app_globals->thread_name_map.end()) {
|
||||
s = i->second;
|
||||
}
|
||||
if (!strstr(s.c_str(), name.c_str())) {
|
||||
if (s.empty()) {
|
||||
s = name;
|
||||
} else {
|
||||
s = s + "+" + name;
|
||||
}
|
||||
}
|
||||
g_app_globals->thread_name_map[std::this_thread::get_id()] = s;
|
||||
}
|
||||
|
||||
void Thread::ClearCurrentThreadName() {
|
||||
std::lock_guard<std::mutex> lock(g_app_globals->thread_name_map_mutex);
|
||||
auto i = g_app_globals->thread_name_map.find(std::this_thread::get_id());
|
||||
if (i != g_app_globals->thread_name_map.end()) {
|
||||
g_app_globals->thread_name_map.erase(i);
|
||||
}
|
||||
}
|
||||
|
||||
void Thread::UpdateMainThreadID() {
|
||||
auto current_id = std::this_thread::get_id();
|
||||
|
||||
// This gets called a lot and it may happen before we are spun up,
|
||||
// so just ignore it in that case..
|
||||
if (g_app_globals) {
|
||||
g_app_globals->main_thread_id = current_id;
|
||||
}
|
||||
if (g_app) {
|
||||
g_app->thread()->set_thread_id(current_id);
|
||||
}
|
||||
}
|
||||
|
||||
void Thread::KillModule(const Module& module) {
|
||||
for (auto i = modules_.begin(); i != modules_.end(); i++) {
|
||||
if (*i == &module) {
|
||||
delete *i;
|
||||
modules_.erase(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw Exception("Module not found on this thread");
|
||||
}
|
||||
|
||||
void Thread::KillModules() {
|
||||
for (auto i : modules_) {
|
||||
delete i;
|
||||
}
|
||||
modules_.clear();
|
||||
}
|
||||
|
||||
// These are all exactly the same, but by running different ones for
|
||||
// different thread groups makes its easy to see which thread is which
|
||||
// in profilers, backtraces, etc.
|
||||
auto Thread::RunGameThread(void* data) -> int {
|
||||
return static_cast<Thread*>(data)->ThreadMain();
|
||||
}
|
||||
|
||||
auto Thread::RunAudioThread(void* data) -> int {
|
||||
return static_cast<Thread*>(data)->ThreadMain();
|
||||
}
|
||||
|
||||
auto Thread::RunBGDynamicThread(void* data) -> int {
|
||||
return static_cast<Thread*>(data)->ThreadMain();
|
||||
}
|
||||
|
||||
auto Thread::RunNetworkWriteThread(void* data) -> int {
|
||||
return static_cast<Thread*>(data)->ThreadMain();
|
||||
}
|
||||
|
||||
auto Thread::RunStdInputThread(void* data) -> int {
|
||||
return static_cast<Thread*>(data)->ThreadMain();
|
||||
}
|
||||
|
||||
auto Thread::RunMediaThread(void* data) -> int {
|
||||
return static_cast<Thread*>(data)->ThreadMain();
|
||||
}
|
||||
|
||||
void Thread::SetPaused(bool paused) {
|
||||
// Can be toggled from the main thread only.
|
||||
assert(std::this_thread::get_id() == g_app_globals->main_thread_id);
|
||||
PushThreadMessage(ThreadMessage(paused ? ThreadMessage::Type::kPause
|
||||
: ThreadMessage::Type::kResume));
|
||||
}
|
||||
|
||||
void Thread::WaitForNextEvent(bool single_cycle) {
|
||||
// If we're running a single cycle we never stop to wait.
|
||||
if (single_cycle) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We also never wait if any of our modules have pending runnables.
|
||||
// (we run all existing runnables in each loop cycle, but one of those
|
||||
// may have enqueued more).
|
||||
for (auto&& i : modules_) {
|
||||
if (i->has_pending_runnables()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// While we're waiting, allow other python threads to run.
|
||||
if (owns_python_) {
|
||||
g_python->ReleaseGIL();
|
||||
}
|
||||
|
||||
// If we've got active timers, wait for messages with a timeout so we can
|
||||
// run the next timer payload.
|
||||
if ((!paused_) && timers_.active_timer_count() > 0) {
|
||||
millisecs_t real_time = GetRealTime();
|
||||
millisecs_t wait_time = timers_.GetTimeToNextExpire(real_time);
|
||||
if (wait_time > 0) {
|
||||
std::unique_lock<std::mutex> lock(thread_message_mutex_);
|
||||
if (thread_message_count_ == 0) {
|
||||
thread_message_cv_.wait_for(lock, std::chrono::milliseconds(wait_time),
|
||||
[this] {
|
||||
// Go back to sleep on spurious wakeups
|
||||
// if we didn't wind up with any new
|
||||
// messages.
|
||||
return (thread_message_count_ > 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Not running timers; just wait indefinitely for the next message.
|
||||
std::unique_lock<std::mutex> lock(thread_message_mutex_);
|
||||
if (thread_message_count_ == 0) {
|
||||
thread_message_cv_.wait(lock, [this] {
|
||||
// Go back to sleep on spurious wakeups
|
||||
// (if we didn't wind up with any new messages).
|
||||
return (thread_message_count_ > 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (owns_python_) {
|
||||
g_python->AcquireGIL();
|
||||
}
|
||||
}
|
||||
|
||||
void Thread::LoopUpkeep(bool single_cycle) {
|
||||
// Keep our autorelease pool clean on mac/ios
|
||||
// FIXME: Should define a Platform::ThreadHelper or something
|
||||
// so we don't have platform-specific code here.
|
||||
#if BA_XCODE_BUILD
|
||||
// Let's not do autorelease pools when being called ad-hoc,
|
||||
// since in that case we're part of another run loop
|
||||
// (and its crashing on drain for some reason)
|
||||
if (!single_cycle) {
|
||||
if (auto_release_pool_) {
|
||||
g_platform->DrainAutoReleasePool(auto_release_pool_);
|
||||
auto_release_pool_ = nullptr;
|
||||
}
|
||||
auto_release_pool_ = g_platform->NewAutoReleasePool();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
auto Thread::RunEventLoop(bool single_cycle) -> int {
|
||||
while (true) {
|
||||
LoopUpkeep(single_cycle);
|
||||
|
||||
WaitForNextEvent(single_cycle);
|
||||
|
||||
// Process all queued thread messages.
|
||||
std::list<ThreadMessage> thread_messages;
|
||||
GetThreadMessages(&thread_messages);
|
||||
for (auto& thread_message : thread_messages) {
|
||||
switch (thread_message.type) {
|
||||
case ThreadMessage::Type::kNewModule: {
|
||||
// Launch a new module and unlock.
|
||||
ModuleLauncher* tl;
|
||||
tl = static_cast<ModuleLauncher*>(thread_message.pval);
|
||||
tl->Launch(this);
|
||||
auto cmd =
|
||||
static_cast<uint32_t>(ThreadMessage::Type::kNewModuleConfirm);
|
||||
WriteToOwner(&cmd, sizeof(cmd));
|
||||
break;
|
||||
}
|
||||
case ThreadMessage::Type::kRunnable: {
|
||||
auto module_id = thread_message.ival;
|
||||
Module* t = GetModule(module_id);
|
||||
assert(t);
|
||||
auto e = static_cast<Runnable*>(thread_message.pval);
|
||||
|
||||
// Add the event to our list.
|
||||
t->PushLocalRunnable(e);
|
||||
RunnablesWhilePausedSanityCheck(e);
|
||||
|
||||
break;
|
||||
}
|
||||
case ThreadMessage::Type::kShutdown: {
|
||||
// Shutdown; die!
|
||||
done_ = true;
|
||||
break;
|
||||
}
|
||||
case ThreadMessage::Type::kResume: {
|
||||
assert(paused_);
|
||||
|
||||
// Let all modules do pause-related stuff.
|
||||
for (auto&& i : modules_) {
|
||||
i->HandleThreadResume();
|
||||
}
|
||||
paused_ = false;
|
||||
break;
|
||||
}
|
||||
case ThreadMessage::Type::kPause: {
|
||||
assert(!paused_);
|
||||
|
||||
// Let all modules do pause-related stuff.
|
||||
for (auto&& i : modules_) {
|
||||
i->HandleThreadPause();
|
||||
}
|
||||
paused_ = true;
|
||||
last_pause_time_ = GetRealTime();
|
||||
messages_since_paused_ = 0;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw Exception();
|
||||
}
|
||||
}
|
||||
|
||||
// If the thread is going down.
|
||||
if (done_) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Run timers && queued module runnables unless we're paused.
|
||||
if (!paused_) {
|
||||
// Run timers.
|
||||
timers_.Run(GetRealTime());
|
||||
|
||||
// Run module-messages.
|
||||
for (auto& module_entry : modules_) {
|
||||
module_entry->RunPendingRunnables();
|
||||
}
|
||||
}
|
||||
if (done_ || single_cycle) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Thread::RunnablesWhilePausedSanityCheck(Runnable* e) {
|
||||
// We generally shouldn't be getting messages while paused..
|
||||
// (check both our pause-state and the global one; wanna ignore things
|
||||
// that might slip through if some just-unlocked thread msgs us but we
|
||||
// haven't been unlocked yet)
|
||||
|
||||
// UPDATE - we are migrating away from distinct message classes and towards
|
||||
// LambdaRunnables for everything, which means that we can't easily
|
||||
// see details of what is coming through. Disabling this check for now.
|
||||
}
|
||||
|
||||
void Thread::GetThreadMessages(std::list<ThreadMessage>* messages) {
|
||||
assert(messages);
|
||||
assert(std::this_thread::get_id() == thread_id());
|
||||
|
||||
// Make sure they passed an empty one in.
|
||||
assert(messages->empty());
|
||||
if (thread_message_count_ > 0) {
|
||||
std::unique_lock<std::mutex> lock(thread_message_mutex_);
|
||||
assert(thread_messages_.size() == thread_message_count_);
|
||||
messages->swap(thread_messages_);
|
||||
thread_message_count_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Thread::WriteToOwner(const void* data, uint32_t size) {
|
||||
assert(std::this_thread::get_id() == thread_id());
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(data_to_client_mutex_);
|
||||
data_to_client_.emplace_back(size);
|
||||
memcpy(&(data_to_client_.back()[0]), data, size);
|
||||
}
|
||||
data_to_client_cv_.notify_all();
|
||||
}
|
||||
|
||||
Thread::Thread(ThreadIdentifier identifier_in, ThreadType type_in)
|
||||
: type_(type_in), identifier_(identifier_in) {
|
||||
switch (type_) {
|
||||
case ThreadType::kStandard: {
|
||||
// Lock down until the thread is up and running. It'll unlock us when
|
||||
// it's ready to go.
|
||||
int (*func)(void*);
|
||||
switch (identifier_) {
|
||||
case ThreadIdentifier::kGame:
|
||||
func = RunGameThread;
|
||||
break;
|
||||
case ThreadIdentifier::kMedia:
|
||||
func = RunMediaThread;
|
||||
break;
|
||||
case ThreadIdentifier::kMain:
|
||||
// Shouldn't happen; this thread gets wrapped; not launched.
|
||||
throw Exception();
|
||||
case ThreadIdentifier::kAudio:
|
||||
func = RunAudioThread;
|
||||
break;
|
||||
case ThreadIdentifier::kBGDynamics:
|
||||
func = RunBGDynamicThread;
|
||||
break;
|
||||
case ThreadIdentifier::kNetworkWrite:
|
||||
func = RunNetworkWriteThread;
|
||||
break;
|
||||
case ThreadIdentifier::kStdin:
|
||||
func = RunStdInputThread;
|
||||
break;
|
||||
default:
|
||||
throw Exception();
|
||||
}
|
||||
|
||||
// Let 'er rip.
|
||||
thread_ = new std::thread(func, this);
|
||||
|
||||
// The thread lets us know when its up and running.
|
||||
std::unique_lock<std::mutex> lock(data_to_client_mutex_);
|
||||
|
||||
uint32_t cmd;
|
||||
ReadFromThread(&lock, &cmd, sizeof(cmd));
|
||||
assert(static_cast<ThreadMessage::Type>(cmd)
|
||||
== ThreadMessage::Type::kNewThreadConfirm);
|
||||
break;
|
||||
}
|
||||
case ThreadType::kMain: {
|
||||
// We've got no thread of our own to launch
|
||||
// so we run our setup stuff right here instead of off in some.
|
||||
assert(std::this_thread::get_id() == g_app_globals->main_thread_id);
|
||||
thread_id_ = std::this_thread::get_id();
|
||||
|
||||
// Hmmm we might want to set our thread name here,
|
||||
// as we do for other threads?
|
||||
// However on linux that winds up being what we see in top/etc
|
||||
// so maybe shouldn't.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Thread::ThreadMain() -> int {
|
||||
try {
|
||||
assert(type_ == ThreadType::kStandard);
|
||||
thread_id_ = std::this_thread::get_id();
|
||||
|
||||
const char* id_string;
|
||||
switch (identifier_) {
|
||||
case ThreadIdentifier::kGame:
|
||||
id_string = "ballistica game";
|
||||
break;
|
||||
case ThreadIdentifier::kStdin:
|
||||
id_string = "ballistica stdin";
|
||||
break;
|
||||
case ThreadIdentifier::kMedia:
|
||||
id_string = "ballistica media";
|
||||
break;
|
||||
case ThreadIdentifier::kFileOut:
|
||||
id_string = "ballistica file-out";
|
||||
break;
|
||||
case ThreadIdentifier::kMain:
|
||||
id_string = "ballistica main";
|
||||
break;
|
||||
case ThreadIdentifier::kAudio:
|
||||
id_string = "ballistica audio";
|
||||
break;
|
||||
case ThreadIdentifier::kBGDynamics:
|
||||
id_string = "ballistica bg-dynamics";
|
||||
break;
|
||||
case ThreadIdentifier::kNetworkWrite:
|
||||
id_string = "ballistica network writing";
|
||||
break;
|
||||
default:
|
||||
throw Exception();
|
||||
}
|
||||
g_platform->SetCurrentThreadName(id_string);
|
||||
|
||||
// Send our owner a confirmation that we're alive.
|
||||
auto cmd = static_cast<uint32_t>(ThreadMessage::Type::kNewThreadConfirm);
|
||||
WriteToOwner(&cmd, sizeof(cmd));
|
||||
|
||||
// Now just run our loop until we die.
|
||||
int result = RunEventLoop();
|
||||
|
||||
KillModules();
|
||||
ClearCurrentThreadName();
|
||||
return result;
|
||||
} catch (const std::exception& e) {
|
||||
auto error_msg = std::string("Unhandled exception in ")
|
||||
+ GetCurrentThreadName() + " thread:\n" + e.what();
|
||||
|
||||
FatalError::ReportFatalError(error_msg, true);
|
||||
bool exit_cleanly = !IsUnmodifiedBlessedBuild();
|
||||
bool handled = FatalError::HandleFatalError(exit_cleanly, true);
|
||||
|
||||
// Do the default thing if platform didn't handle it.
|
||||
if (!handled) {
|
||||
if (exit_cleanly) {
|
||||
exit(1);
|
||||
} else {
|
||||
throw;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Thread::SetOwnsPython() {
|
||||
owns_python_ = true;
|
||||
g_python->AcquireGIL();
|
||||
}
|
||||
|
||||
// Explicitly kill the main thread.
|
||||
void Thread::Quit() {
|
||||
assert(type_ == ThreadType::kMain);
|
||||
if (type_ == ThreadType::kMain) {
|
||||
done_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
Thread::~Thread() = default;
|
||||
|
||||
void Thread::LogThreadMessageTally() {
|
||||
// Prevent recursion.
|
||||
if (!writing_tally_) {
|
||||
writing_tally_ = true;
|
||||
|
||||
std::map<std::string, int> tally;
|
||||
Log("Thread message tally (" + std::to_string(thread_messages_.size())
|
||||
+ " in list):");
|
||||
for (auto&& m : thread_messages_) {
|
||||
std::string s;
|
||||
switch (m.type) {
|
||||
case ThreadMessage::Type::kShutdown:
|
||||
s += "kShutdown";
|
||||
break;
|
||||
case ThreadMessage::Type::kRunnable:
|
||||
s += "kRunnable";
|
||||
break;
|
||||
case ThreadMessage::Type::kNewModule:
|
||||
s += "kNewModule";
|
||||
break;
|
||||
case ThreadMessage::Type::kNewModuleConfirm:
|
||||
s += "kNewModuleConfirm";
|
||||
break;
|
||||
case ThreadMessage::Type::kNewThreadConfirm:
|
||||
s += "kNewThreadConfirm";
|
||||
break;
|
||||
case ThreadMessage::Type::kPause:
|
||||
s += "kPause";
|
||||
break;
|
||||
case ThreadMessage::Type::kResume:
|
||||
s += "kResume";
|
||||
break;
|
||||
default:
|
||||
s += "UNKNOWN(" + std::to_string(static_cast<int>(m.type)) + ")";
|
||||
break;
|
||||
}
|
||||
if (m.type == ThreadMessage::Type::kRunnable) {
|
||||
// Runnable* e;
|
||||
// e = static_cast<Runnable*>(m.pval);
|
||||
{
|
||||
std::string m_name = g_platform->DemangleCXXSymbol(
|
||||
typeid(*(static_cast<Runnable*>(m.pval))).name());
|
||||
s += std::string(": ") + m_name;
|
||||
}
|
||||
}
|
||||
auto j = tally.find(s);
|
||||
if (j == tally.end()) {
|
||||
tally[s] = 1;
|
||||
} else {
|
||||
tally[s]++;
|
||||
}
|
||||
}
|
||||
int entry = 1;
|
||||
for (auto&& i : tally) {
|
||||
Log(" #" + std::to_string(entry++) + " (" + std::to_string(i.second)
|
||||
+ "x): " + i.first);
|
||||
}
|
||||
writing_tally_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Thread::PushThreadMessage(const ThreadMessage& t) {
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(thread_message_mutex_);
|
||||
|
||||
// Plop the data on to the list; we're assuming the mutex is locked.
|
||||
thread_messages_.push_back(t);
|
||||
|
||||
// Keep our own count; apparently size() on an stl list involves
|
||||
// iterating.
|
||||
// FIXME: Actually I don't think this is the case anymore; should check.
|
||||
thread_message_count_++;
|
||||
assert(thread_message_count_ == thread_messages_.size());
|
||||
|
||||
// Show message count states.
|
||||
if (explicit_bool(false)) {
|
||||
static int one_off = 0;
|
||||
static int foo = 0;
|
||||
foo++;
|
||||
one_off++;
|
||||
|
||||
// Show momemtary spikes.
|
||||
if (thread_message_count_ > 100 && one_off > 100) {
|
||||
one_off = 0;
|
||||
foo = 999;
|
||||
}
|
||||
|
||||
// Show count periodically.
|
||||
if ((std::this_thread::get_id() == g_app_globals->main_thread_id)
|
||||
&& foo > 100) {
|
||||
foo = 0;
|
||||
Log("MSG COUNT " + std::to_string(thread_message_count_));
|
||||
}
|
||||
}
|
||||
|
||||
if (thread_message_count_ > 1000) {
|
||||
static bool sent_error = false;
|
||||
if (!sent_error) {
|
||||
sent_error = true;
|
||||
Log("Error: ThreadMessage list > 1000 in thread: "
|
||||
+ GetCurrentThreadName());
|
||||
LogThreadMessageTally();
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent runaway mem usage if the list gets out of control.
|
||||
if (thread_message_count_ > 10000) {
|
||||
throw Exception("KILLING APP: ThreadMessage list > 10000 in thread: "
|
||||
+ GetCurrentThreadName());
|
||||
}
|
||||
|
||||
// Unlock thread-message list and inform thread that there's something
|
||||
// available.
|
||||
}
|
||||
thread_message_cv_.notify_all();
|
||||
}
|
||||
|
||||
void Thread::ReadFromThread(std::unique_lock<std::mutex>* lock, void* buffer,
|
||||
uint32_t size) {
|
||||
// Threads cant read from themselves.. could load to lock-deadlock.
|
||||
assert(std::this_thread::get_id() != thread_id());
|
||||
data_to_client_cv_.wait(*lock, [this] {
|
||||
// Go back to sleep on spurious wakeups
|
||||
// (if we didn't wind up with any new messages)
|
||||
return (!data_to_client_.empty());
|
||||
});
|
||||
|
||||
// Read the oldest thing on our in-data list.
|
||||
assert(!data_to_client_.empty());
|
||||
assert(data_to_client_.front().size() == size);
|
||||
memcpy(buffer, &(data_to_client_.front()[0]), size);
|
||||
data_to_client_.pop_front();
|
||||
}
|
||||
|
||||
void Thread::SetThreadsPaused(bool paused) {
|
||||
threads_paused_ = paused;
|
||||
for (auto&& i : g_app_globals->pausable_threads) {
|
||||
i->SetPaused(paused);
|
||||
}
|
||||
}
|
||||
|
||||
auto Thread::AreThreadsPaused() -> bool { return threads_paused_; }
|
||||
|
||||
auto Thread::RegisterModule(const std::string& name, Module* module) -> int {
|
||||
AddCurrentThreadName(name);
|
||||
// This should assure we were properly launched.
|
||||
// (the ModuleLauncher will set the index to what ours will be)
|
||||
int index = static_cast<int>(modules_.size());
|
||||
// module_entries_.emplace_back(module, name, index);
|
||||
modules_.push_back(module);
|
||||
return index;
|
||||
}
|
||||
|
||||
auto Thread::NewTimer(millisecs_t length, bool repeat,
|
||||
const Object::Ref<Runnable>& runnable) -> Timer* {
|
||||
assert(IsCurrent());
|
||||
assert(runnable.exists());
|
||||
return timers_.NewTimer(GetRealTime(), length, 0, repeat ? -1 : 0, runnable);
|
||||
}
|
||||
|
||||
auto Thread::GetCurrentThreadName() -> std::string {
|
||||
if (g_app_globals == nullptr) {
|
||||
return "unknown(not-yet-inited)";
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_app_globals->thread_name_map_mutex);
|
||||
auto i = g_app_globals->thread_name_map.find(std::this_thread::get_id());
|
||||
if (i != g_app_globals->thread_name_map.end()) {
|
||||
return i->second;
|
||||
}
|
||||
}
|
||||
#if BA_OSTYPE_MACOS || BA_OSTYPE_IOS_TVOS || BA_OSTYPE_LINUX
|
||||
std::string name = "unknown (sys-name=";
|
||||
char buffer[256];
|
||||
int result = pthread_getname_np(pthread_self(), buffer, sizeof(buffer));
|
||||
if (result == 0) {
|
||||
name += std::string("\"") + buffer + "\")";
|
||||
} else {
|
||||
name += "<error " + std::to_string(result) + ">";
|
||||
}
|
||||
return name;
|
||||
#else
|
||||
return "unknown";
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace ballistica
|
||||
249
src/ballistica/core/thread.h
Normal file
249
src/ballistica/core/thread.h
Normal file
@ -0,0 +1,249 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_CORE_THREAD_H_
|
||||
#define BALLISTICA_CORE_THREAD_H_
|
||||
|
||||
#include <condition_variable>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ballistica/app/app_globals.h"
|
||||
#include "ballistica/ballistica.h"
|
||||
#include "ballistica/generic/timer_list.h"
|
||||
#include "ballistica/platform/min_sdl.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
// A thread with a built-in event loop.
|
||||
class Thread {
|
||||
public:
|
||||
explicit Thread(ThreadIdentifier id,
|
||||
ThreadType type_in = ThreadType::kStandard);
|
||||
virtual ~Thread();
|
||||
|
||||
/// Register a name for the current thread (should generally describe its
|
||||
/// purpose). If called multiple times, names will be combined with a '+'. ie:
|
||||
/// "graphics+animation+audio".
|
||||
void AddCurrentThreadName(const std::string& name);
|
||||
void ClearCurrentThreadName();
|
||||
|
||||
static auto GetCurrentThreadName() -> std::string;
|
||||
|
||||
/// Call this if the main thread changes.
|
||||
static void UpdateMainThreadID();
|
||||
|
||||
static void SetThreadsPaused(bool enable);
|
||||
static auto AreThreadsPaused() -> bool;
|
||||
|
||||
auto IsCurrent() const -> bool {
|
||||
return std::this_thread::get_id() == thread_id();
|
||||
}
|
||||
|
||||
// Used to quit the main thread.
|
||||
void Quit();
|
||||
|
||||
struct ModuleLauncher {
|
||||
virtual void Launch(Thread* g) = 0;
|
||||
virtual ~ModuleLauncher() = default;
|
||||
};
|
||||
|
||||
template <class MODULETYPE>
|
||||
struct ModuleLauncherTemplate : public ModuleLauncher {
|
||||
void Launch(Thread* g) override { new MODULETYPE(g); }
|
||||
};
|
||||
|
||||
template <class MODULETYPE, class ARGTYPE>
|
||||
struct ModuleLauncherArgTemplate : public ModuleLauncher {
|
||||
explicit ModuleLauncherArgTemplate(ARGTYPE arg_in) : arg(arg_in) {}
|
||||
ARGTYPE arg;
|
||||
void Launch(Thread* g) override { new MODULETYPE(g, arg); }
|
||||
};
|
||||
|
||||
void SetOwnsPython();
|
||||
|
||||
// Add a new module to a thread. This doesn't return anything. If you need
|
||||
// a pointer to the module, have it store itself somewhere in its constructor
|
||||
// or whatnot. Returning a pointer made it too easy to introduce race
|
||||
// conditions with the thread trying to access itself via this pointer
|
||||
// before it was set up.
|
||||
template <class THREADTYPE>
|
||||
void AddModule() {
|
||||
switch (type_) {
|
||||
case ThreadType::kStandard: {
|
||||
// Launching a module in the current thread: do it immediately.
|
||||
if (IsCurrent()) {
|
||||
ModuleLauncherTemplate<THREADTYPE> launcher;
|
||||
launcher.Launch(this);
|
||||
} else {
|
||||
// Launching a module in another thread;
|
||||
// send a module-launcher and wait for the confirmation.
|
||||
ModuleLauncherTemplate<THREADTYPE> launcher;
|
||||
ModuleLauncher* tl = &launcher;
|
||||
PushThreadMessage(
|
||||
ThreadMessage(ThreadMessage::Type::kNewModule, 0, tl));
|
||||
std::unique_lock<std::mutex> lock(data_to_client_mutex_);
|
||||
uint32_t cmd;
|
||||
ReadFromThread(&lock, &cmd, sizeof(cmd));
|
||||
assert(static_cast<ThreadMessage::Type>(cmd)
|
||||
== ThreadMessage::Type::kNewModuleConfirm);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ThreadType::kMain: {
|
||||
assert(std::this_thread::get_id() == g_app_globals->main_thread_id);
|
||||
new THREADTYPE(this);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw Exception();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An alternate version of AddModule that passes an argument along
|
||||
// to the thread's constructor.
|
||||
template <class THREADTYPE, class ARGTYPE>
|
||||
void AddModule(ARGTYPE arg) {
|
||||
switch (type_) {
|
||||
case ThreadType::kStandard: {
|
||||
// Launching a module in the current thread: do it immediately.
|
||||
if (IsCurrent()) {
|
||||
ModuleLauncherArgTemplate<THREADTYPE, ARGTYPE> launcher(arg);
|
||||
launcher.Launch(this);
|
||||
} else {
|
||||
// Launching a module in another thread;
|
||||
// send a module-launcher and wait for the confirmation.
|
||||
ModuleLauncherArgTemplate<THREADTYPE, ARGTYPE> launcher(arg);
|
||||
ModuleLauncher* tl = &launcher;
|
||||
PushThreadMessage(
|
||||
ThreadMessage(ThreadMessage::Type::kNewModule, 0, tl));
|
||||
|
||||
std::unique_lock<std::mutex> lock(data_to_client_mutex_);
|
||||
|
||||
uint32_t cmd;
|
||||
|
||||
ReadFromThread(&lock, &cmd, sizeof(cmd));
|
||||
|
||||
assert(static_cast<ThreadMessage::Type>(cmd)
|
||||
== ThreadMessage::Type::kNewModuleConfirm);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ThreadType::kMain: {
|
||||
assert(std::this_thread::get_id() == g_app_globals->main_thread_id);
|
||||
new THREADTYPE(this, arg);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw Exception();
|
||||
}
|
||||
}
|
||||
}
|
||||
void KillModule(const Module& module);
|
||||
|
||||
void SetPaused(bool paused);
|
||||
auto thread_id() const -> std::thread::id { return thread_id_; }
|
||||
|
||||
// Needed in rare cases where we jump physical threads.
|
||||
// (Our 'main' thread on Android can switch under us as
|
||||
// rendering contexts are recreated in new threads/etc.)
|
||||
void set_thread_id(std::thread::id id) { thread_id_ = id; }
|
||||
|
||||
auto RunEventLoop(bool single_cycle = false) -> int;
|
||||
auto identifier() const -> ThreadIdentifier { return identifier_; }
|
||||
|
||||
// For use by modules.
|
||||
auto RegisterModule(const std::string& name, Module* module) -> int;
|
||||
void PushModuleRunnable(Runnable* runnable, int module_index) {
|
||||
PushThreadMessage(Thread::ThreadMessage(
|
||||
Thread::ThreadMessage::Type::kRunnable, module_index, runnable));
|
||||
}
|
||||
|
||||
// Register a timer to run on the thread.
|
||||
auto NewTimer(millisecs_t length, bool repeat,
|
||||
const Object::Ref<Runnable>& runnable) -> Timer*;
|
||||
|
||||
private:
|
||||
struct ThreadMessage {
|
||||
enum class Type {
|
||||
kShutdown = 999,
|
||||
kRunnable,
|
||||
kNewModule,
|
||||
kNewModuleConfirm,
|
||||
kNewThreadConfirm,
|
||||
kPause,
|
||||
kResume
|
||||
};
|
||||
Type type;
|
||||
void* pval;
|
||||
int ival;
|
||||
explicit ThreadMessage(Type type_in, int ival_in = 0,
|
||||
void* pval_in = nullptr)
|
||||
: type(type_in), ival(ival_in), pval(pval_in) {}
|
||||
};
|
||||
static void RunnablesWhilePausedSanityCheck(Runnable* r);
|
||||
void WaitForNextEvent(bool single_cycle);
|
||||
void LoopUpkeep(bool once);
|
||||
void LogThreadMessageTally();
|
||||
void ReadFromThread(std::unique_lock<std::mutex>* lock, void* buffer,
|
||||
uint32_t size);
|
||||
|
||||
void WriteToOwner(const void* data, uint32_t size);
|
||||
bool writing_tally_ = false;
|
||||
bool paused_ = false;
|
||||
millisecs_t last_pause_time_ = 0;
|
||||
int messages_since_paused_ = 0;
|
||||
millisecs_t last_paused_message_report_time_ = 0;
|
||||
bool done_ = false;
|
||||
ThreadType type_;
|
||||
int listen_sd_ = 0;
|
||||
std::thread::id thread_id_{};
|
||||
ThreadIdentifier identifier_ = ThreadIdentifier::kInvalid;
|
||||
millisecs_t last_complaint_time_ = 0;
|
||||
bool owns_python_ = false;
|
||||
|
||||
// FIXME: Should generalize this to some sort of PlatformThreadData class.
|
||||
#if BA_XCODE_BUILD
|
||||
void* auto_release_pool_ = nullptr;
|
||||
#endif
|
||||
|
||||
void KillModules();
|
||||
|
||||
// These are all exactly the same, but by running different ones for
|
||||
// different thread groups makes its easy to see which thread is which
|
||||
// in profilers, backtraces, etc.
|
||||
static auto RunGameThread(void* data) -> int;
|
||||
static auto RunAudioThread(void* data) -> int;
|
||||
static auto RunBGDynamicThread(void* data) -> int;
|
||||
static auto RunNetworkWriteThread(void* data) -> int;
|
||||
static auto RunStdInputThread(void* data) -> int;
|
||||
static auto RunMediaThread(void* data) -> int;
|
||||
|
||||
auto ThreadMain() -> int;
|
||||
std::thread* thread_;
|
||||
void GetThreadMessages(std::list<ThreadMessage>* messages);
|
||||
void PushThreadMessage(const ThreadMessage& t);
|
||||
std::condition_variable thread_message_cv_;
|
||||
std::mutex thread_message_mutex_;
|
||||
std::list<ThreadMessage> thread_messages_;
|
||||
int thread_message_count_ = 0;
|
||||
std::condition_variable data_to_client_cv_;
|
||||
std::mutex data_to_client_mutex_;
|
||||
std::list<std::vector<char> > data_to_client_;
|
||||
std::vector<Module*> modules_;
|
||||
auto GetModule(int id) -> Module* {
|
||||
assert(id >= 0 && id < static_cast<int>(modules_.size()));
|
||||
return modules_[id];
|
||||
}
|
||||
|
||||
// Complete list of all timers created by this group's modules.
|
||||
TimerList timers_;
|
||||
static bool threads_paused_;
|
||||
};
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // BALLISTICA_CORE_THREAD_H_
|
||||
1068
src/ballistica/core/types.h
Normal file
1068
src/ballistica/core/types.h
Normal file
File diff suppressed because it is too large
Load Diff
161
src/ballistica/generic/base64.cc
Normal file
161
src/ballistica/generic/base64.cc
Normal file
@ -0,0 +1,161 @@
|
||||
// Copyright (c) 2011-2020 Eric Froemling
|
||||
// Derived from code licensed as follows:
|
||||
|
||||
/*
|
||||
base64.cpp and base64.h
|
||||
|
||||
Copyright (C) 2004-2008 René Nyffenegger
|
||||
|
||||
This source code is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the author be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this source code must not be misrepresented; you must not
|
||||
claim that you wrote the original source code. If you use this source code
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original source code.
|
||||
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
René Nyffenegger rene.nyffenegger@adp-gmbh.ch
|
||||
|
||||
*/
|
||||
#include "ballistica/generic/base64.h"
|
||||
|
||||
#include <cctype>
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
// NOLINTNEXTLINE(cert-err58-cpp)
|
||||
static const std::string* base64_chars_non_urlsafe = new std::string(
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"0123456789+/");
|
||||
|
||||
// NOLINTNEXTLINE(cert-err58-cpp)
|
||||
static const std::string* base64_chars_urlsafe = new std::string(
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"0123456789+/");
|
||||
|
||||
static inline auto is_base64(unsigned char c, bool urlsafe) -> bool {
|
||||
if (urlsafe) {
|
||||
return (isalnum(c) || (c == '-') || (c == '_'));
|
||||
} else {
|
||||
return (isalnum(c) || (c == '+') || (c == '/'));
|
||||
}
|
||||
}
|
||||
|
||||
auto base64_encode(const unsigned char* bytes_to_encode, unsigned int in_len,
|
||||
bool urlsafe) -> std::string {
|
||||
std::string ret;
|
||||
int i = 0;
|
||||
// int j = 0;
|
||||
unsigned char char_array_3[3];
|
||||
unsigned char char_array_4[4];
|
||||
|
||||
static const std::string& base64_chars =
|
||||
urlsafe ? *base64_chars_urlsafe : *base64_chars_non_urlsafe;
|
||||
|
||||
while (in_len--) {
|
||||
char_array_3[i++] = *(bytes_to_encode++);
|
||||
if (i == 3) {
|
||||
char_array_4[0] =
|
||||
static_cast<unsigned char>((char_array_3[0] & 0xfcu) >> 2u);
|
||||
char_array_4[1] =
|
||||
static_cast<unsigned char>(((char_array_3[0] & 0x03u) << 4u)
|
||||
+ ((char_array_3[1] & 0xf0u) >> 4u));
|
||||
char_array_4[2] =
|
||||
static_cast<unsigned char>(((char_array_3[1] & 0x0fu) << 2u)
|
||||
+ ((char_array_3[2] & 0xc0u) >> 6u));
|
||||
char_array_4[3] = static_cast<unsigned char>(char_array_3[2] & 0x3fu);
|
||||
|
||||
for (i = 0; (i < 4); i++) ret += base64_chars[char_array_4[i]];
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (i) {
|
||||
for (int j = i; j < 3; j++) {
|
||||
char_array_3[j] = '\0';
|
||||
}
|
||||
char_array_4[0] =
|
||||
static_cast<unsigned char>((char_array_3[0] & 0xfcu) >> 2u);
|
||||
char_array_4[1] = static_cast<unsigned char>(
|
||||
((char_array_3[0] & 0x03u) << 4u) + ((char_array_3[1] & 0xf0u) >> 4u));
|
||||
char_array_4[2] = static_cast<unsigned char>(
|
||||
((char_array_3[1] & 0x0fu) << 2u) + ((char_array_3[2] & 0xc0u) >> 6u));
|
||||
char_array_4[3] = static_cast<unsigned char>(char_array_3[2u] & 0x3fu);
|
||||
for (int j = 0; (j < i + 1); j++) {
|
||||
ret += base64_chars[char_array_4[j]];
|
||||
}
|
||||
while ((i++ < 3)) {
|
||||
ret += '=';
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
auto base64_decode(const std::string& encoded_string, bool urlsafe)
|
||||
-> std::string {
|
||||
int in_len = static_cast<int>(encoded_string.size());
|
||||
int i = 0;
|
||||
// int j = 0;
|
||||
int in_ = 0;
|
||||
unsigned char char_array_4[4], char_array_3[3];
|
||||
std::string ret;
|
||||
|
||||
static const std::string& base64_chars =
|
||||
urlsafe ? *base64_chars_urlsafe : *base64_chars_non_urlsafe;
|
||||
|
||||
while (in_len-- && (encoded_string[in_] != '=')
|
||||
&& is_base64((unsigned char)encoded_string[in_], urlsafe)) {
|
||||
char_array_4[i++] = (unsigned char)encoded_string[in_];
|
||||
in_++;
|
||||
if (i == 4) {
|
||||
for (i = 0; i < 4; i++) {
|
||||
char_array_4[i] =
|
||||
static_cast<unsigned char>(base64_chars.find(char_array_4[i]));
|
||||
}
|
||||
|
||||
char_array_3[0] = static_cast<unsigned char>(
|
||||
(char_array_4[0] << 2u) + ((char_array_4[1] & 0x30u) >> 4u));
|
||||
char_array_3[1] = static_cast<unsigned char>(
|
||||
((char_array_4[1] & 0xfu) << 4u) + ((char_array_4[2] & 0x3cu) >> 2u));
|
||||
char_array_3[2] = static_cast<unsigned char>(
|
||||
((char_array_4[2] & 0x3u) << 6u) + char_array_4[3]);
|
||||
|
||||
for (i = 0; (i < 3); i++) ret += char_array_3[i];
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
if (i) {
|
||||
for (int j = i; j < 4; j++) {
|
||||
char_array_4[j] = 0;
|
||||
}
|
||||
for (int j = 0; j < 4; j++) { // NOLINT(modernize-loop-convert)
|
||||
char_array_4[j] =
|
||||
static_cast<unsigned char>(base64_chars.find(char_array_4[j]));
|
||||
}
|
||||
char_array_3[0] = static_cast<unsigned char>(
|
||||
(char_array_4[0] << 2u) + ((char_array_4[1] & 0x30u) >> 4u));
|
||||
char_array_3[1] = static_cast<unsigned char>(
|
||||
((char_array_4[1] & 0xfu) << 4u) + ((char_array_4[2] & 0x3cu) >> 2u));
|
||||
char_array_3[2] = static_cast<unsigned char>(
|
||||
((char_array_4[2] & 0x3u) << 6u) + char_array_4[3]);
|
||||
for (int j = 0; (j < i - 1); j++) {
|
||||
ret += char_array_3[j];
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace ballistica
|
||||
16
src/ballistica/generic/base64.h
Normal file
16
src/ballistica/generic/base64.h
Normal file
@ -0,0 +1,16 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_GENERIC_BASE64_H_
|
||||
#define BALLISTICA_GENERIC_BASE64_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
auto base64_encode(const unsigned char*, unsigned int len, bool urlsafe = false)
|
||||
-> std::string;
|
||||
auto base64_decode(const std::string& s, bool urlsafe = false) -> std::string;
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // BALLISTICA_GENERIC_BASE64_H_
|
||||
95
src/ballistica/generic/buffer.h
Normal file
95
src/ballistica/generic/buffer.h
Normal file
@ -0,0 +1,95 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_GENERIC_BUFFER_H_
|
||||
#define BALLISTICA_GENERIC_BUFFER_H_
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include "ballistica/generic/utils.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
// Simple data-holding buffer class.
|
||||
// (FIXME: should kill this and just use std::vector for this purpose)
|
||||
template <class T>
|
||||
class Buffer {
|
||||
public:
|
||||
Buffer(const Buffer& b) : data_(nullptr), size_(0) {
|
||||
Resize(b.size());
|
||||
if (b.size() > 0) {
|
||||
memcpy(data_, b.data_, b.size() * sizeof(T));
|
||||
}
|
||||
}
|
||||
|
||||
~Buffer() {
|
||||
if (data_) {
|
||||
free(data_);
|
||||
}
|
||||
}
|
||||
|
||||
auto operator=(const Buffer& src) -> Buffer& {
|
||||
assert(this != &src); // Shouldn't be self-assigning.
|
||||
Resize(src.size());
|
||||
if (size_ > 0) {
|
||||
memcpy(data_, src.data_, size_ * sizeof(T));
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
explicit Buffer(size_t size_in = 0) : data_(nullptr), size_(size_in) {
|
||||
if (size_ > 0) {
|
||||
Resize(size_);
|
||||
}
|
||||
}
|
||||
|
||||
Buffer(const T* data_in, size_t length) : data_(nullptr), size_(0) {
|
||||
if (length > 0) {
|
||||
Resize(length);
|
||||
memcpy(data_, data_in, length * sizeof(T));
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the amount of space needed to embed this buffer
|
||||
auto GetFlattenedSize() -> size_t { return 4 + size_ * sizeof(T); }
|
||||
|
||||
/// Embed this buffer into a flat memory buffer.
|
||||
void embed(char** b) {
|
||||
// Embed our size (in items not bytes).
|
||||
Utils::EmbedInt32NBO(b, static_cast<int32_t>(size_));
|
||||
memcpy(*b, data_, size_ * sizeof(T));
|
||||
*b += size_ * sizeof(T);
|
||||
}
|
||||
|
||||
/// Extract this buffer for a flat memory buffer.
|
||||
void Extract(const char** b) {
|
||||
Resize(static_cast_check_fit<size_t>(Utils::ExtractInt32NBO(b)));
|
||||
memcpy(data_, *b, size_ * sizeof(T));
|
||||
*b += size_ * sizeof(T);
|
||||
}
|
||||
|
||||
void Resize(size_t new_size) {
|
||||
if (data_) {
|
||||
free(data_);
|
||||
}
|
||||
if (new_size > 0) {
|
||||
data_ = static_cast<T*>(malloc(new_size * sizeof(T)));
|
||||
BA_PRECONDITION(data_);
|
||||
} else {
|
||||
data_ = nullptr;
|
||||
}
|
||||
size_ = new_size;
|
||||
}
|
||||
|
||||
// gets the length in the buffer's units (not bytes)
|
||||
auto size() const -> size_t { return size_; }
|
||||
|
||||
auto data() const -> T* { return data_; }
|
||||
|
||||
private:
|
||||
T* data_;
|
||||
size_t size_;
|
||||
};
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // BALLISTICA_GENERIC_BUFFER_H_
|
||||
590
src/ballistica/generic/huffman.cc
Normal file
590
src/ballistica/generic/huffman.cc
Normal file
@ -0,0 +1,590 @@
|
||||
// Copyright (c) 2011-2020 Eric Froemling
|
||||
|
||||
#include "ballistica/generic/huffman.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "ballistica/networking/networking.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
// Yes, I should clean this up to use unsigned vals, but it seems to work
|
||||
// fine for now so I don't want to touch it.
|
||||
#pragma clang diagnostic push
|
||||
#pragma ide diagnostic ignored "hicpp-signed-bitwise"
|
||||
|
||||
// how much data we read in training mode before spitting out results
|
||||
#if HUFFMAN_TRAINING_MODE
|
||||
const int kTrainingLength = 200000;
|
||||
#endif
|
||||
|
||||
// we currently just have a static table of char frequencies - this can be
|
||||
// generated by setting "training mode" on.
|
||||
static int g_freqs[] = {
|
||||
101342, 9667, 3497, 1072, 0, 3793, 0, 0, 2815, 5235, 0, 0, 0, 3570, 0, 0,
|
||||
0, 1383, 0, 0, 0, 2970, 0, 0, 2857, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1494,
|
||||
1974, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1351, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1475,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
|
||||
static void DoWriteBits(char** ptr, int* bit, int val, int val_bits) {
|
||||
int src_bit = 0;
|
||||
while (src_bit < val_bits) {
|
||||
**ptr |= ((val >> src_bit) & 0x01) << (*bit); // NOLINT
|
||||
if ((*bit) == 7) (*ptr)++;
|
||||
(*bit) = ((*bit) + 1) % 8;
|
||||
src_bit++;
|
||||
}
|
||||
}
|
||||
|
||||
Huffman::Huffman() : built(false) {
|
||||
static_assert(sizeof(g_freqs) == sizeof(int) * 256);
|
||||
build();
|
||||
}
|
||||
|
||||
Huffman::~Huffman() = default;
|
||||
|
||||
auto Huffman::compress(const std::vector<uint8_t>& src)
|
||||
-> std::vector<uint8_t> {
|
||||
#if BA_HUFFMAN_NET_COMPRESSION
|
||||
|
||||
auto length = static_cast<uint32_t>(src.size());
|
||||
const char* data = (const char*)src.data();
|
||||
|
||||
// IMPORTANT:
|
||||
// our uncompressed packets have a type byte at the beginning
|
||||
// (which should just be a few bits)
|
||||
// and the compressed ones have a remainder byte (4 bits of which are used)
|
||||
// ...so the first few bits should always be unused.
|
||||
// we hijack the highest bit to denote whether we're sending
|
||||
// a compressed or uncompressed packet (1 for compressed, 0 for uncompressed)
|
||||
BA_PRECONDITION(data[0] >> 7 == 0);
|
||||
|
||||
// see how many bits we'll need
|
||||
uint32_t bit_count = 0;
|
||||
for (uint32_t i = 0; i < length; i++) {
|
||||
bit_count += nodes_[static_cast<uint8_t>(data[i])].bits;
|
||||
}
|
||||
|
||||
// round up to next byte and add our one-byte header
|
||||
uint32_t length_out = bit_count / 8 + 1;
|
||||
if (bit_count % 8) {
|
||||
length_out++;
|
||||
}
|
||||
bit_count %= 8;
|
||||
|
||||
// if compressed is bigger than uncompressed, go with uncompressed - just
|
||||
// return the data they provided
|
||||
if ((length_out >= length)) {
|
||||
return src;
|
||||
} else {
|
||||
std::vector<uint8_t> out(length_out, 0);
|
||||
|
||||
// first byte gives our number of empty trailing bits
|
||||
char* ptr = reinterpret_cast<char*>(out.data());
|
||||
int bit = 0;
|
||||
|
||||
*ptr = static_cast<char>(8 - bit_count % 8);
|
||||
if (*ptr == 8) {
|
||||
*ptr = 0;
|
||||
}
|
||||
ptr++;
|
||||
|
||||
for (uint32_t i = 0; i < length; i++) {
|
||||
DoWriteBits(&ptr, &bit, nodes_[static_cast<uint8_t>(data[i])].val,
|
||||
nodes_[static_cast<uint8_t>(data[i])].bits);
|
||||
}
|
||||
// make sure we're either at the end of our allotted buffer or we're one
|
||||
// from the end and the bitcount takes care of the rest
|
||||
assert(ptr - reinterpret_cast<char*>(out.data()) == length_out
|
||||
|| (ptr - reinterpret_cast<char*>(out.data()) == length_out - 1
|
||||
&& bit_count != 0));
|
||||
assert(bit == bit_count % 8);
|
||||
|
||||
// mark it as compressed
|
||||
out[0] |= (0x01 << 7);
|
||||
return out;
|
||||
}
|
||||
#else
|
||||
|
||||
#if HUFFMAN_TRAINING_MODE
|
||||
train(data, length);
|
||||
#endif
|
||||
|
||||
data_out = data;
|
||||
length_out = length;
|
||||
#endif
|
||||
}
|
||||
|
||||
// hmmm - I saw a crash logged in this function; need to make sure this is
|
||||
// bulletproof since untrusted data is coming through here..
|
||||
auto Huffman::decompress(const std::vector<uint8_t>& src)
|
||||
-> std::vector<uint8_t> {
|
||||
#if BA_HUFFMAN_NET_COMPRESSION
|
||||
|
||||
auto length = static_cast<uint32_t>(src.size());
|
||||
BA_PRECONDITION(length > 0);
|
||||
|
||||
const char* data = (const char*)src.data();
|
||||
|
||||
auto remainder = static_cast<uint8_t>(*data & 0x0F);
|
||||
bool compressed = *data >> 7;
|
||||
|
||||
if (compressed) {
|
||||
std::vector<uint8_t> out;
|
||||
out.reserve(src.size() * 2); // hopefully minimize reallocations..
|
||||
|
||||
uint32_t bit_length = ((length - 1) * 8);
|
||||
if (remainder > bit_length) throw Exception("invalid huffman data");
|
||||
bit_length -= remainder;
|
||||
uint32_t bit = 0;
|
||||
const char* ptr = data + 1;
|
||||
|
||||
// navigate bit by bit through our nodes to build values from binary codes
|
||||
while (bit < bit_length) {
|
||||
bool bitval = static_cast<bool>((ptr[bit / 8] >> (bit % 8)) & 0x01);
|
||||
bit++;
|
||||
|
||||
// 1 in first bit denotes huffman compressed
|
||||
if (bitval) {
|
||||
int val;
|
||||
int n = 510;
|
||||
BA_PRECONDITION(nodes_[n].parent == 0);
|
||||
while (true) {
|
||||
BA_PRECONDITION(n <= 510);
|
||||
|
||||
bitval = static_cast<bool>((ptr[bit / 8] >> (bit % 8)) & 0x01);
|
||||
|
||||
// 1 for right, 0 for left
|
||||
if (bitval == 0) {
|
||||
if (nodes_[n].left_child == -1) {
|
||||
val = n;
|
||||
break;
|
||||
} else {
|
||||
n = nodes_[n].left_child;
|
||||
bit++;
|
||||
}
|
||||
} else {
|
||||
if (nodes_[n].right_child == -1) {
|
||||
val = n;
|
||||
break;
|
||||
} else {
|
||||
n = nodes_[n].right_child;
|
||||
bit++;
|
||||
}
|
||||
}
|
||||
// ERICF FIX - if both new children are dead-ends, stop reading
|
||||
// bits; otherwise we might read past the end of the buffer.
|
||||
// (I assume the original code didn't have child nodes with dual -1s
|
||||
// so this case probably never came up)
|
||||
if (nodes_[n].left_child == -1 && nodes_[n].right_child == -1) {
|
||||
val = n;
|
||||
break;
|
||||
}
|
||||
|
||||
if (bit > bit_length) {
|
||||
throw Exception("huffman decompress got bit > bitlength");
|
||||
}
|
||||
}
|
||||
out.push_back(static_cast<unsigned char&&>(val));
|
||||
} else {
|
||||
// just read next 8 bits as value
|
||||
uint8_t val;
|
||||
if (bit % 8 == 0) {
|
||||
BA_PRECONDITION((bit / 8) < (length - 1));
|
||||
val = static_cast<uint8_t>(ptr[bit / 8]);
|
||||
} else {
|
||||
BA_PRECONDITION((bit / 8 + 1) < (length - 1));
|
||||
val = (static_cast<uint8_t>(ptr[bit / 8]) >> bit % 8)
|
||||
| (static_cast<uint8_t>(ptr[bit / 8 + 1]) << (8 - bit % 8));
|
||||
}
|
||||
out.push_back(val);
|
||||
bit += 8;
|
||||
if (bit > bit_length) {
|
||||
throw Exception("huffman decompress got bit > bitlength b");
|
||||
}
|
||||
}
|
||||
}
|
||||
BA_PRECONDITION(bit == bit_length);
|
||||
return out;
|
||||
} else {
|
||||
// uncompressed - just provide it as is
|
||||
return src;
|
||||
}
|
||||
|
||||
#else
|
||||
data_out = data;
|
||||
length_out = length;
|
||||
#endif
|
||||
}
|
||||
|
||||
// old janky version..
|
||||
#if 0
|
||||
void Huffman::compress(const char* data, uint32_t length, const char*& data_out,
|
||||
uint32_t& length_out) {
|
||||
if (length > kMaxPacketSize) {
|
||||
throw Exception("packet too large for huffman compressor: "
|
||||
+ std::to_string(length) + " (packet "
|
||||
+ std::to_string(static_cast<int>(data[0])) + ")");
|
||||
}
|
||||
|
||||
#if BA_HUFFMAN_NET_COMPRESSION
|
||||
|
||||
// all our packets have a type bit at the beginning
|
||||
// we hijack the highest bit to denote whether we're sending
|
||||
// a compressed or uncompressed packet (1 for compressed, 0 for uncompressed)
|
||||
BA_PRECONDITION(data[0] >> 7 == 0);
|
||||
|
||||
// length_out = length;
|
||||
// if (buffer.size() < length_out)
|
||||
// buffer.resize(length_out);
|
||||
// memcpy(buffer.data,data,length_out);
|
||||
|
||||
// see how many bits we'll need
|
||||
uint32_t bit_count = 0;
|
||||
for (uint32_t i = 0; i < length; i++) {
|
||||
bit_count += nodes[static_cast<uint8_t>(data[i])].bits;
|
||||
}
|
||||
|
||||
// round up to next byte and add our one-byte header
|
||||
length_out = bit_count / 8 + 1;
|
||||
if (bit_count % 8) length_out++;
|
||||
bit_count %= 8;
|
||||
|
||||
// if compressed is bigger than uncompressed, go with uncompressed -
|
||||
// just return the data they provided
|
||||
|
||||
// lets always do huffman in debug builds; make sure we aren't making
|
||||
// any incorrect assumptions about
|
||||
// where stuff is compressed vs uncompressed.
|
||||
#if BA_DEBUG_BUILD
|
||||
bool force = false;
|
||||
#else
|
||||
bool force = false;
|
||||
#endif
|
||||
|
||||
if ((length_out >= length) && !force) {
|
||||
// throw Exception();
|
||||
data_out = data;
|
||||
length_out = length;
|
||||
} else {
|
||||
if (buffer.size() < length_out) {
|
||||
buffer.resize(length_out);
|
||||
}
|
||||
|
||||
// first byte gives our number of empty trailing bits
|
||||
memset(buffer.data, 0, buffer.size());
|
||||
char* ptr = buffer.data;
|
||||
int bit = 0;
|
||||
|
||||
*ptr = (8 - bit_count % 8);
|
||||
if (*ptr == 8) *ptr = 0;
|
||||
ptr++;
|
||||
|
||||
for (uint32_t i = 0; i < length; i++) {
|
||||
DoWriteBits(ptr, bit, nodes[static_cast<uint8_t>(data[i])].val,
|
||||
nodes[static_cast<uint8_t>(data[i])].bits);
|
||||
}
|
||||
|
||||
// make sure we're either at the end of our alloted buffer or we're one
|
||||
// from the end and the bitcount takes care of the rest
|
||||
assert(ptr - buffer.data == length_out
|
||||
|| (ptr - buffer.data == length_out - 1 && bit_count != 0));
|
||||
assert(bit == bit_count % 8);
|
||||
// for (int i = 0; i < length_out;i++)
|
||||
// buffer.data[i]--;
|
||||
|
||||
data_out = buffer.data;
|
||||
|
||||
// mark it as compressed
|
||||
buffer.data[0] |= (0x01 << 7);
|
||||
}
|
||||
#else
|
||||
|
||||
#if HUFFMAN_TRAINING_MODE
|
||||
train(data, length);
|
||||
#endif
|
||||
|
||||
data_out = data;
|
||||
length_out = length;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
void Huffman::decompress(const char* data, uint32_t length,
|
||||
const char*& data_out, uint32_t& length_out) {
|
||||
#if BA_HUFFMAN_NET_COMPRESSION
|
||||
|
||||
uint8_t remainder = *data & 0x0F;
|
||||
bool compressed = *data >> 7;
|
||||
|
||||
if (compressed) {
|
||||
uint32_t bit_length = ((length - 1) * 8) - remainder;
|
||||
|
||||
uint32_t bit = 0;
|
||||
const char* ptr = data + 1;
|
||||
uint32_t bytes = 0;
|
||||
|
||||
// navigate bit by bit through our nodes to build values from binary codes
|
||||
while (bit < bit_length) {
|
||||
bool bitval = (ptr[bit / 8] >> (bit % 8)) & 0x01;
|
||||
bit++;
|
||||
|
||||
// 1 in first bit denotes huffman compressed
|
||||
if (bitval) {
|
||||
int val;
|
||||
int n = 510;
|
||||
assert(nodes[n].parent == 0);
|
||||
while (true) {
|
||||
bitval = (ptr[bit / 8] >> (bit % 8)) & 0x01;
|
||||
|
||||
// 1 for right, 0 for left
|
||||
if (bitval == 0) {
|
||||
if (nodes[n].left_child == -1) {
|
||||
val = n;
|
||||
break;
|
||||
} else {
|
||||
n = nodes[n].left_child;
|
||||
bit++;
|
||||
}
|
||||
} else {
|
||||
if (nodes[n].right_child == -1) {
|
||||
val = n;
|
||||
break;
|
||||
} else {
|
||||
n = nodes[n].right_child;
|
||||
bit++;
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer.data[bytes] = val;
|
||||
bytes++;
|
||||
} else {
|
||||
// just read next 8 bits as value
|
||||
// unsigned int val = (((ptr[(bit+0)/8] >> ((bit+0)%8)) & 0x01)
|
||||
// << 0)
|
||||
// | (((ptr[(bit+1)/8] >> ((bit+1)%8)) & 0x01) << 1)
|
||||
// | (((ptr[(bit+2)/8] >> ((bit+2)%8)) & 0x01) << 2)
|
||||
// | (((ptr[(bit+3)/8] >> ((bit+3)%8)) & 0x01) << 3)
|
||||
// | (((ptr[(bit+4)/8] >> ((bit+4)%8)) & 0x01) << 4)
|
||||
// | (((ptr[(bit+5)/8] >> ((bit+5)%8)) & 0x01) << 5)
|
||||
// | (((ptr[(bit+6)/8] >> ((bit+6)%8)) & 0x01) << 6)
|
||||
// | (((ptr[(bit+7)/8] >> ((bit+7)%8)) & 0x01) << 7);
|
||||
|
||||
uint8_t val;
|
||||
if (bit % 8 == 0)
|
||||
val = static_cast<uint8_t>(ptr[bit / 8]);
|
||||
else
|
||||
val = (static_cast<uint8_t>(ptr[bit / 8]) >> bit % 8)
|
||||
| (static_cast<uint8_t>(ptr[bit / 8 + 1]) << (8 - bit % 8));
|
||||
// uint8_t val2 = (((ptr[(bit+0)/8] >> ((bit+0)%8)) & 0x01) <<
|
||||
// 0)
|
||||
// | (((ptr[(bit+1)/8] >> ((bit+1)%8)) & 0x01) << 1)
|
||||
// | (((ptr[(bit+2)/8] >> ((bit+2)%8)) & 0x01) << 2)
|
||||
// | (((ptr[(bit+3)/8] >> ((bit+3)%8)) & 0x01) << 3)
|
||||
// | (((ptr[(bit+4)/8] >> ((bit+4)%8)) & 0x01) << 4)
|
||||
// | (((ptr[(bit+5)/8] >> ((bit+5)%8)) & 0x01) << 5)
|
||||
// | (((ptr[(bit+6)/8] >> ((bit+6)%8)) & 0x01) << 6)
|
||||
// | (((ptr[(bit+7)/8] >> ((bit+7)%8)) & 0x01) << 7);
|
||||
// assert(val2 == val);
|
||||
buffer.data[bytes] = val;
|
||||
bytes++;
|
||||
bit += 8;
|
||||
// throw Exception();
|
||||
}
|
||||
}
|
||||
assert(bit == bit_length);
|
||||
|
||||
// fixme??
|
||||
if (bytes > kMaxPacketSize) {
|
||||
Log("HUFFMAN DECOMPRESSING TO TOO LARGE: " + std::to_string(bytes));
|
||||
}
|
||||
assert(bytes <= kMaxPacketSize);
|
||||
|
||||
// throw Exception();
|
||||
|
||||
// length_out = length;
|
||||
// if (buffer.size() < length_out)
|
||||
// buffer.resize(length_out);
|
||||
// memcpy(buffer.data,data,length_out);
|
||||
// data_out = buffer.data;
|
||||
// for (int i = 0; i < length_out;i++)
|
||||
// buffer.data[i]++;
|
||||
data_out = buffer.data;
|
||||
length_out = bytes;
|
||||
|
||||
} else {
|
||||
// uncompressed - just provide it as is
|
||||
data_out = data;
|
||||
length_out = length;
|
||||
}
|
||||
#else
|
||||
data_out = data;
|
||||
length_out = length;
|
||||
#endif
|
||||
}
|
||||
#endif // 0
|
||||
|
||||
#if HUFFMAN_TRAINING_MODE
|
||||
void Huffman::train(const char* buffer, int len) {
|
||||
if (built) {
|
||||
test_bytes += len;
|
||||
for (int i = 0; i < len; i++) {
|
||||
test_bits_compressed += nodes[static_cast<uint8_t>(buffer[i])].bits;
|
||||
}
|
||||
static int poo = 0;
|
||||
poo++;
|
||||
if (poo > 100) {
|
||||
poo = 0;
|
||||
test_bytes = 0;
|
||||
test_bits_compressed = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
total_length += len;
|
||||
while (len > 0) {
|
||||
nodes[static_cast<uint8_t>(*buffer)].frequency++;
|
||||
total_count++;
|
||||
buffer++;
|
||||
len--;
|
||||
}
|
||||
if (total_length > kTrainingLength) {
|
||||
Log("HUFFMAN TRAINING COMPLETE:");
|
||||
|
||||
build();
|
||||
|
||||
// spit the C array to stdout for insertion into our code
|
||||
string s = "{";
|
||||
for (int i = 0; i < 256; i++) {
|
||||
s += std::to_string(nodes[i].frequency);
|
||||
if (i < 255) s += ",";
|
||||
}
|
||||
s += "}";
|
||||
Log("FINAL: " + s);
|
||||
}
|
||||
}
|
||||
#endif // HUFFMAN_TRAINING_MODE
|
||||
|
||||
void Huffman::build() {
|
||||
assert(!built);
|
||||
|
||||
// if we're not in training mode, use our hard-coded values
|
||||
#if 1
|
||||
for (int i = 0; i < 256; i++) {
|
||||
nodes_[i].frequency = g_freqs[i];
|
||||
}
|
||||
#else
|
||||
// go through and set all but the top 15 or so to zero
|
||||
// this is because all smaller values will be provided in full binary
|
||||
// form and thus don't need to be influencing the graph
|
||||
for (int i = 0; i < 256; i++) {
|
||||
int bigger = 0;
|
||||
for (int j = 0; j < 256; j++) {
|
||||
if (nodes[j].frequency > nodes[i].frequency) {
|
||||
bigger++;
|
||||
if (bigger > 15) {
|
||||
nodes[i].frequency = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// first 256 nodes are leaves
|
||||
int node_count = 256;
|
||||
|
||||
// now loop through existing nodes finding the two smallest values without
|
||||
// parents and creating a new parent node for them with their sum as its
|
||||
// frequency value once there's only 1 node without a parent we're done
|
||||
// (that's the root node)
|
||||
int smallest1;
|
||||
int smallest2;
|
||||
while (node_count < 511) {
|
||||
int i = 0;
|
||||
|
||||
// find first two non-parented nodes
|
||||
while (nodes_[i].parent != 0) i++;
|
||||
smallest1 = i;
|
||||
i++;
|
||||
while (nodes_[i].parent != 0) i++;
|
||||
smallest2 = i;
|
||||
i++;
|
||||
while (i < node_count) {
|
||||
if (nodes_[i].parent == 0) {
|
||||
// compare each node to the larger of the two existing to try and knock
|
||||
// it off
|
||||
if (nodes_[smallest1].frequency > nodes_[smallest2].frequency) {
|
||||
if (nodes_[i].frequency < nodes_[smallest1].frequency) smallest1 = i;
|
||||
} else {
|
||||
if (nodes_[i].frequency < nodes_[smallest2].frequency) smallest2 = i;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
nodes_[node_count].frequency =
|
||||
nodes_[smallest1].frequency + nodes_[smallest2].frequency;
|
||||
nodes_[smallest1].parent = static_cast<uint8_t>(node_count - 255);
|
||||
nodes_[smallest2].parent = static_cast<uint8_t>(node_count - 255);
|
||||
nodes_[node_count].right_child = static_cast<int16_t>(smallest1);
|
||||
nodes_[node_count].left_child = static_cast<int16_t>(smallest2);
|
||||
|
||||
node_count++;
|
||||
}
|
||||
|
||||
assert(nodes_[509].parent != 0);
|
||||
assert(nodes_[510].parent == 0);
|
||||
|
||||
// now store binary values for each base value (0-255)
|
||||
for (int i = 0; i < 256; i++) {
|
||||
// uint32_t val = 0;
|
||||
nodes_[i].val = 0;
|
||||
nodes_[i].bits = 0;
|
||||
int index = i;
|
||||
while (nodes_[index].parent != 0) {
|
||||
// 0 if we're left child, 1 if we're right
|
||||
if (nodes_[nodes_[index].parent + 255].right_child == index) {
|
||||
nodes_[i].val = static_cast<uint16_t>(nodes_[i].val << 1 | 0x01);
|
||||
} else {
|
||||
assert(nodes_[nodes_[index].parent + 255].left_child == index);
|
||||
nodes_[i].val = nodes_[i].val << 1;
|
||||
}
|
||||
nodes_[i].bits++;
|
||||
|
||||
index = nodes_[index].parent + 255;
|
||||
}
|
||||
// we're slightly different than normal huffman in that
|
||||
// our first bit denotes whether the following values are the huffman bits
|
||||
// or the full 8 bit value.
|
||||
if (nodes_[i].bits >= 8) {
|
||||
nodes_[i].bits = 8;
|
||||
// nodes[i].val = nodes[i].val << 1;
|
||||
nodes_[i].val = static_cast<uint16_t>(i << 1);
|
||||
} else {
|
||||
nodes_[i].val = static_cast<uint16_t>(
|
||||
nodes_[i].val << 1
|
||||
| 0x01); // 1 in first bit denotes huffman compressed
|
||||
}
|
||||
// nodes[i].val = 0;
|
||||
nodes_[i].bits += 1;
|
||||
}
|
||||
|
||||
built = true;
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
} // namespace ballistica
|
||||
61
src/ballistica/generic/huffman.h
Normal file
61
src/ballistica/generic/huffman.h
Normal file
@ -0,0 +1,61 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_GENERIC_HUFFMAN_H_
|
||||
#define BALLISTICA_GENERIC_HUFFMAN_H_
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "ballistica/core/object.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
class Huffman {
|
||||
public:
|
||||
Huffman();
|
||||
~Huffman();
|
||||
|
||||
#if HUFFMAN_TRAINING_MODE
|
||||
void train(const char* buffer, int len);
|
||||
#endif
|
||||
|
||||
void build();
|
||||
|
||||
// NOTE: this assumes the topmost bit of the first byte is unused
|
||||
// (see details in implementation).
|
||||
auto compress(const std::vector<uint8_t>& src) -> std::vector<uint8_t>;
|
||||
auto decompress(const std::vector<uint8_t>& src) -> std::vector<uint8_t>;
|
||||
auto get_built() const -> bool { return built; }
|
||||
|
||||
private:
|
||||
bool built;
|
||||
#if HUFFMAN_TRAINING_MODE
|
||||
uint32_t test_bytes = 0;
|
||||
uint32_t test_bits_compressed = 0;
|
||||
int total_count = 0;
|
||||
int total_length = 0;
|
||||
#endif
|
||||
|
||||
class Node {
|
||||
public:
|
||||
Node() = default;
|
||||
|
||||
// Left child index in node array (-1 for none).
|
||||
int16_t left_child = -1;
|
||||
|
||||
// Right child index in node array (-1 for none).
|
||||
int16_t right_child = -1;
|
||||
|
||||
// Parent index in node array (0 for none - add 255 to this to get actual
|
||||
// index).
|
||||
uint8_t parent = 0;
|
||||
uint8_t bits = 0;
|
||||
uint16_t val = 0;
|
||||
int frequency = 0;
|
||||
};
|
||||
|
||||
Node nodes_[511];
|
||||
};
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // BALLISTICA_GENERIC_HUFFMAN_H_
|
||||
1105
src/ballistica/generic/json.cc
Normal file
1105
src/ballistica/generic/json.cc
Normal file
File diff suppressed because it is too large
Load Diff
239
src/ballistica/generic/json.h
Normal file
239
src/ballistica/generic/json.h
Normal file
@ -0,0 +1,239 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_GENERIC_JSON_H_
|
||||
#define BALLISTICA_GENERIC_JSON_H_
|
||||
|
||||
/*
|
||||
Copyright (c) 2009 Dave Gamble
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "ballistica/ballistica.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma ide diagnostic ignored "OCUnusedMacroInspection"
|
||||
|
||||
// #ifdef __cplusplus
|
||||
// extern "C" {
|
||||
// #endif
|
||||
|
||||
/* cJSON Types: */
|
||||
#define cJSON_False 0u
|
||||
#define cJSON_True 1u
|
||||
#define cJSON_NULL 2u
|
||||
#define cJSON_Number 3u
|
||||
#define cJSON_String 4u
|
||||
#define cJSON_Array 5u
|
||||
#define cJSON_Object 6u
|
||||
|
||||
#define cJSON_IsReference 256u
|
||||
|
||||
/* The cJSON structure: */
|
||||
typedef struct cJSON {
|
||||
struct cJSON *next,
|
||||
*prev; /* next/prev allow you to walk array/object chains. Alternatively,
|
||||
use GetArraySize/GetArrayItem/GetObjectItem */
|
||||
struct cJSON*
|
||||
child; /* An array or object item will have a child pointer pointing to a
|
||||
chain of the items in the array/object. */
|
||||
|
||||
uint32_t type; /* The type of the item, as above. */
|
||||
|
||||
char* valuestring; /* The item's string, if type==cJSON_String */
|
||||
int valueint; /* The item's number, if type==cJSON_Number */
|
||||
double valuedouble; /* The item's number, if type==cJSON_Number */
|
||||
|
||||
char* string; /* The item's name string, if this item is the child of, or is
|
||||
in the list of subitems of an object. */
|
||||
} cJSON;
|
||||
|
||||
typedef struct cJSON_Hooks {
|
||||
void* (*malloc_fn)(size_t sz);
|
||||
void (*free_fn)(void* ptr);
|
||||
} cJSON_Hooks;
|
||||
|
||||
/* Supply malloc, realloc and free functions to cJSON */
|
||||
extern void cJSON_InitHooks(cJSON_Hooks* hooks);
|
||||
|
||||
/* Supply a block of JSON, and this returns a cJSON object you can interrogate.
|
||||
* Call cJSON_Delete when finished. */
|
||||
extern auto cJSON_Parse(const char* value) -> cJSON*;
|
||||
/* Render a cJSON entity to text for transfer/storage. Free the char* when
|
||||
* finished. */
|
||||
extern auto cJSON_Print(cJSON* item) -> char*;
|
||||
/* Render a cJSON entity to text for transfer/storage without any formatting.
|
||||
* Free the char* when finished. */
|
||||
extern auto cJSON_PrintUnformatted(cJSON* item) -> char*;
|
||||
/* Delete a cJSON entity and all subentities. */
|
||||
extern void cJSON_Delete(cJSON* c);
|
||||
|
||||
/* Returns the number of items in an array (or object). */
|
||||
extern auto cJSON_GetArraySize(cJSON* array) -> int;
|
||||
/* Retrieve item number "item" from array "array". Returns NULL if unsuccessful.
|
||||
*/
|
||||
extern auto cJSON_GetArrayItem(cJSON* array, int item) -> cJSON*;
|
||||
/* Get item "string" from object. Case insensitive. */
|
||||
extern auto cJSON_GetObjectItem(cJSON* object, const char* string) -> cJSON*;
|
||||
|
||||
/* For analysing failed parses. This returns a pointer to the parse error.
|
||||
* You'll probably need to look a few chars back to make sense of it. Defined
|
||||
* when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
|
||||
extern auto cJSON_GetErrorPtr() -> const char*;
|
||||
|
||||
/* These calls create a cJSON item of the appropriate type. */
|
||||
extern auto cJSON_CreateNull() -> cJSON*;
|
||||
extern auto cJSON_CreateTrue() -> cJSON*;
|
||||
extern auto cJSON_CreateFalse() -> cJSON*;
|
||||
extern auto cJSON_CreateBool(int b) -> cJSON*;
|
||||
extern auto cJSON_CreateNumber(double num) -> cJSON*;
|
||||
extern auto cJSON_CreateString(const char* string) -> cJSON*;
|
||||
extern auto cJSON_CreateArray() -> cJSON*;
|
||||
extern auto cJSON_CreateObject() -> cJSON*;
|
||||
|
||||
/* These utilities create an Array of count items. */
|
||||
extern auto cJSON_CreateIntArray(const int* numbers, int count) -> cJSON*;
|
||||
extern auto cJSON_CreateFloatArray(const float* numbers, int count) -> cJSON*;
|
||||
extern auto cJSON_CreateDoubleArray(const double* numbers, int count) -> cJSON*;
|
||||
extern auto cJSON_CreateStringArray(const char** strings, int count) -> cJSON*;
|
||||
|
||||
/* Append item to the specified array/object. */
|
||||
extern void cJSON_AddItemToArray(cJSON* array, cJSON* item);
|
||||
extern void cJSON_AddItemToObject(cJSON* object, const char* string,
|
||||
cJSON* item);
|
||||
/* Append reference to item to the specified array/object. Use this when you
|
||||
* want to add an existing cJSON to a new cJSON, but don't want to corrupt your
|
||||
* existing cJSON. */
|
||||
extern void cJSON_AddItemReferenceToArray(cJSON* array, cJSON* item);
|
||||
extern void cJSON_AddItemReferenceToObject(cJSON* object, const char* string,
|
||||
cJSON* item);
|
||||
|
||||
/* Remove/Detach items from Arrays/Objects. */
|
||||
extern auto cJSON_DetachItemFromArray(cJSON* array, int which) -> cJSON*;
|
||||
extern void cJSON_DeleteItemFromArray(cJSON* array, int which);
|
||||
extern auto cJSON_DetachItemFromObject(cJSON* object, const char* string)
|
||||
-> cJSON*;
|
||||
extern void cJSON_DeleteItemFromObject(cJSON* object, const char* string);
|
||||
|
||||
/* Update array items. */
|
||||
extern void cJSON_ReplaceItemInArray(cJSON* array, int which, cJSON* newitem);
|
||||
extern void cJSON_ReplaceItemInObject(cJSON* object, const char* string,
|
||||
cJSON* newitem);
|
||||
|
||||
/* Duplicate a cJSON item */
|
||||
extern auto cJSON_Duplicate(cJSON* item, int recurse) -> cJSON*;
|
||||
/* Duplicate will create a new, identical cJSON item to the one you pass, in new
|
||||
memory that will need to be released. With recurse!=0, it will duplicate any
|
||||
children connected to the item. The item->next and ->prev pointers are always
|
||||
zero on return from Duplicate. */
|
||||
|
||||
/* ParseWithOpts allows you to require (and check) that the JSON is null
|
||||
* terminated, and to retrieve the pointer to the final byte parsed. */
|
||||
extern auto cJSON_ParseWithOpts(const char* value,
|
||||
const char** return_parse_end,
|
||||
int require_null_terminated) -> cJSON*;
|
||||
|
||||
extern void cJSON_Minify(char* json);
|
||||
|
||||
/* Macros for creating things quickly. */
|
||||
#define cJSON_AddNullToObject(object, name) \
|
||||
cJSON_AddItemToObject(object, name, cJSON_CreateNull())
|
||||
#define cJSON_AddTrueToObject(object, name) \
|
||||
cJSON_AddItemToObject(object, name, cJSON_CreateTrue())
|
||||
#define cJSON_AddFalseToObject(object, name) \
|
||||
cJSON_AddItemToObject(object, name, cJSON_CreateFalse())
|
||||
#define cJSON_AddBoolToObject(object, name, b) \
|
||||
cJSON_AddItemToObject(object, name, cJSON_CreateBool(b))
|
||||
#define cJSON_AddNumberToObject(object, name, n) \
|
||||
cJSON_AddItemToObject(object, name, cJSON_CreateNumber(n))
|
||||
#define cJSON_AddStringToObject(object, name, s) \
|
||||
cJSON_AddItemToObject(object, name, cJSON_CreateString(s))
|
||||
|
||||
/* When assigning an integer value, it needs to be propagated to valuedouble
|
||||
* too. */
|
||||
#define cJSON_SetIntValue(object, val) \
|
||||
((object) ? (object)->valueint = (object)->valuedouble = (val) : (val))
|
||||
|
||||
// ericf addition: c++ wrapper for this stuff.
|
||||
|
||||
// NOTE: once added to a dict/list/etc, the underlying cJSON's
|
||||
// lifecycle is dependent on its parent, not this object.
|
||||
// ..So be sure to keep the root JsonObject alive as long as child
|
||||
// objects are being accessed.
|
||||
class JsonObject {
|
||||
public:
|
||||
~JsonObject() {
|
||||
if (obj_ && root_) {
|
||||
cJSON_Delete(obj_);
|
||||
}
|
||||
}
|
||||
auto root() const -> bool { return root_; }
|
||||
auto obj() const -> cJSON* { return obj_; }
|
||||
|
||||
// Root objects will clean themselves up.
|
||||
// turn this off when adding to a dict/list/etc.
|
||||
// that will take responsibility for that instead.
|
||||
void set_root(bool val) { root_ = val; }
|
||||
|
||||
protected:
|
||||
JsonObject() = default;
|
||||
|
||||
// Used by subclasses to fill value.
|
||||
void set_obj(cJSON* val) {
|
||||
assert(obj_ == nullptr);
|
||||
obj_ = val;
|
||||
}
|
||||
|
||||
private:
|
||||
cJSON* obj_ = nullptr;
|
||||
bool root_ = true;
|
||||
};
|
||||
|
||||
class JsonDict : public JsonObject {
|
||||
public:
|
||||
JsonDict() { set_obj(cJSON_CreateObject()); }
|
||||
void AddNumber(const std::string& name, double val) {
|
||||
cJSON_AddItemToObject(obj(), name.c_str(), cJSON_CreateNumber(val));
|
||||
}
|
||||
void AddString(const std::string& name, const std::string& val) {
|
||||
cJSON_AddItemToObject(obj(), name.c_str(), cJSON_CreateString(val.c_str()));
|
||||
}
|
||||
auto PrintUnformatted() -> std::string {
|
||||
return cJSON_PrintUnformatted(obj());
|
||||
}
|
||||
};
|
||||
|
||||
// class JsonNumber : public JsonObject {
|
||||
// public:
|
||||
// JsonNumber(double val) { set_obj(cJSON_CreateNumber(val)); }
|
||||
// };
|
||||
|
||||
// class JsonString : public JsonObject {
|
||||
// public:
|
||||
// JsonString(const std::string& s) { set_obj(cJSON_CreateString(s.c_str()));
|
||||
// } JsonString(const char* s) { set_obj(cJSON_CreateString(s)); }
|
||||
// };
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // BALLISTICA_GENERIC_JSON_H_
|
||||
38
src/ballistica/generic/lambda_runnable.h
Normal file
38
src/ballistica/generic/lambda_runnable.h
Normal file
@ -0,0 +1,38 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_GENERIC_LAMBDA_RUNNABLE_H_
|
||||
#define BALLISTICA_GENERIC_LAMBDA_RUNNABLE_H_
|
||||
|
||||
#include "ballistica/generic/runnable.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
// (don't use this class directly; call NewLambdaRunnable below)
|
||||
// from what I hear, heavy use of std::function can slow
|
||||
// compiles down dramatically, so sticking to raw lambdas here
|
||||
template <typename F>
|
||||
class LambdaRunnable : public Runnable {
|
||||
public:
|
||||
explicit LambdaRunnable(F lambda) : lambda_(lambda) {}
|
||||
void Run() override { lambda_(); }
|
||||
|
||||
private:
|
||||
F lambda_;
|
||||
};
|
||||
|
||||
// Call this to allocate and return a raw lambda runnable
|
||||
template <typename F>
|
||||
auto NewLambdaRunnable(const F& lambda) -> Object::Ref<Runnable> {
|
||||
return Object::New<Runnable, LambdaRunnable<F>>(lambda);
|
||||
}
|
||||
|
||||
// Same but returns the raw pointer instead of a ref;
|
||||
// (used when passing across threads).
|
||||
template <typename F>
|
||||
auto NewLambdaRunnableRaw(const F& lambda) -> Runnable* {
|
||||
return Object::NewDeferred<LambdaRunnable<F>>(lambda);
|
||||
}
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // BALLISTICA_GENERIC_LAMBDA_RUNNABLE_H_
|
||||
49
src/ballistica/generic/real_timer.h
Normal file
49
src/ballistica/generic/real_timer.h
Normal file
@ -0,0 +1,49 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_GENERIC_REAL_TIMER_H_
|
||||
#define BALLISTICA_GENERIC_REAL_TIMER_H_
|
||||
|
||||
#include "ballistica/ballistica.h"
|
||||
#include "ballistica/core/object.h"
|
||||
#include "ballistica/game/game.h"
|
||||
#include "ballistica/generic/runnable.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
// Manages a timer which runs on real time and calls a
|
||||
// 'HandleRealTimerExpired' method on the provided pointer.
|
||||
template <typename T>
|
||||
class RealTimer : public Object {
|
||||
public:
|
||||
RealTimer(millisecs_t length, bool repeat, T* delegate) {
|
||||
assert(g_game);
|
||||
assert(InGameThread());
|
||||
timer_id_ = g_game->NewRealTimer(
|
||||
length, repeat, Object::New<Runnable, Callback>(delegate, this));
|
||||
}
|
||||
void SetLength(uint32_t length) {
|
||||
assert(InGameThread());
|
||||
g_game->SetRealTimerLength(timer_id_, length);
|
||||
}
|
||||
~RealTimer() override {
|
||||
assert(InGameThread());
|
||||
g_game->DeleteRealTimer(timer_id_);
|
||||
}
|
||||
|
||||
private:
|
||||
class Callback : public Runnable {
|
||||
public:
|
||||
Callback(T* delegate, RealTimer<T>* timer)
|
||||
: delegate_(delegate), timer_(timer) {}
|
||||
void Run() override { delegate_->HandleRealTimerExpired(timer_); }
|
||||
|
||||
private:
|
||||
RealTimer<T>* timer_;
|
||||
T* delegate_;
|
||||
};
|
||||
int timer_id_;
|
||||
};
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // BALLISTICA_GENERIC_REAL_TIMER_H_
|
||||
11
src/ballistica/generic/runnable.cc
Normal file
11
src/ballistica/generic/runnable.cc
Normal file
@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2011-2020 Eric Froemling
|
||||
|
||||
#include "ballistica/generic/runnable.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
auto Runnable::GetThreadOwnership() const -> Object::ThreadOwnership {
|
||||
return ThreadOwnership::kNextReferencing;
|
||||
}
|
||||
|
||||
} // namespace ballistica
|
||||
23
src/ballistica/generic/runnable.h
Normal file
23
src/ballistica/generic/runnable.h
Normal file
@ -0,0 +1,23 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_GENERIC_RUNNABLE_H_
|
||||
#define BALLISTICA_GENERIC_RUNNABLE_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "ballistica/core/object.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
class Runnable : public Object {
|
||||
public:
|
||||
virtual void Run() = 0;
|
||||
|
||||
// these are used on lots of threads; lets
|
||||
// lock to wherever we're first referenced
|
||||
auto GetThreadOwnership() const -> ThreadOwnership override;
|
||||
};
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // BALLISTICA_GENERIC_RUNNABLE_H_
|
||||
55
src/ballistica/generic/timer.cc
Normal file
55
src/ballistica/generic/timer.cc
Normal file
@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2011-2020 Eric Froemling
|
||||
|
||||
#include "ballistica/generic/timer.h"
|
||||
|
||||
#include "ballistica/generic/timer_list.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
Timer::Timer(TimerList* list_in, int id_in, TimerMedium current_time,
|
||||
TimerMedium length_in, TimerMedium offset_in, int repeat_count_in)
|
||||
: list_(list_in),
|
||||
on_list_(false),
|
||||
initial_(true),
|
||||
dead_(false),
|
||||
list_died_(false),
|
||||
last_run_time_(current_time),
|
||||
expire_time_(current_time + offset_in),
|
||||
id_(id_in),
|
||||
length_(length_in),
|
||||
repeat_count_(repeat_count_in) {
|
||||
list_->timer_count_total_++;
|
||||
}
|
||||
|
||||
Timer::~Timer() {
|
||||
// If the list is dead, dont touch the corpse.
|
||||
if (!list_died_) {
|
||||
if (on_list_) {
|
||||
list_->PullTimer(id_);
|
||||
} else {
|
||||
// Should never be explicitly deleting the current client timer
|
||||
// (it should just get marked as dead so the loop can kill it when
|
||||
// re-submitted).
|
||||
assert(list_->client_timer_ != this);
|
||||
}
|
||||
list_->timer_count_total_--;
|
||||
}
|
||||
}
|
||||
|
||||
void Timer::SetLength(TimerMedium l, bool set_start_time,
|
||||
TimerMedium starttime) {
|
||||
if (on_list_) {
|
||||
assert(id_ != 0); // zero denotes "no-id"
|
||||
Timer* t = list_->PullTimer(id_);
|
||||
BA_PRECONDITION(t == this);
|
||||
length_ = l;
|
||||
if (set_start_time) last_run_time_ = starttime;
|
||||
expire_time_ = last_run_time_ + length_;
|
||||
list_->AddTimer(this);
|
||||
} else {
|
||||
length_ = l;
|
||||
if (set_start_time) last_run_time_ = starttime;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ballistica
|
||||
41
src/ballistica/generic/timer.h
Normal file
41
src/ballistica/generic/timer.h
Normal file
@ -0,0 +1,41 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_GENERIC_TIMER_H_
|
||||
#define BALLISTICA_GENERIC_TIMER_H_
|
||||
|
||||
#include "ballistica/ballistica.h"
|
||||
#include "ballistica/core/object.h"
|
||||
#include "ballistica/generic/runnable.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
class Timer {
|
||||
public:
|
||||
auto id() const -> int { return id_; }
|
||||
auto length() const -> TimerMedium { return length_; }
|
||||
void SetLength(TimerMedium l, bool set_start_time = false,
|
||||
TimerMedium starttime = 0);
|
||||
|
||||
private:
|
||||
Timer(TimerList* list_in, int id_in, TimerMedium current_time,
|
||||
TimerMedium length_in, TimerMedium offset_in, int repeat_count_in);
|
||||
virtual ~Timer();
|
||||
TimerList* list_{};
|
||||
bool on_list_{};
|
||||
Timer* next_{};
|
||||
bool initial_{};
|
||||
bool dead_{};
|
||||
bool list_died_{};
|
||||
TimerMedium last_run_time_{};
|
||||
TimerMedium expire_time_{};
|
||||
int id_{};
|
||||
TimerMedium length_{};
|
||||
int repeat_count_{};
|
||||
Object::Ref<Runnable> runnable_;
|
||||
// FIXME: Shouldn't have friend classes in different files.
|
||||
friend class TimerList;
|
||||
};
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // BALLISTICA_GENERIC_TIMER_H_
|
||||
271
src/ballistica/generic/timer_list.cc
Normal file
271
src/ballistica/generic/timer_list.cc
Normal file
@ -0,0 +1,271 @@
|
||||
// Copyright (c) 2011-2020 Eric Froemling
|
||||
|
||||
#include "ballistica/generic/timer_list.h"
|
||||
|
||||
#include "ballistica/generic/runnable.h"
|
||||
#include "ballistica/generic/timer.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
TimerList::TimerList() = default;
|
||||
|
||||
TimerList::~TimerList() {
|
||||
Clear();
|
||||
|
||||
// Don't delete the client timer if one exists; just inform it that the list
|
||||
// is dead.
|
||||
if (client_timer_) {
|
||||
client_timer_->list_died_ = true;
|
||||
}
|
||||
|
||||
if (g_buildconfig.debug_build()) {
|
||||
if (timer_count_active_ != 0) {
|
||||
Log("Error: Invalid timerlist state on teardown.");
|
||||
}
|
||||
if (timer_count_inactive_ != 0) {
|
||||
Log("Error: Invalid timerlist state on teardown.");
|
||||
}
|
||||
if (!((timer_count_total_ == 0)
|
||||
|| (client_timer_ != nullptr && timer_count_total_ == 1))) {
|
||||
Log("Error: Invalid timerlist state on teardown.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TimerList::Clear() {
|
||||
assert(!are_clearing_);
|
||||
are_clearing_ = true;
|
||||
while (timers_) {
|
||||
Timer* t = timers_;
|
||||
t->on_list_ = false;
|
||||
timer_count_active_--;
|
||||
timers_ = t->next_;
|
||||
delete t;
|
||||
}
|
||||
while (timers_inactive_) {
|
||||
Timer* t = timers_inactive_;
|
||||
t->on_list_ = false;
|
||||
timer_count_inactive_--;
|
||||
timers_inactive_ = t->next_;
|
||||
delete t;
|
||||
}
|
||||
are_clearing_ = false;
|
||||
}
|
||||
|
||||
// Pull a timer out of the list.
|
||||
auto TimerList::PullTimer(int timer_id, bool remove) -> Timer* {
|
||||
Timer* t = timers_;
|
||||
Timer* p = nullptr;
|
||||
while (t) {
|
||||
if (t->id_ == timer_id) {
|
||||
if (remove) {
|
||||
if (p) {
|
||||
p->next_ = t->next_;
|
||||
} else {
|
||||
timers_ = t->next_;
|
||||
}
|
||||
t->on_list_ = false;
|
||||
timer_count_active_--;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
p = t;
|
||||
t = t->next_;
|
||||
}
|
||||
|
||||
// Didn't find it. check the inactive list.
|
||||
t = timers_inactive_;
|
||||
p = nullptr;
|
||||
while (t) {
|
||||
if (t->id_ == timer_id) {
|
||||
if (remove) {
|
||||
if (p) {
|
||||
p->next_ = t->next_;
|
||||
} else {
|
||||
timers_inactive_ = t->next_;
|
||||
}
|
||||
t->on_list_ = false;
|
||||
timer_count_inactive_--;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
p = t;
|
||||
t = t->next_;
|
||||
}
|
||||
|
||||
// Not on either list; only other possibility is the current client timer.
|
||||
if (client_timer_ && client_timer_->id_ == timer_id) {
|
||||
return client_timer_;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TimerList::Run(TimerMedium target_time) {
|
||||
assert(!are_clearing_);
|
||||
|
||||
// Limit our runs to whats initially on the list so we don't spin all day if
|
||||
// a timer resets itself to run immediately.
|
||||
// FIXME - what if this timer kills one or more of the initially-expired ones
|
||||
// ..that means it could potentially run more than once.. does it matter?
|
||||
int expired_count = GetExpiredCount(target_time);
|
||||
for (int timers_to_run = expired_count; timers_to_run > 0; timers_to_run--) {
|
||||
Timer* t = GetExpiredTimer(target_time);
|
||||
if (t) {
|
||||
assert(!t->dead_);
|
||||
try {
|
||||
t->runnable_->Run();
|
||||
} catch (const std::exception&) {
|
||||
// If something went wrong, put our list back in order and propagate.
|
||||
if (t->list_died_) {
|
||||
delete t; // nothing is left but this timer
|
||||
} else {
|
||||
SubmitTimer(t);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
// If this timer killed the list, stop; otherwise put it back and keep on
|
||||
// trucking.
|
||||
if (t->list_died_) {
|
||||
delete t; // nothing is left but this timer
|
||||
return;
|
||||
} else {
|
||||
SubmitTimer(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
auto TimerList::GetExpiredCount(TimerMedium target_time) -> int {
|
||||
assert(!are_clearing_);
|
||||
|
||||
Timer* t = timers_;
|
||||
int count = 0;
|
||||
while (t && t->expire_time_ <= target_time) {
|
||||
count++;
|
||||
t = t->next_;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Returns the next expired timer. When done with the timer,
|
||||
// return it to the list with Timer::submit()
|
||||
// (this will either put it back in line or delete it)
|
||||
auto TimerList::GetExpiredTimer(TimerMedium target_time) -> Timer* {
|
||||
assert(!are_clearing_);
|
||||
|
||||
Timer* t;
|
||||
if (timers_ != nullptr && timers_->expire_time_ <= target_time) {
|
||||
t = timers_;
|
||||
t->last_run_time_ = target_time;
|
||||
timers_ = timers_->next_;
|
||||
timer_count_active_--;
|
||||
t->on_list_ = false;
|
||||
|
||||
// Exactly one timer at a time can be out in userland and not on
|
||||
// any list - this is now that one.
|
||||
assert(client_timer_ == nullptr);
|
||||
client_timer_ = t;
|
||||
return t;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto TimerList::NewTimer(TimerMedium current_time, TimerMedium length,
|
||||
TimerMedium offset, int repeat_count,
|
||||
const Object::Ref<Runnable>& runnable) -> Timer* {
|
||||
assert(!are_clearing_);
|
||||
auto* t = new Timer(this, next_timer_id_++, current_time, length, offset,
|
||||
repeat_count);
|
||||
t->runnable_ = runnable;
|
||||
t = SubmitTimer(t);
|
||||
return t;
|
||||
}
|
||||
|
||||
auto TimerList::GetTimeToNextExpire(TimerMedium current_time) -> TimerMedium {
|
||||
assert(!are_clearing_);
|
||||
if (!timers_) {
|
||||
return (TimerMedium)-1;
|
||||
}
|
||||
TimerMedium diff = timers_->expire_time_ - current_time;
|
||||
return (diff < 0) ? 0 : diff;
|
||||
}
|
||||
|
||||
auto TimerList::GetTimer(int id) -> Timer* {
|
||||
assert(!are_clearing_);
|
||||
|
||||
assert(id != 0); // Zero denotes "no-id".
|
||||
Timer* t = PullTimer(id, false);
|
||||
return t->dead_ ? nullptr : t;
|
||||
}
|
||||
|
||||
void TimerList::DeleteTimer(int timer_id) {
|
||||
assert(timer_id != 0); // zero denotes "no-id"
|
||||
Timer* t = PullTimer(timer_id);
|
||||
if (t) {
|
||||
// If its the client timer, just mark it as dead, so the client can still
|
||||
// resubmit it without crashing.
|
||||
if (client_timer_ == t) {
|
||||
t->dead_ = true;
|
||||
} else {
|
||||
// Not in the client domain; kill it now.
|
||||
delete t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto TimerList::SubmitTimer(Timer* t) -> Timer* {
|
||||
assert(t->list_ == this);
|
||||
assert(t->initial_ || t == client_timer_ || t->dead_);
|
||||
|
||||
// Aside from initial timer submissions, only the one client timer should be
|
||||
// coming thru here.
|
||||
if (!t->initial_) {
|
||||
assert(client_timer_ == t);
|
||||
client_timer_ = nullptr;
|
||||
}
|
||||
|
||||
// If its a one-shot timer or is dead, kill it.
|
||||
if ((t->repeat_count_ == 0 && !t->initial_) || t->dead_) {
|
||||
delete t;
|
||||
return nullptr;
|
||||
} else {
|
||||
// Its still alive. Shove it back in line and tell it to keep working.
|
||||
if (!t->initial_ && t->repeat_count_ > 0) t->repeat_count_--;
|
||||
t->initial_ = false;
|
||||
|
||||
// No drift.
|
||||
if (explicit_bool(false)) {
|
||||
t->expire_time_ = t->expire_time_ + t->length_;
|
||||
} else {
|
||||
// Drift.
|
||||
t->expire_time_ = t->last_run_time_ + t->length_;
|
||||
}
|
||||
AddTimer(t);
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
void TimerList::AddTimer(Timer* t) {
|
||||
assert(t && !t->on_list_);
|
||||
|
||||
// If its set to never go off, throw it on the inactive list.
|
||||
if (t->length_ == -1) {
|
||||
t->next_ = timers_inactive_;
|
||||
timers_inactive_ = t;
|
||||
timer_count_inactive_++;
|
||||
} else {
|
||||
Timer** list = &timers_;
|
||||
|
||||
// Go along till we find an expire time later than ourself.
|
||||
while (*list != nullptr) {
|
||||
if ((*list)->expire_time_ > t->expire_time_) break;
|
||||
list = &((*list)->next_);
|
||||
}
|
||||
Timer* tmp = (*list);
|
||||
(*list) = t;
|
||||
t->next_ = tmp;
|
||||
timer_count_active_++;
|
||||
}
|
||||
t->on_list_ = true;
|
||||
}
|
||||
|
||||
} // namespace ballistica
|
||||
68
src/ballistica/generic/timer_list.h
Normal file
68
src/ballistica/generic/timer_list.h
Normal file
@ -0,0 +1,68 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_GENERIC_TIMER_LIST_H_
|
||||
#define BALLISTICA_GENERIC_TIMER_LIST_H_
|
||||
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
#include "ballistica/ballistica.h"
|
||||
#include "ballistica/core/object.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
class TimerList {
|
||||
public:
|
||||
TimerList();
|
||||
~TimerList();
|
||||
|
||||
// Run timers up to the provided target time.
|
||||
void Run(TimerMedium target_time);
|
||||
|
||||
// Create a timer with provided runnable.
|
||||
auto NewTimer(TimerMedium current_time, TimerMedium length,
|
||||
TimerMedium offset, int repeat_count,
|
||||
const Object::Ref<Runnable>& runnable) -> Timer*;
|
||||
|
||||
// Return a timer by its id, or nullptr if the timer no longer exists.
|
||||
auto GetTimer(int id) -> Timer*;
|
||||
|
||||
// Delete a currently-queued timer via its id.
|
||||
void DeleteTimer(int timer_id);
|
||||
|
||||
// Return the time until the next timer goes off.
|
||||
// If no timers are present, -1 is returned.
|
||||
auto GetTimeToNextExpire(TimerMedium current_time) -> TimerMedium;
|
||||
|
||||
// Return the active timer count. Note that this does not include the client
|
||||
// timer (a timer returned via getExpiredTimer() but not yet re-submitted).
|
||||
auto active_timer_count() const -> int { return timer_count_active_; }
|
||||
|
||||
auto empty() -> bool { return (timers_ == nullptr); }
|
||||
|
||||
void Clear();
|
||||
|
||||
private:
|
||||
// Returns the next expired timer. When done with the timer,
|
||||
// return it to the list with Timer::submit()
|
||||
// (this will either put it back in line or delete it)
|
||||
auto GetExpiredTimer(TimerMedium target_time) -> Timer*;
|
||||
auto GetExpiredCount(TimerMedium target_time) -> int;
|
||||
auto PullTimer(int timer_id, bool remove = true) -> Timer*;
|
||||
auto SubmitTimer(Timer* t) -> Timer*;
|
||||
void AddTimer(Timer* t);
|
||||
int timer_count_active_ = 0;
|
||||
int timer_count_inactive_ = 0;
|
||||
int timer_count_total_ = 0;
|
||||
Timer* client_timer_ = nullptr;
|
||||
Timer* timers_ = nullptr;
|
||||
Timer* timers_inactive_ = nullptr;
|
||||
int next_timer_id_ = 1;
|
||||
bool running_ = false;
|
||||
bool are_clearing_ = false;
|
||||
friend class Timer;
|
||||
};
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // BALLISTICA_GENERIC_TIMER_LIST_H_
|
||||
449
src/ballistica/generic/utf8.cc
Normal file
449
src/ballistica/generic/utf8.cc
Normal file
@ -0,0 +1,449 @@
|
||||
// Copyright (c) 2011-2020 Eric Froemling
|
||||
// Derived from code licensed as follows:
|
||||
|
||||
/*
|
||||
Basic UTF-8 manipulation routines
|
||||
by Jeff Bezanson
|
||||
placed in the public domain Fall 2005
|
||||
|
||||
This code is designed to provide the utilities you need to manipulate
|
||||
UTF-8 as an internal string encoding. These functions do not perform the
|
||||
error checking normally needed when handling UTF-8 data, so if you happen
|
||||
to be from the Unicode Consortium you will want to flay me alive.
|
||||
I do this because error checking can be performed at the boundaries (I/O),
|
||||
with these routines reserved for higher performance on data known to be
|
||||
valid.
|
||||
*/
|
||||
#include "ballistica/generic/utf8.h"
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#if _WIN32 || _WIN64
|
||||
#include <malloc.h>
|
||||
#else
|
||||
#include <alloca.h>
|
||||
#endif
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
// Should tidy this up but don't want to risk breaking anything for now.
|
||||
#pragma clang diagnostic push
|
||||
#pragma ide diagnostic ignored "hicpp-signed-bitwise"
|
||||
#pragma ide diagnostic ignored "bugprone-narrowing-conversions"
|
||||
|
||||
static const uint32_t offsetsFromUTF8[6] = {0x00000000UL, 0x00003080UL,
|
||||
0x000E2080UL, 0x03C82080UL,
|
||||
0xFA082080UL, 0x82082080UL};
|
||||
|
||||
static const char trailingBytesForUTF8[256] = {
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5};
|
||||
|
||||
/* returns length of next utf-8 sequence */
|
||||
auto u8_seqlen(const char* s) -> int {
|
||||
return trailingBytesForUTF8[(unsigned int)(unsigned char)s[0]] + 1;
|
||||
}
|
||||
|
||||
/* conversions without error checking
|
||||
only works for valid UTF-8, i.e. no 5- or 6-byte sequences
|
||||
srcsz = source size in bytes, or -1 if 0-terminated
|
||||
sz = dest size in # of wide characters
|
||||
|
||||
returns # characters converted
|
||||
dest will always be L'\0'-terminated, even if there isn't enough room
|
||||
for all the characters.
|
||||
if sz = srcsz+1 (i.e. 4*srcsz+4 bytes), there will always be enough space.
|
||||
*/
|
||||
auto u8_toucs(uint32_t* dest, int sz, const char* src, int srcsz) -> int {
|
||||
uint32_t ch;
|
||||
const char* src_end = src + srcsz;
|
||||
int nb;
|
||||
int i = 0;
|
||||
|
||||
while (i < sz - 1) {
|
||||
nb = trailingBytesForUTF8[(unsigned char)*src]; // NOLINT(cert-str34-c)
|
||||
if (srcsz == -1) {
|
||||
if (*src == 0) goto done_toucs;
|
||||
} else {
|
||||
if (src + nb >= src_end) goto done_toucs;
|
||||
}
|
||||
ch = 0;
|
||||
switch (nb) {
|
||||
/* these fall through deliberately */
|
||||
case 3: // NOLINT(bugprone-branch-clone)
|
||||
ch += (unsigned char)*src++;
|
||||
ch <<= 6;
|
||||
case 2:
|
||||
ch += (unsigned char)*src++;
|
||||
ch <<= 6;
|
||||
case 1:
|
||||
ch += (unsigned char)*src++;
|
||||
ch <<= 6;
|
||||
case 0:
|
||||
ch += (unsigned char)*src++;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
ch -= offsetsFromUTF8[nb];
|
||||
dest[i++] = ch;
|
||||
}
|
||||
done_toucs:
|
||||
dest[i] = 0;
|
||||
return i;
|
||||
}
|
||||
|
||||
/* srcsz = number of source characters, or -1 if 0-terminated
|
||||
sz = size of dest buffer in bytes
|
||||
|
||||
returns # characters converted
|
||||
dest will only be '\0'-terminated if there is enough space. this is
|
||||
for consistency; imagine there are 2 bytes of space left, but the next
|
||||
character requires 3 bytes. in this case we could NUL-terminate, but in
|
||||
general we can't when there's insufficient space. therefore this function
|
||||
only NUL-terminates if all the characters fit, and there's space for
|
||||
the NUL as well.
|
||||
the destination string will never be bigger than the source string.
|
||||
*/
|
||||
auto u8_toutf8(char* dest, int sz, const uint32_t* src, int srcsz) -> int {
|
||||
uint32_t ch;
|
||||
int i = 0;
|
||||
char* dest_end = dest + sz;
|
||||
|
||||
while (srcsz < 0 ? src[i] != 0 : i < srcsz) {
|
||||
ch = src[i];
|
||||
if (ch < 0x80) {
|
||||
if (dest >= dest_end) return i;
|
||||
*dest++ = (char)ch;
|
||||
} else if (ch < 0x800) {
|
||||
if (dest >= dest_end - 1) return i;
|
||||
*dest++ = static_cast<char>((ch >> 6) | 0xC0);
|
||||
*dest++ = static_cast<char>((ch & 0x3F) | 0x80);
|
||||
} else if (ch < 0x10000) {
|
||||
if (dest >= dest_end - 2) return i;
|
||||
*dest++ = static_cast<char>((ch >> 12) | 0xE0);
|
||||
*dest++ = static_cast<char>(((ch >> 6) & 0x3F) | 0x80);
|
||||
*dest++ = static_cast<char>((ch & 0x3F) | 0x80);
|
||||
} else if (ch < 0x110000) {
|
||||
if (dest >= dest_end - 3) return i;
|
||||
*dest++ = static_cast<char>((ch >> 18) | 0xF0);
|
||||
*dest++ = static_cast<char>(((ch >> 12) & 0x3F) | 0x80);
|
||||
*dest++ = static_cast<char>(((ch >> 6) & 0x3F) | 0x80);
|
||||
*dest++ = static_cast<char>((ch & 0x3F) | 0x80);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (dest < dest_end) *dest = '\0';
|
||||
return i;
|
||||
}
|
||||
|
||||
auto u8_wc_toutf8(char* dest, uint32_t ch) -> int {
|
||||
if (ch < 0x80) {
|
||||
dest[0] = (char)ch;
|
||||
return 1;
|
||||
}
|
||||
if (ch < 0x800) {
|
||||
dest[0] = static_cast<char>((ch >> 6) | 0xC0);
|
||||
dest[1] = static_cast<char>((ch & 0x3F) | 0x80);
|
||||
return 2;
|
||||
}
|
||||
if (ch < 0x10000) {
|
||||
dest[0] = static_cast<char>((ch >> 12) | 0xE0);
|
||||
dest[1] = static_cast<char>(((ch >> 6) & 0x3F) | 0x80);
|
||||
dest[2] = static_cast<char>((ch & 0x3F) | 0x80);
|
||||
return 3;
|
||||
}
|
||||
if (ch < 0x110000) {
|
||||
dest[0] = static_cast<char>((ch >> 18) | 0xF0);
|
||||
dest[1] = static_cast<char>(((ch >> 12) & 0x3F) | 0x80);
|
||||
dest[2] = static_cast<char>(((ch >> 6) & 0x3F) | 0x80);
|
||||
dest[3] = static_cast<char>((ch & 0x3F) | 0x80);
|
||||
return 4;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* charnum => byte offset */
|
||||
auto u8_offset(const char* str, int charnum) -> int {
|
||||
int offs = 0;
|
||||
|
||||
while (charnum > 0 && str[offs]) {
|
||||
(void)(isutf(str[++offs]) || isutf(str[++offs]) || isutf(str[++offs])
|
||||
|| ++offs);
|
||||
charnum--;
|
||||
}
|
||||
return offs;
|
||||
}
|
||||
|
||||
/* byte offset => charnum */
|
||||
auto u8_charnum(const char* s, int offset) -> int {
|
||||
int charnum = 0, offs = 0;
|
||||
|
||||
while (offs < offset && s[offs]) {
|
||||
(void)(isutf(s[++offs]) || isutf(s[++offs]) || isutf(s[++offs]) || ++offs);
|
||||
charnum++;
|
||||
}
|
||||
return charnum;
|
||||
}
|
||||
|
||||
/* number of characters */
|
||||
auto u8_strlen(const char* s) -> int {
|
||||
int count = 0;
|
||||
int i = 0;
|
||||
while (u8_nextchar(s, &i) != 0) {
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
auto u8_nextchar(const char* s, int* i) -> uint32_t {
|
||||
uint32_t ch = 0;
|
||||
size_t sz = 0;
|
||||
|
||||
do {
|
||||
ch <<= 6;
|
||||
ch += (unsigned char)s[(*i)];
|
||||
sz++;
|
||||
} while (s[*i] && (++(*i)) && !isutf(s[*i]));
|
||||
ch -= offsetsFromUTF8[sz - 1];
|
||||
|
||||
return ch;
|
||||
}
|
||||
|
||||
void u8_inc(const char* s, int* i) {
|
||||
(void)(isutf(s[++(*i)]) || isutf(s[++(*i)]) || isutf(s[++(*i)]) || ++(*i));
|
||||
}
|
||||
|
||||
void u8_dec(const char* s, int* i) {
|
||||
(void)(isutf(s[--(*i)]) || isutf(s[--(*i)]) || isutf(s[--(*i)]) || --(*i));
|
||||
}
|
||||
|
||||
auto octal_digit(char c) -> int { return (c >= '0' && c <= '7'); }
|
||||
|
||||
auto hex_digit(char c) -> int {
|
||||
return ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')
|
||||
|| (c >= 'a' && c <= 'f'));
|
||||
}
|
||||
|
||||
/* assumes that src points to the character after a backslash
|
||||
returns number of input characters processed */
|
||||
auto u8_read_escape_sequence(char* str, uint32_t* dest) -> int {
|
||||
uint32_t ch;
|
||||
char digs[9] = "\0\0\0\0\0\0\0\0";
|
||||
int dno = 0, i = 1;
|
||||
|
||||
ch = (uint32_t)str[0]; /* take literal character */ // NOLINT(cert-str34-c)
|
||||
if (str[0] == 'n')
|
||||
ch = L'\n';
|
||||
else if (str[0] == 't')
|
||||
ch = L'\t';
|
||||
else if (str[0] == 'r')
|
||||
ch = L'\r';
|
||||
else if (str[0] == 'b')
|
||||
ch = L'\b';
|
||||
else if (str[0] == 'f')
|
||||
ch = L'\f';
|
||||
else if (str[0] == 'v')
|
||||
ch = L'\v';
|
||||
else if (str[0] == 'a')
|
||||
ch = L'\a';
|
||||
else if (octal_digit(str[0])) {
|
||||
i = 0;
|
||||
do {
|
||||
digs[dno++] = str[i++];
|
||||
} while (octal_digit(str[i]) && dno < 3);
|
||||
ch = static_cast<uint32_t>(strtol(digs, nullptr, 8));
|
||||
} else if (str[0] == 'x') {
|
||||
while (hex_digit(str[i]) && dno < 2) {
|
||||
digs[dno++] = str[i++];
|
||||
}
|
||||
if (dno > 0) ch = static_cast<uint32_t>(strtol(digs, nullptr, 16));
|
||||
} else if (str[0] == 'u') {
|
||||
while (hex_digit(str[i]) && dno < 4) {
|
||||
digs[dno++] = str[i++];
|
||||
}
|
||||
if (dno > 0) ch = static_cast<uint32_t>(strtol(digs, nullptr, 16));
|
||||
} else if (str[0] == 'U') {
|
||||
while (hex_digit(str[i]) && dno < 8) {
|
||||
digs[dno++] = str[i++];
|
||||
}
|
||||
if (dno > 0) ch = static_cast<uint32_t>(strtol(digs, nullptr, 16));
|
||||
}
|
||||
*dest = ch;
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
/* convert a string with literal \uxxxx or \Uxxxxxxxx characters to UTF-8
|
||||
example: u8_unescape(mybuf, 256, "hello\\u220e")
|
||||
note the double backslash is needed if called on a C string literal */
|
||||
auto u8_unescape(char* buf, int sz, char* src) -> int {
|
||||
int c = 0, amt;
|
||||
uint32_t ch;
|
||||
char temp[4];
|
||||
|
||||
while (*src && c < sz) {
|
||||
if (*src == '\\') {
|
||||
src++;
|
||||
amt = u8_read_escape_sequence(src, &ch);
|
||||
} else {
|
||||
ch = (uint32_t)*src; // NOLINT(cert-str34-c)
|
||||
amt = 1;
|
||||
}
|
||||
src += amt;
|
||||
amt = u8_wc_toutf8(temp, ch);
|
||||
if (amt > sz - c) break;
|
||||
memcpy(&buf[c], temp, static_cast<size_t>(amt));
|
||||
c += amt;
|
||||
}
|
||||
if (c < sz) buf[c] = '\0';
|
||||
return c;
|
||||
}
|
||||
|
||||
auto u8_escape_wchar(char* buf, int sz, uint32_t ch) -> int {
|
||||
if (ch == L'\n')
|
||||
return snprintf(buf, static_cast<size_t>(sz), "\\n");
|
||||
else if (ch == L'\t')
|
||||
return snprintf(buf, static_cast<size_t>(sz), "\\t");
|
||||
else if (ch == L'\r')
|
||||
return snprintf(buf, static_cast<size_t>(sz), "\\r");
|
||||
else if (ch == L'\b')
|
||||
return snprintf(buf, static_cast<size_t>(sz), "\\b");
|
||||
else if (ch == L'\f')
|
||||
return snprintf(buf, static_cast<size_t>(sz), "\\f");
|
||||
else if (ch == L'\v')
|
||||
return snprintf(buf, static_cast<size_t>(sz), "\\v");
|
||||
else if (ch == L'\a')
|
||||
return snprintf(buf, static_cast<size_t>(sz), "\\a");
|
||||
else if (ch == L'\\')
|
||||
return snprintf(buf, static_cast<size_t>(sz), "\\\\");
|
||||
else if (ch < 32 || ch == 0x7f)
|
||||
return snprintf(buf, static_cast<size_t>(sz), "\\x%hhX", (unsigned char)ch);
|
||||
else if (ch > 0xFFFF)
|
||||
return snprintf(buf, static_cast<size_t>(sz), "\\U%.8X", (uint32_t)ch);
|
||||
else if (ch >= 0x80 && ch <= 0xFFFF)
|
||||
return snprintf(buf, static_cast<size_t>(sz), "\\u%.4hX",
|
||||
(unsigned short)ch);
|
||||
|
||||
return snprintf(buf, static_cast<size_t>(sz), "%c", (char)ch);
|
||||
}
|
||||
|
||||
auto u8_escape(char* buf, int sz, char* src, int escape_quotes) -> int {
|
||||
int c = 0, i = 0, amt;
|
||||
|
||||
while (src[i] && c < sz) {
|
||||
if (escape_quotes && src[i] == '"') {
|
||||
amt = snprintf(buf, static_cast<size_t>(sz - c), "\\\"");
|
||||
i++;
|
||||
} else {
|
||||
amt = u8_escape_wchar(buf, sz - c, u8_nextchar(src, &i));
|
||||
}
|
||||
c += amt;
|
||||
buf += amt;
|
||||
}
|
||||
if (c < sz) *buf = '\0';
|
||||
return c;
|
||||
}
|
||||
|
||||
auto u8_strchr(char* s, uint32_t ch, int* charn) -> char* {
|
||||
int i = 0, lasti = 0;
|
||||
uint32_t c;
|
||||
|
||||
*charn = 0;
|
||||
while (s[i]) {
|
||||
c = u8_nextchar(s, &i);
|
||||
if (c == ch) {
|
||||
return &s[lasti];
|
||||
}
|
||||
lasti = i;
|
||||
(*charn)++;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto u8_memchr(char* s, uint32_t ch, size_t sz, int* charn) -> char* {
|
||||
size_t i = 0, lasti = 0;
|
||||
uint32_t c;
|
||||
int csz;
|
||||
|
||||
*charn = 0;
|
||||
while (i < sz) {
|
||||
c = static_cast<uint32_t>(csz = 0);
|
||||
do {
|
||||
c <<= 6;
|
||||
c += (unsigned char)s[i++];
|
||||
csz++;
|
||||
} while (i < sz && !isutf(s[i]));
|
||||
c -= offsetsFromUTF8[csz - 1];
|
||||
|
||||
if (c == ch) {
|
||||
return &s[lasti];
|
||||
}
|
||||
lasti = i;
|
||||
(*charn)++;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto u8_is_locale_utf8(const char* locale) -> int {
|
||||
/* this code based on libutf8 */
|
||||
const char* cp = locale;
|
||||
|
||||
for (; *cp != '\0' && *cp != '@' && *cp != '+' && *cp != ','; cp++) {
|
||||
if (*cp == '.') {
|
||||
const char* encoding = ++cp;
|
||||
for (; *cp != '\0' && *cp != '@' && *cp != '+' && *cp != ','; cp++)
|
||||
;
|
||||
if ((cp - encoding == 5 && !strncmp(encoding, "UTF-8", 5))
|
||||
|| (cp - encoding == 4 && !strncmp(encoding, "utf8", 4)))
|
||||
return 1; /* it's UTF-8 */
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto u8_vprintf(char* fmt, va_list ap) -> int {
|
||||
char* buf;
|
||||
uint32_t* wcs;
|
||||
|
||||
int sz{512};
|
||||
buf = (char*)alloca(sz);
|
||||
try_print:
|
||||
int cnt = vsnprintf(buf, static_cast<size_t>(sz), fmt, ap);
|
||||
if (cnt >= sz) {
|
||||
buf = (char*)alloca(cnt - sz + 1);
|
||||
sz = cnt + 1;
|
||||
goto try_print;
|
||||
}
|
||||
wcs = (uint32_t*)alloca((cnt + 1) * sizeof(uint32_t));
|
||||
cnt = u8_toucs(wcs, cnt + 1, buf, cnt);
|
||||
printf("%ls", (wchar_t*)wcs);
|
||||
return cnt;
|
||||
}
|
||||
|
||||
auto u8_printf(char* fmt, ...) -> int {
|
||||
int cnt;
|
||||
va_list args;
|
||||
|
||||
va_start(args, fmt);
|
||||
|
||||
cnt = u8_vprintf(fmt, args);
|
||||
|
||||
va_end(args);
|
||||
return cnt;
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
} // namespace ballistica
|
||||
85
src/ballistica/generic/utf8.h
Normal file
85
src/ballistica/generic/utf8.h
Normal file
@ -0,0 +1,85 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_GENERIC_UTF8_H_
|
||||
#define BALLISTICA_GENERIC_UTF8_H_
|
||||
|
||||
#include <cstdarg>
|
||||
|
||||
#include "ballistica/ballistica.h"
|
||||
|
||||
// ericf note: i think this is cutef8?...
|
||||
namespace ballistica {
|
||||
|
||||
/* is c the start of a utf8 sequence? */
|
||||
#define isutf(c) (((c)&0xC0) != 0x80)
|
||||
|
||||
/* convert UTF-8 data to wide character */
|
||||
auto u8_toucs(uint32_t* dest, int sz, const char* src, int srcsz) -> int;
|
||||
|
||||
/* the opposite conversion */
|
||||
auto u8_toutf8(char* dest, int sz, const uint32_t* src, int srcsz) -> int;
|
||||
|
||||
/* single character to UTF-8 */
|
||||
auto u8_wc_toutf8(char* dest, uint32_t ch) -> int;
|
||||
|
||||
/* character number to byte offset */
|
||||
auto u8_offset(const char* str, int charnum) -> int;
|
||||
|
||||
/* byte offset to character number */
|
||||
auto u8_charnum(const char* s, int offset) -> int;
|
||||
|
||||
/* return next character, updating an index variable */
|
||||
auto u8_nextchar(const char* s, int* i) -> uint32_t;
|
||||
|
||||
/* move to next character */
|
||||
void u8_inc(const char* s, int* i);
|
||||
|
||||
/* move to previous character */
|
||||
void u8_dec(const char* s, int* i);
|
||||
|
||||
/* returns length of next utf-8 sequence */
|
||||
auto u8_seqlen(const char* s) -> int;
|
||||
|
||||
/* assuming src points to the character after a backslash, read an
|
||||
escape sequence, storing the result in dest and returning the number of
|
||||
input characters processed */
|
||||
auto u8_read_escape_sequence(char* src, uint32_t* dest) -> int;
|
||||
|
||||
/* given a wide character, convert it to an ASCII escape sequence stored in
|
||||
buf, where buf is "sz" bytes. returns the number of characters output. */
|
||||
auto u8_escape_wchar(char* buf, int sz, uint32_t ch) -> int;
|
||||
|
||||
/* convert a string "src" containing escape sequences to UTF-8 */
|
||||
auto u8_unescape(char* buf, int sz, char* src) -> int;
|
||||
|
||||
/* convert UTF-8 "src" to ASCII with escape sequences.
|
||||
if escape_quotes is nonzero, quote characters will be preceded by
|
||||
backslashes as well. */
|
||||
auto u8_escape(char* buf, int sz, char* src, int escape_quotes) -> int;
|
||||
|
||||
/* utility predicates used by the above */
|
||||
auto octal_digit(char c) -> int;
|
||||
auto hex_digit(char c) -> int;
|
||||
|
||||
/* return a pointer to the first occurrence of ch in s, or NULL if not
|
||||
found. character index of found character returned in *charn. */
|
||||
auto u8_strchr(char* s, uint32_t ch, int* charn) -> char*;
|
||||
|
||||
/* same as the above, but searches a buffer of a given size instead of
|
||||
a NUL-terminated string. */
|
||||
auto u8_memchr(char* s, uint32_t ch, size_t sz, int* charn) -> char*;
|
||||
|
||||
/* count the number of characters in a UTF-8 string */
|
||||
auto u8_strlen(const char* s) -> int;
|
||||
|
||||
auto u8_is_locale_utf8(const char* locale) -> int;
|
||||
|
||||
/* printf where the format string and arguments may be in UTF-8.
|
||||
you can avoid this function and just use ordinary printf() if the current
|
||||
locale is UTF-8. */
|
||||
auto u8_vprintf(char* fmt, va_list ap) -> int;
|
||||
auto u8_printf(char* fmt, ...) -> int;
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // BALLISTICA_GENERIC_UTF8_H_
|
||||
639
src/ballistica/generic/utils.cc
Normal file
639
src/ballistica/generic/utils.cc
Normal file
@ -0,0 +1,639 @@
|
||||
// Copyright (c) 2011-2020 Eric Froemling
|
||||
|
||||
#include "ballistica/generic/utils.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "ballistica/app/app_globals.h"
|
||||
#include "ballistica/generic/base64.h"
|
||||
#include "ballistica/generic/huffman.h"
|
||||
#include "ballistica/generic/json.h"
|
||||
#include "ballistica/generic/utf8.h"
|
||||
#include "ballistica/math/vector3f.h"
|
||||
#include "ballistica/platform/platform.h"
|
||||
#include "ballistica/scene/scene.h"
|
||||
|
||||
// FIXME: Cleaner to add the lib to the project(s) instead?
|
||||
#if BA_OSTYPE_WINDOWS
|
||||
#pragma comment(lib, "Ws2_32.lib")
|
||||
#endif
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
#define USE_BAKED_RANDS 1
|
||||
|
||||
#if BA_OSTYPE_WINDOWS
|
||||
#endif
|
||||
|
||||
#if USE_BAKED_RANDS
|
||||
float Utils::precalc_rands_1[kPrecalcRandsCount] = {
|
||||
0.00424972f, 0.0470216f, 0.545227f, 0.538243f, 0.214183f, 0.627205f,
|
||||
0.194698f, 0.917583f, 0.468622f, 0.0779965f, 0.304211f, 0.773231f,
|
||||
0.522742f, 0.378898f, 0.404598f, 0.468434f, 0.081512f, 0.408348f,
|
||||
0.0808838f, 0.427364f, 0.226629f, 0.234887f, 0.516467f, 0.0457478f,
|
||||
0.455418f, 0.194083f, 0.502244f, 0.0733989f, 0.458193f, 0.898715f,
|
||||
0.624819f, 0.70762f, 0.759858f, 0.559276f, 0.956318f, 0.408562f,
|
||||
0.206264f, 0.322909f, 0.293165f, 0.524073f, 0.407753f, 0.961242f,
|
||||
0.278234f, 0.423968f, 0.631937f, 0.534858f, 0.842336f, 0.786993f,
|
||||
0.934668f, 0.739984f, 0.968577f, 0.468159f, 0.804702f, 0.0686368f,
|
||||
0.397594f, 0.60871f, 0.485322f, 0.907066f, 0.587516f, 0.364387f,
|
||||
0.791611f, 0.899199f, 0.0186556f, 0.446891f, 0.0138f, 0.999024f,
|
||||
0.556364f, 0.29821f, 0.23943f, 0.338024f, 0.157135f, 0.25299f,
|
||||
0.791138f, 0.367175f, 0.584245f, 0.496136f, 0.358228f, 0.280143f,
|
||||
0.538658f, 0.190721f, 0.656737f, 0.010905f, 0.520343f, 0.678249f,
|
||||
0.930145f, 0.823978f, 0.457201f, 0.988418f, 0.854635f, 0.955912f,
|
||||
0.0226999f, 0.183605f, 0.838141f, 0.210646f, 0.160344f, 0.111269f,
|
||||
0.348488f, 0.648031f, 0.844362f, 0.65157f, 0.0598469f, 0.952439f,
|
||||
0.265193f, 0.768256f, 0.773861f, 0.723251f, 0.53157f, 0.36183f,
|
||||
0.485393f, 0.348683f, 0.551617f, 0.648207f, 0.656125f, 0.879799f,
|
||||
0.0674501f, 0.000782927f, 0.607129f, 0.116035f, 0.67095f, 0.692934f,
|
||||
0.276618f, 0.137535f, 0.771033f, 0.278625f, 0.686023f, 0.873823f,
|
||||
0.254666f, 0.75378f};
|
||||
float Utils::precalc_rands_2[kPrecalcRandsCount] = {
|
||||
0.425019f, 0.29261f, 0.623541f, 0.241628f, 0.772656f, 0.434116f,
|
||||
0.295335f, 0.814317f, 0.122326f, 0.887651f, 0.873536f, 0.692463f,
|
||||
0.730894f, 0.142115f, 0.0722184f, 0.977652f, 0.971393f, 0.111517f,
|
||||
0.41341f, 0.699999f, 0.955932f, 0.746667f, 0.267962f, 0.883952f,
|
||||
0.202871f, 0.952115f, 0.221069f, 0.616162f, 0.842076f, 0.705628f,
|
||||
0.332754f, 0.974675f, 0.940277f, 0.756059f, 0.831943f, 0.70631f,
|
||||
0.674705f, 0.13903f, 0.22751f, 0.0875125f, 0.101364f, 0.593826f,
|
||||
0.271567f, 0.63593f, 0.970994f, 0.359381f, 0.147583f, 0.987353f,
|
||||
0.960315f, 0.904639f, 0.874661f, 0.352573f, 0.630782f, 0.578075f,
|
||||
0.364932f, 0.588095f, 0.799978f, 0.0502811f, 0.379093f, 0.252171f,
|
||||
0.598992f, 0.843808f, 0.544584f, 0.895444f, 0.935885f, 0.592526f,
|
||||
0.810681f, 0.0200064f, 0.0986983f, 0.164623f, 0.975185f, 0.0102097f,
|
||||
0.648763f, 0.114897f, 0.400273f, 0.549732f, 0.732205f, 0.363931f,
|
||||
0.223837f, 0.4427f, 0.770981f, 0.280827f, 0.407232f, 0.323108f,
|
||||
0.9429f, 0.594368f, 0.175995f, 0.34f, 0.857507f, 0.016013f,
|
||||
0.516969f, 0.847756f, 0.638805f, 0.324338f, 0.897038f, 0.0950314f,
|
||||
0.0460401f, 0.449791f, 0.189096f, 0.931966f, 0.846644f, 0.64728f,
|
||||
0.096389f, 0.075902f, 0.27798f, 0.673576f, 0.102553f, 0.275159f,
|
||||
0.00170948f, 0.319388f, 0.0328678f, 0.411649f, 0.496922f, 0.778794f,
|
||||
0.634341f, 0.158655f, 0.0157559f, 0.195268f, 0.663882f, 0.148622f,
|
||||
0.118159f, 0.552174f, 0.757064f, 0.854851f, 0.991449f, 0.349681f,
|
||||
0.17858f, 0.774876f};
|
||||
float Utils::precalc_rands_3[kPrecalcRandsCount] = {
|
||||
0.29369f, 0.894838f, 0.857948f, 0.04309f, 0.0296678f, 0.180115f,
|
||||
0.694884f, 0.227017f, 0.936936f, 0.746493f, 0.511976f, 0.231185f,
|
||||
0.1333f, 0.524805f, 0.774586f, 0.395971f, 0.206664f, 0.274414f,
|
||||
0.178939f, 0.88643f, 0.346536f, 0.22934f, 0.635988f, 0.589186f,
|
||||
0.652835f, 0.195603f, 0.504794f, 0.831229f, 0.769911f, 0.494712f,
|
||||
0.60128f, 0.367987f, 0.239279f, 0.0791311f, 0.469948f, 0.948189f,
|
||||
0.760893f, 0.670452f, 0.753765f, 0.822003f, 0.628783f, 0.432039f,
|
||||
0.226478f, 0.0678665f, 0.497384f, 0.110421f, 0.428975f, 0.446298f,
|
||||
0.00813589f, 0.2634f, 0.434728f, 0.693152f, 0.547276f, 0.702469f,
|
||||
0.407723f, 0.11742f, 0.235373f, 0.0738137f, 0.410148f, 0.231855f,
|
||||
0.256911f, 0.879873f, 0.818198f, 0.73404f, 0.423038f, 0.577114f,
|
||||
0.116636f, 0.247292f, 0.822178f, 0.817466f, 0.940992f, 0.593788f,
|
||||
0.751732f, 0.0681611f, 0.38832f, 0.352672f, 0.174289f, 0.582884f,
|
||||
0.0338663f, 0.460085f, 0.869757f, 0.854794f, 0.35513f, 0.477297f,
|
||||
0.31343f, 0.545157f, 0.943892f, 0.383522f, 0.121732f, 0.131018f,
|
||||
0.690497f, 0.231025f, 0.395681f, 0.144711f, 0.521456f, 0.192024f,
|
||||
0.796611f, 0.64258f, 0.13998f, 0.560008f, 0.549709f, 0.831634f,
|
||||
0.010101f, 0.684939f, 0.00884889f, 0.796426f, 0.603282f, 0.591985f,
|
||||
0.731204f, 0.950351f, 0.408559f, 0.592352f, 0.76991f, 0.196648f,
|
||||
0.376926f, 0.508574f, 0.809908f, 0.862359f, 0.863431f, 0.884588f,
|
||||
0.895885f, 0.391311f, 0.976098f, 0.473118f, 0.286659f, 0.0946781f,
|
||||
0.402437f, 0.347471f};
|
||||
#else // USE_BAKED_RANDS
|
||||
float Utils::precalc_rands_1[kPrecalcRandsCount];
|
||||
float Utils::precalc_rands_2[kPrecalcRandsCount];
|
||||
float Utils::precalc_rands_3[kPrecalcRandsCount];
|
||||
#endif // USE_BAKED_RANDS
|
||||
|
||||
Utils::Utils() {
|
||||
// Is this gonna be consistent cross-platform?... :-/
|
||||
srand(543); // NOLINT
|
||||
|
||||
// Test our static-type-name functionality.
|
||||
// This code runs at compile time and extracts human readable type names using
|
||||
// __PRETTY_FUNCTION__ type functionality. However, it is dependent on
|
||||
// specific compiler output and so could break easily if anything changes.
|
||||
// Here we add some compile-time checks to alert us if that happens.
|
||||
|
||||
// Remember that results can vary per compiler; make sure we match
|
||||
// one of the expected formats.
|
||||
static_assert(static_type_name_constexpr<decltype(g_app_globals)>()
|
||||
== "ballistica::AppGlobals *"
|
||||
|| static_type_name_constexpr<decltype(g_app_globals)>()
|
||||
== "ballistica::AppGlobals*"
|
||||
|| static_type_name_constexpr<decltype(g_app_globals)>()
|
||||
== "class ballistica::AppGlobals*");
|
||||
Object::Ref<Node> testnode{};
|
||||
static_assert(
|
||||
static_type_name_constexpr<decltype(testnode)>()
|
||||
== "ballistica::Object::Ref<ballistica::Node>"
|
||||
|| static_type_name_constexpr<decltype(testnode)>()
|
||||
== "class ballistica::Object::Ref<class ballistica::Node>");
|
||||
|
||||
// int testint{};
|
||||
// static_assert(static_type_name_constexpr<decltype(testint)>() == "int");
|
||||
|
||||
// If anything above breaks, enable this code to debug/fix it.
|
||||
// This will print a calculated type name as well as the full string
|
||||
// it was parsed from. Use this to adjust the filtering as necessary so
|
||||
// the resulting type name matches what is expected.
|
||||
if (explicit_bool(false)) {
|
||||
Log("static_type_name check; name is '"
|
||||
+ static_type_name<decltype(testnode)>() + "' debug_full is '"
|
||||
+ static_type_name<decltype(testnode)>(true) + "'");
|
||||
}
|
||||
|
||||
// We now bake these in so they match across platforms...
|
||||
#if USE_BAKED_RANDS
|
||||
#else
|
||||
// set up our precalculated rand vals
|
||||
for (int i = 0; i < kPrecalcRandsCount; i++) {
|
||||
precalc_rands_1[i] = static_cast<float>(rand()) / RAND_MAX; // NOLINT
|
||||
precalc_rands_2[i] = static_cast<float>(rand()) / RAND_MAX; // NOLINT
|
||||
precalc_rands_3[i] = static_cast<float>(rand()) / RAND_MAX; // NOLINT
|
||||
}
|
||||
#endif
|
||||
huffman_ = std::make_unique<Huffman>();
|
||||
}
|
||||
|
||||
Utils::~Utils() = default;
|
||||
|
||||
auto Utils::StringReplaceOne(std::string* target, const std::string& key,
|
||||
const std::string& replacement) -> bool {
|
||||
assert(target != nullptr);
|
||||
size_t pos = target->find(key);
|
||||
if (pos != std::string::npos) {
|
||||
target->replace(pos, key.size(), replacement);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// from https://stackoverflow.com/questions/5343190/
|
||||
// how-do-i-replace-all-instances-of-a-string-with-another-string/14678800
|
||||
auto Utils::StringReplaceAll(std::string* target, const std::string& key,
|
||||
const std::string& replacement) -> void {
|
||||
assert(target != nullptr);
|
||||
if (key.empty()) {
|
||||
return;
|
||||
}
|
||||
std::string ws_ret;
|
||||
ws_ret.reserve(target->length());
|
||||
size_t start_pos = 0, pos;
|
||||
while ((pos = target->find(key, start_pos)) != std::string::npos) {
|
||||
ws_ret += target->substr(start_pos, pos - start_pos);
|
||||
ws_ret += replacement;
|
||||
pos += key.length();
|
||||
start_pos = pos;
|
||||
}
|
||||
ws_ret += target->substr(start_pos);
|
||||
target->swap(ws_ret); // faster than str = ws_ret;
|
||||
}
|
||||
|
||||
auto Utils::IsValidUTF8(const std::string& val) -> bool {
|
||||
std::string out = Utils::GetValidUTF8(val.c_str(), "bsivu8");
|
||||
return (out == val);
|
||||
}
|
||||
|
||||
static auto utf8_check_is_valid(const std::string& string) -> bool {
|
||||
int c, i, ix, n, j;
|
||||
for (i = 0, ix = static_cast<int>(string.length()); i < ix; i++) {
|
||||
c = (unsigned char)string[i];
|
||||
// if (c==0x09 || c==0x0a || c==0x0d
|
||||
// || (0x20 <= c && c <= 0x7e) ) n = 0; // is_printable_ascii
|
||||
if (0x00 <= c && c <= 0x7f) {
|
||||
n = 0; // 0bbbbbbb
|
||||
} else if ((c & 0xE0) == 0xC0) { // NOLINT
|
||||
n = 1; // 110bbbbb
|
||||
} else if (c == 0xed && i < (ix - 1)
|
||||
&& ((unsigned char)string[i + 1] & 0xa0) == 0xa0) { // NOLINT
|
||||
return false; // U+d800 to U+dfff
|
||||
} else if ((c & 0xF0) == 0xE0) { // NOLINT
|
||||
n = 2; // 1110bbbb
|
||||
} else if ((c & 0xF8) == 0xF0) { // NOLINT
|
||||
n = 3; // 11110bbb
|
||||
} else {
|
||||
// else if (($c & 0xFC) == 0xF8)
|
||||
// n=4; // 111110bb //byte 5, unnecessary in 4 byte UTF-8
|
||||
// else if (($c & 0xFE) == 0xFC)
|
||||
// n=5; // 1111110b //byte 6, unnecessary in 4 byte UTF-8
|
||||
|
||||
return false;
|
||||
}
|
||||
for (j = 0; j < n && i < ix; j++) { // n bytes matching 10bbbbbb follow ?
|
||||
// NOLINTNEXTLINE
|
||||
if ((++i == ix) || (((unsigned char)string[i] & 0xC0) != 0x80)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// added by ericf from http://stackoverflow.com/questions/17316506/
|
||||
// strip-invalid-utf8-from-string-in-c-c
|
||||
// static std::string correct_non_utf_8(std::string *str) {
|
||||
auto Utils::GetValidUTF8(const char* str, const char* loc) -> std::string {
|
||||
int i, f_size = static_cast<int>(strlen(str));
|
||||
unsigned char c, c2 = 0, c3, c4;
|
||||
std::string to;
|
||||
to.reserve(static_cast<size_t>(f_size));
|
||||
|
||||
// ok, it seems we're somehow letting some funky utf8 through that's
|
||||
// causing crashes.. for now lets try this all-or-nothing func and return
|
||||
// ascii only if it fails
|
||||
if (!utf8_check_is_valid(str)) {
|
||||
// now strip out anything but normal ascii...
|
||||
for (i = 0; i < f_size; i++) {
|
||||
c = (unsigned char)(str)[i];
|
||||
if (c < 127) { // normal ASCII
|
||||
to.append(1, c);
|
||||
}
|
||||
}
|
||||
|
||||
// phone home a few times for bad strings
|
||||
static int logged_count = 0;
|
||||
if (logged_count < 10) {
|
||||
std::string log_str;
|
||||
for (i = 0; i < f_size; i++) {
|
||||
c = (unsigned char)(str)[i];
|
||||
log_str += std::to_string(static_cast<int>(c));
|
||||
if (i + 1 < f_size) {
|
||||
log_str += ',';
|
||||
}
|
||||
}
|
||||
logged_count++;
|
||||
Log("GOT INVALID UTF8 SEQUENCE: (" + log_str + "); RETURNING '" + to
|
||||
+ "'; LOC '" + loc + "'");
|
||||
}
|
||||
|
||||
} else {
|
||||
for (i = 0; i < f_size; i++) {
|
||||
c = (unsigned char)(str)[i];
|
||||
if (c < 32) { // control char
|
||||
if (c == 9 || c == 10 || c == 13) { // allow only \t \n \r
|
||||
to.append(1, c);
|
||||
}
|
||||
continue;
|
||||
} else if (c < 127) { // normal ASCII
|
||||
to.append(1, c);
|
||||
continue;
|
||||
} else if (c < 160) {
|
||||
// control char (nothing should be defined here either
|
||||
// ASCI, ISO_8859-1 or UTF8, so skipping)
|
||||
if (c2 == 128) { // fix microsoft mess, add euro
|
||||
to.append(1, (unsigned char)(226));
|
||||
to.append(1, (unsigned char)(130));
|
||||
to.append(1, (unsigned char)(172));
|
||||
}
|
||||
if (c2 == 133) { // fix IBM mess, add NEL = \n\r
|
||||
to.append(1, 10);
|
||||
to.append(1, 13);
|
||||
}
|
||||
continue;
|
||||
} else if (c < 192) { // invalid for UTF8, converting ASCII
|
||||
to.append(1, (unsigned char)194);
|
||||
to.append(1, c);
|
||||
continue;
|
||||
} else if (c < 194) { // invalid for UTF8, converting ASCII
|
||||
to.append(1, (unsigned char)195);
|
||||
to.append(1, c - 64);
|
||||
continue;
|
||||
} else if (c < 224 && i + 1 < f_size) { // possibly 2byte UTF8
|
||||
c2 = (unsigned char)(str)[i + 1];
|
||||
if (c2 > 127 && c2 < 192) { // valid 2byte UTF8
|
||||
if (c == 194 && c2 < 160) { // control char, skipping
|
||||
} else {
|
||||
to.append(1, c);
|
||||
to.append(1, c2);
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
} else if (c < 240 && i + 2 < f_size) { // possibly 3byte UTF8
|
||||
c2 = (unsigned char)(str)[i + 1];
|
||||
c3 = (unsigned char)(str)[i + 2];
|
||||
if (c2 > 127 && c2 < 192 && c3 > 127 && c3 < 192) { // valid 3byte UTF8
|
||||
to.append(1, c);
|
||||
to.append(1, c2);
|
||||
to.append(1, c3);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
} else if (c < 245 && i + 3 < f_size) { // possibly 4byte UTF8
|
||||
c2 = (unsigned char)(str)[i + 1];
|
||||
c3 = (unsigned char)(str)[i + 2];
|
||||
c4 = (unsigned char)(str)[i + 3];
|
||||
if (c2 > 127 && c2 < 192 && c3 > 127 && c3 < 192 && c4 > 127
|
||||
&& c4 < 192) {
|
||||
// valid 4byte UTF8
|
||||
to.append(1, c);
|
||||
to.append(1, c2);
|
||||
to.append(1, c3);
|
||||
to.append(1, c4);
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// invalid UTF8, converting ASCII
|
||||
// (c>245 || string too short for multi-byte))
|
||||
to.append(1, (unsigned char)195);
|
||||
to.append(1, c - 64);
|
||||
}
|
||||
}
|
||||
return to;
|
||||
}
|
||||
|
||||
auto Utils::UTF8StringLength(const char* val) -> int {
|
||||
std::string valid_str = GetValidUTF8(val, "gusl1");
|
||||
return u8_strlen(valid_str.c_str());
|
||||
}
|
||||
|
||||
auto Utils::GetUTF8Value(const char* c) -> uint32_t {
|
||||
int offset = 0;
|
||||
uint32_t val = u8_nextchar(c, &offset);
|
||||
|
||||
// Hack: allow showing euro even if we don't support unicode font rendering.
|
||||
if (!g_buildconfig.enable_os_font_rendering()) {
|
||||
if (val == 8364) {
|
||||
val = 0xE000;
|
||||
}
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
auto Utils::UTF8FromUnicode(std::vector<uint32_t> unichars) -> std::string {
|
||||
int buffer_size = static_cast<int>(unichars.size() * 4 + 1);
|
||||
// at most 4 chars per unichar plus ending zero
|
||||
std::vector<char> buffer(static_cast<size_t>(buffer_size));
|
||||
int len = u8_toutf8(buffer.data(), buffer_size, unichars.data(),
|
||||
static_cast<int>(unichars.size()));
|
||||
assert(len == unichars.size());
|
||||
buffer.resize(strlen(buffer.data()) + 1);
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
auto Utils::UnicodeFromUTF8(const std::string& s_in, const char* loc)
|
||||
-> std::vector<uint32_t> {
|
||||
std::string s = GetValidUTF8(s_in.c_str(), loc);
|
||||
// worst case every char is a character (plus trailing 0)
|
||||
std::vector<uint32_t> vals(s.size() + 1);
|
||||
int converted = u8_toucs(&vals[0], static_cast<int>(vals.size()), s.c_str(),
|
||||
static_cast<int>(s.size()));
|
||||
vals.resize(static_cast<size_t>(converted));
|
||||
return vals;
|
||||
}
|
||||
|
||||
auto Utils::UTF8FromUnicodeChar(uint32_t c) -> std::string {
|
||||
char buffer[10];
|
||||
u8_toutf8(buffer, sizeof(buffer), &c, 1);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
void Utils::AdvanceUTF8(const char** c) {
|
||||
int offset = 0;
|
||||
u8_nextchar(*c, &offset);
|
||||
*c += offset;
|
||||
}
|
||||
|
||||
auto Utils::GetJSONString(const char* s) -> std::string {
|
||||
std::string str;
|
||||
cJSON* str_obj = cJSON_CreateString(s);
|
||||
char* str_buffer = cJSON_PrintUnformatted(str_obj);
|
||||
str = str_buffer;
|
||||
free(str_buffer);
|
||||
cJSON_Delete(str_obj);
|
||||
return str;
|
||||
}
|
||||
|
||||
auto Utils::PtrToString(const void* val) -> std::string {
|
||||
char buffer[128];
|
||||
snprintf(buffer, sizeof(buffer), "%p", val);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
static const char* g_default_random_names[] = {
|
||||
"Flopsy", "Skippy", "Boomer", "Jolly", "Zeus", "Garth",
|
||||
"Dizzy", "Mullet", "Ogre", "Ginger", "Nippy", "Murphy",
|
||||
"Crom", "Sparky", "Wedge", "Arthur", "Benji", "Pan",
|
||||
"Wallace", "Hamish", "Luke", "Cowboy", "Uncas", "Magua",
|
||||
"Robin", "Lancelot", "Mad Dog", "Maximus", "Leonidas", "Don Quixote",
|
||||
"Beowulf", "Gilgamesh", "Conan", "Cicero", "Elmer", "Flynn",
|
||||
"Duck", "Uther", "Darkness", "Sunshine", "Willy", "Elvis",
|
||||
"Dolph", "Rico", "Magoogan", "Willow", "Rose", "Egg",
|
||||
"Thunder", "Jack", "Dude", "Walter", "Donny", "Larry",
|
||||
"Chunk", "Socrates", nullptr};
|
||||
|
||||
static std::list<std::string>* g_random_names_list = nullptr;
|
||||
|
||||
auto Utils::GetRandomNameList() -> const std::list<std::string>& {
|
||||
assert(InGameThread());
|
||||
if (!g_random_names_list) {
|
||||
// this will init the list with our default english names
|
||||
SetRandomNameList(std::list<std::string>(1, "DEFAULT_NAMES"));
|
||||
}
|
||||
return *g_random_names_list;
|
||||
}
|
||||
|
||||
void Utils::SetRandomNameList(const std::list<std::string>& custom_names) {
|
||||
assert(InGameThread());
|
||||
if (!g_random_names_list) {
|
||||
g_random_names_list = new std::list<std::string>;
|
||||
} else {
|
||||
g_random_names_list->clear();
|
||||
}
|
||||
bool add_default_names = false;
|
||||
if (custom_names.empty()) {
|
||||
add_default_names = true;
|
||||
}
|
||||
for (const auto& custom_name : custom_names) {
|
||||
if (custom_name == "DEFAULT_NAMES") {
|
||||
add_default_names = true;
|
||||
} else {
|
||||
g_random_names_list->push_back(custom_name);
|
||||
}
|
||||
}
|
||||
if (add_default_names) {
|
||||
for (const char** c = g_default_random_names; *c != nullptr; c++) {
|
||||
g_random_names_list->push_back(*c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#define HEXVAL(x) ('0' + (x) + ((x) > 9u) * 7u)
|
||||
static auto ToHex(const std::string& s_in) -> std::string {
|
||||
uint32_t s_size = static_cast<int>(s_in.size());
|
||||
std::string s_out;
|
||||
s_out.resize(static_cast<size_t>(s_size) * 2);
|
||||
for (uint32_t i = 0; i < s_size; i++) {
|
||||
s_out[i * 2] =
|
||||
static_cast<char>(HEXVAL((static_cast<uint32_t>(s_in[i])) >> 4u));
|
||||
s_out[i * 2 + 1] =
|
||||
static_cast<char>(HEXVAL((static_cast<uint32_t>(s_in[i]) & 15u)));
|
||||
}
|
||||
return s_out;
|
||||
}
|
||||
#undef HEXVAL
|
||||
|
||||
static auto FromHex(const std::string& s_in) -> std::string {
|
||||
int s_size = static_cast<int>(s_in.size());
|
||||
BA_PRECONDITION(s_size % 2 == 0);
|
||||
s_size /= 2;
|
||||
std::string s_out;
|
||||
s_out.resize(static_cast<size_t>(s_size));
|
||||
for (int i = 0; i < s_size; i++) {
|
||||
auto val = (uint32_t)s_in[i * 2]; // NOLINT(cert-str34-c)
|
||||
if (val >= '0' && val <= '9') {
|
||||
s_out[i] = static_cast<char>((val - '0') << 4u);
|
||||
} else if (val >= 'A' && val <= 'F') {
|
||||
s_out[i] = static_cast<char>((10u + (val - 'A')) << 4u);
|
||||
} else {
|
||||
throw Exception();
|
||||
}
|
||||
val = (uint32_t)s_in[i * 2 + 1]; // NOLINT(cert-str34-c)
|
||||
if (val >= '0' && val <= '9') {
|
||||
s_out[i] =
|
||||
static_cast<char>(static_cast<uint32_t>(s_out[i]) | (val - '0'));
|
||||
} else if (val >= 'A' && val <= 'F') {
|
||||
s_out[i] = static_cast<char>(static_cast<uint32_t>(s_out[i])
|
||||
| (10 + (val - 'A')));
|
||||
} else {
|
||||
throw Exception();
|
||||
}
|
||||
}
|
||||
return s_out;
|
||||
}
|
||||
|
||||
static auto EncryptDecrypt(const std::string& to_encrypt) -> std::string {
|
||||
assert(g_platform);
|
||||
const char* key = g_platform->GetUniqueDeviceIdentifier().c_str();
|
||||
int key_size =
|
||||
static_cast<int>(g_platform->GetUniqueDeviceIdentifier().size());
|
||||
std::string output = to_encrypt;
|
||||
for (size_t i = 0; i < to_encrypt.size(); i++) {
|
||||
output[i] = to_encrypt[i] ^ key[i % (key_size)]; // NOLINT
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
static auto EncryptDecryptCustom(const std::string& to_encrypt,
|
||||
const std::string& key_in) -> std::string {
|
||||
assert(g_platform);
|
||||
const char* key = key_in.c_str();
|
||||
int key_size = static_cast<int>(key_in.size());
|
||||
std::string output = to_encrypt;
|
||||
for (size_t i = 0; i < to_encrypt.size(); i++) {
|
||||
output[i] = to_encrypt[i] ^ key[i % (key_size)]; // NOLINT
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
static auto PublicEncryptDecrypt(const std::string& to_encrypt) -> std::string {
|
||||
std::string key_str = "create an account"; // A non-key-looking key.
|
||||
const char* key = key_str.c_str();
|
||||
int key_size = static_cast<int>(key_str.size());
|
||||
std::string output = to_encrypt;
|
||||
for (size_t i = 0; i < to_encrypt.size(); i++)
|
||||
output[i] = to_encrypt[i] ^ key[i % (key_size)]; // NOLINT
|
||||
return output;
|
||||
}
|
||||
|
||||
auto Utils::LocalEncrypt(const std::string& s_in) -> std::string {
|
||||
return ToHex(EncryptDecrypt(s_in));
|
||||
}
|
||||
|
||||
auto Utils::LocalEncrypt2(const std::string& s_in) -> std::string {
|
||||
std::string s = EncryptDecrypt(s_in);
|
||||
return base64_encode((const unsigned char*)s.c_str(),
|
||||
static_cast<int>(s.size()));
|
||||
}
|
||||
auto Utils::EncryptCustom(const std::string& s_in, const std::string& key)
|
||||
-> std::string {
|
||||
std::string s = EncryptDecryptCustom(s_in, key);
|
||||
return base64_encode((const unsigned char*)s.c_str(),
|
||||
static_cast<int>(s.size()));
|
||||
}
|
||||
|
||||
auto Utils::LocalDecrypt(const std::string& s_in) -> std::string {
|
||||
return EncryptDecrypt(FromHex(s_in));
|
||||
}
|
||||
|
||||
auto Utils::LocalDecrypt2(const std::string& s_in) -> std::string {
|
||||
return EncryptDecrypt(base64_decode(s_in));
|
||||
}
|
||||
auto Utils::DecryptCustom(const std::string& s_in, const std::string& key)
|
||||
-> std::string {
|
||||
return EncryptDecryptCustom(base64_decode(s_in), key);
|
||||
}
|
||||
|
||||
auto Utils::PublicEncrypt(const std::string& s_in) -> std::string {
|
||||
return ToHex(PublicEncryptDecrypt(s_in));
|
||||
}
|
||||
|
||||
auto Utils::PublicDecrypt(const std::string& s_in) -> std::string {
|
||||
return PublicEncryptDecrypt(FromHex(s_in));
|
||||
}
|
||||
|
||||
auto Utils::PublicEncrypt2(const std::string& s_in) -> std::string {
|
||||
std::string s = PublicEncryptDecrypt(s_in);
|
||||
return base64_encode((const unsigned char*)s.c_str(),
|
||||
static_cast<int>(s.size()));
|
||||
}
|
||||
|
||||
auto Utils::PublicDecrypt2(const std::string& s_in) -> std::string {
|
||||
return PublicEncryptDecrypt(base64_decode(s_in));
|
||||
}
|
||||
|
||||
auto Utils::Sphrand(float radius) -> Vector3f {
|
||||
while (true) {
|
||||
float x = RandomFloat();
|
||||
float y = RandomFloat();
|
||||
float z = RandomFloat();
|
||||
x = -1.0f + x * 2.0f;
|
||||
y = -1.0f + y * 2.0f;
|
||||
z = -1.0f + z * 2.0f;
|
||||
if (x * x + y * y + z * z <= 1.0f) {
|
||||
return {x * radius, y * radius, z * radius};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto Utils::FileToString(const std::string& file_name) -> std::string {
|
||||
std::ifstream file_stream{file_name};
|
||||
if (file_stream.fail()) {
|
||||
throw Exception("Error opening file for reading: '" + file_name + "'");
|
||||
}
|
||||
std::ostringstream str_stream{};
|
||||
file_stream >> str_stream.rdbuf();
|
||||
if (file_stream.fail() && !file_stream.eof()) {
|
||||
throw Exception("Error reading file: '" + file_name + "'");
|
||||
}
|
||||
return str_stream.str();
|
||||
}
|
||||
|
||||
static void WaitThenDie(millisecs_t wait, const std::string& action) {
|
||||
Platform::SleepMS(wait);
|
||||
throw std::runtime_error("Timed out waiting for " + action + "; aborting.");
|
||||
}
|
||||
|
||||
void Utils::StartSuicideTimer(const std::string& action, millisecs_t delay) {
|
||||
if (!g_app_globals->started_suicide) {
|
||||
new std::thread(WaitThenDie, delay, action);
|
||||
g_app_globals->started_suicide = true;
|
||||
}
|
||||
}
|
||||
|
||||
auto Utils::BaseName(const std::string& val) -> std::string {
|
||||
const char* c = val.c_str();
|
||||
const char* lastvalid = c;
|
||||
while (*c != 0) {
|
||||
if (*c == '/' || *c == '\\') {
|
||||
lastvalid = c + 1;
|
||||
}
|
||||
++c;
|
||||
}
|
||||
return lastvalid;
|
||||
}
|
||||
|
||||
} // namespace ballistica
|
||||
387
src/ballistica/generic/utils.h
Normal file
387
src/ballistica/generic/utils.h
Normal file
@ -0,0 +1,387 @@
|
||||
// Released under the MIT License. See LICENSE for details.
|
||||
|
||||
#ifndef BALLISTICA_GENERIC_UTILS_H_
|
||||
#define BALLISTICA_GENERIC_UTILS_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Need platform-specific headers here so we can inline calls to htonl/etc.
|
||||
// (perhaps should move those functions to their own file?)
|
||||
#if BA_OSTYPE_WINDOWS
|
||||
#include <winsock2.h>
|
||||
#else
|
||||
#include <netinet/in.h>
|
||||
#endif
|
||||
#include <cstring>
|
||||
|
||||
#include "ballistica/ballistica.h"
|
||||
|
||||
namespace ballistica {
|
||||
|
||||
const int kPrecalcRandsCount = 128;
|
||||
|
||||
/// A holding tank for miscellaneous functionality not extensive enough to
|
||||
/// warrant its own class. When possible, things should be moved out of here
|
||||
/// into more organized locations.
|
||||
class Utils {
|
||||
public:
|
||||
Utils();
|
||||
~Utils();
|
||||
|
||||
static auto BaseName(const std::string& val) -> std::string;
|
||||
|
||||
static auto PtrToString(const void* val) -> std::string;
|
||||
|
||||
// This should probably live elsewhere...
|
||||
static auto GetRandomNameList() -> const std::list<std::string>&;
|
||||
static void SetRandomNameList(const std::list<std::string>& names);
|
||||
|
||||
static auto UnicodeFromUTF8(const std::string& s, const char* loc)
|
||||
-> std::vector<uint32_t>;
|
||||
static auto UTF8FromUnicode(std::vector<uint32_t> unichars) -> std::string;
|
||||
static auto UTF8FromUnicodeChar(uint32_t c) -> std::string;
|
||||
static auto UTF8StringLength(const char* val) -> int;
|
||||
|
||||
/// Start a timer to kill the app after the set length of time.
|
||||
/// Use this during shutdown or when trying to send a crash-report before
|
||||
/// dying just to ensure we don't hang indefinitely.
|
||||
static void StartSuicideTimer(const std::string& action, millisecs_t delay);
|
||||
|
||||
/// Replace a single occurrence of key with replacement in the target string.
|
||||
/// Returns whether a replacement occurred.
|
||||
static auto StringReplaceOne(std::string* target, const std::string& key,
|
||||
const std::string& replacement) -> bool;
|
||||
|
||||
static auto StringReplaceAll(std::string* target, const std::string& key,
|
||||
const std::string& replacement) -> void;
|
||||
|
||||
/// Strip out or corrects invalid utf8.
|
||||
/// This is run under the hood for all the above calls but in some cases
|
||||
/// (such as the calls below) you may want to run it by hand.
|
||||
/// Loc is included in debug log output if invalid utf8 is passed in.
|
||||
static auto GetValidUTF8(const char* str, const char* loc) -> std::string;
|
||||
|
||||
/// Use this for debugging (not optimized for speed).
|
||||
/// Currently just runs GetValidUTF8 and compares
|
||||
/// results to original to see if anything got changed.
|
||||
static auto IsValidUTF8(const std::string& val) -> bool;
|
||||
|
||||
// Escape a string so it can be embedded as part of a flattened json string.
|
||||
static auto GetJSONString(const char* s) -> std::string;
|
||||
|
||||
// IMPORTANT - These run on 'trusted' utf8 - make sure you've run
|
||||
// GetValidUTF8 on any data you pass to these
|
||||
static auto GetUTF8Value(const char* s) -> uint32_t;
|
||||
static void AdvanceUTF8(const char** s);
|
||||
|
||||
/* The following code uses bitwise operators to determine
|
||||
if an unsigned integer, x, is a power of two. If x is a power of two,
|
||||
x is represented in binary with only a single bit; therefore, subtraction
|
||||
by one removes that bit and flips all the lower-order bits. The bitwise
|
||||
and <http://www.cprogramming.com/tutorial/bitwise.html>
|
||||
|
||||
then effectively checks to see if any bit is the
|
||||
same. If not, then it's a power of two.*/
|
||||
static inline auto IsPowerOfTwo(unsigned int x) -> int {
|
||||
return !((x - 1) & x);
|
||||
}
|
||||
|
||||
// Yes this stuff should technically be using unsigned values for the
|
||||
// bitwise stuff but I'm not brave enough to convert it at the moment.
|
||||
// Made a quick attempt and everything blew up.
|
||||
#pragma clang diagnostic push
|
||||
#pragma ide diagnostic ignored "hicpp-signed-bitwise"
|
||||
#pragma ide diagnostic ignored "bugprone-narrowing-conversions"
|
||||
|
||||
static inline auto FloatToHalfI(uint32_t i) -> uint16_t {
|
||||
int s = (i >> 16) & 0x00008000; // NOLINT
|
||||
int e = ((i >> 23) & 0x000000ff) - (127 - 15); // NOLINT
|
||||
int m = i & 0x007fffff; // NOLINT
|
||||
|
||||
if (e <= 0) {
|
||||
if (e < -10) {
|
||||
return 0;
|
||||
}
|
||||
m = (m | 0x00800000) >> (1 - e);
|
||||
|
||||
return static_cast<uint16_t>(s | (m >> 13));
|
||||
} else if (e == 0xff - (127 - 15)) {
|
||||
if (m == 0) {
|
||||
// Inf
|
||||
return static_cast<uint16_t>(s | 0x7c00);
|
||||
} else {
|
||||
// NAN
|
||||
m >>= 13;
|
||||
return static_cast<uint16_t>(s | 0x7c00 | m | (m == 0));
|
||||
}
|
||||
} else {
|
||||
if (e > 30) {
|
||||
// Overflow
|
||||
return static_cast<uint16_t>(s | 0x7c00);
|
||||
}
|
||||
|
||||
return static_cast<uint16_t>(s | (e << 10) | (m >> 13));
|
||||
}
|
||||
}
|
||||
|
||||
static inline auto FloatToHalf(float i) -> uint16_t {
|
||||
union {
|
||||
float f;
|
||||
uint32_t i;
|
||||
} v{};
|
||||
v.f = i;
|
||||
return FloatToHalfI(v.i);
|
||||
}
|
||||
|
||||
static inline auto HalfToFloatI(uint16_t y) -> uint32_t {
|
||||
int s = (y >> 15) & 0x00000001;
|
||||
int e = (y >> 10) & 0x0000001f;
|
||||
int m = y & 0x000003ff;
|
||||
if (e == 0) {
|
||||
if (m == 0) { // Plus or minus zero
|
||||
return static_cast<uint32_t>(s << 31);
|
||||
} else { // Denormalized number -- renormalize it
|
||||
while (!(m & 0x00000400)) {
|
||||
m <<= 1;
|
||||
e -= 1;
|
||||
}
|
||||
e += 1;
|
||||
m &= ~0x00000400;
|
||||
}
|
||||
} else if (e == 31) {
|
||||
if (m == 0) { // Inf
|
||||
return static_cast<uint32_t>((s << 31) | 0x7f800000);
|
||||
} else { // NaN
|
||||
return static_cast<uint32_t>((s << 31) | 0x7f800000 | (m << 13));
|
||||
}
|
||||
}
|
||||
e = e + (127 - 15);
|
||||
m = m << 13;
|
||||
return static_cast<uint32_t>((s << 31) | (e << 23) | m);
|
||||
}
|
||||
|
||||
static inline auto HalfToFloat(uint16_t y) -> float {
|
||||
union {
|
||||
float f;
|
||||
uint32_t i;
|
||||
} v{};
|
||||
v.i = HalfToFloatI(y);
|
||||
return v.f;
|
||||
}
|
||||
|
||||
// Value embedding/extracting in buffers.
|
||||
// Note to self:
|
||||
// Whenever its possible to do so cleanly, we should migrate to storing
|
||||
// everything in little-endian and kill off the NBO versions of these.
|
||||
// I don't anticipate having to run on big-endian hardware anytime soon
|
||||
// and it'll save us a few cycles. (plus things are a sloppy mix of
|
||||
// network-byte-ordered and native-ordered as it stands now).
|
||||
|
||||
/// Embed a single bool in a buffer.
|
||||
static inline void EmbedBool(char** b, bool i) {
|
||||
**b = i; // NOLINT
|
||||
(*b)++;
|
||||
}
|
||||
|
||||
/// Embed up to 8 bools in a buffer (in a single byte) use ExtractBools to
|
||||
/// pull them out.
|
||||
static inline void EmbedBools(char** b, bool i1, bool i2 = false,
|
||||
bool i3 = false, bool i4 = false,
|
||||
bool i5 = false, bool i6 = false,
|
||||
bool i7 = false, bool i8 = false) {
|
||||
**b = uint8_t(i1) | (uint8_t(i2) << 1) | (uint8_t(i3) << 2) // NOLINT
|
||||
| (uint8_t(i4) << 3) | (uint8_t(i5) << 4) // NOLINT
|
||||
| (uint8_t(i6) << 5) | (uint8_t(i7) << 6) // NOLINT
|
||||
| (uint8_t(i8) << 7); // NOLINT
|
||||
(*b)++;
|
||||
}
|
||||
|
||||
static inline void EmbedInt8(char** b, int8_t i) {
|
||||
**b = i;
|
||||
(*b)++;
|
||||
}
|
||||
|
||||
/// Embed a 2 byte int (short) into a buffer in network byte order.
|
||||
static inline void EmbedInt16NBO(char** b, int16_t i) {
|
||||
i = htons(i);
|
||||
memcpy(*b, &i, sizeof(i));
|
||||
*b += 2;
|
||||
}
|
||||
|
||||
/// Embed a 4 byte int into a buffer in network-byte-order.
|
||||
static inline void EmbedInt32NBO(char** b, int32_t i) {
|
||||
i = htonl(i);
|
||||
memcpy(*b, &i, sizeof(i));
|
||||
*b += 4;
|
||||
}
|
||||
|
||||
/// Embed a float in 16 bit "half" format (loses some precision) in
|
||||
/// network-byte-order.
|
||||
static inline void EmbedFloat16NBO(char** b, float f) {
|
||||
uint16_t val = htons(FloatToHalf(f));
|
||||
memcpy(*b, &val, sizeof(val));
|
||||
*b += 2;
|
||||
}
|
||||
|
||||
/// Embed 4 byte float into a buffer.
|
||||
static inline void EmbedFloat32(char** b, float f) {
|
||||
memcpy(*b, &f, 4);
|
||||
*b += 4;
|
||||
}
|
||||
|
||||
/// Embed a string into a buffer.
|
||||
static inline void EmbedString(char** b, const char* s) {
|
||||
strcpy(*b, s); // NOLINT
|
||||
*b += strlen(*b) + 1;
|
||||
}
|
||||
|
||||
static inline auto EmbeddedStringSize(const char* s) -> int {
|
||||
return static_cast<int>(strlen(s) + 1);
|
||||
}
|
||||
|
||||
/// Embed a string in a buffer.
|
||||
static inline void EmbedString(char** b, const std::string& s) {
|
||||
strcpy(*b, s.c_str()); // NOLINT
|
||||
*b += s.size() + 1;
|
||||
}
|
||||
|
||||
/// Return the number of bytes an embedded string with occupy.
|
||||
static inline auto EmbeddedStringSize(const std::string& s) -> int {
|
||||
return static_cast<int>(s.size() + 1);
|
||||
}
|
||||
|
||||
/// Extract a string from a buffer.
|
||||
static inline auto ExtractString(const char** b) -> std::string {
|
||||
std::string s = *b;
|
||||
*b += s.size() + 1;
|
||||
return s;
|
||||
}
|
||||
|
||||
/// Extract a single bool from a buffer.
|
||||
static inline auto ExtractBool(const char** b) -> bool {
|
||||
bool i = (**b != 0);
|
||||
(*b)++;
|
||||
return i;
|
||||
}
|
||||
|
||||
/// Extract multiple bools from a buffer.
|
||||
static inline void ExtractBools(const char** b, bool* i1, bool* i2 = nullptr,
|
||||
bool* i3 = nullptr, bool* i4 = nullptr,
|
||||
bool* i5 = nullptr, bool* i6 = nullptr,
|
||||
bool* i7 = nullptr, bool* i8 = nullptr) {
|
||||
auto i = static_cast<uint8_t>(**b);
|
||||
*i1 = static_cast<bool>(i & 0x01);
|
||||
if (i2) *i2 = static_cast<bool>((i >> 1) & 0x01);
|
||||
if (i3) *i3 = static_cast<bool>((i >> 2) & 0x01);
|
||||
if (i4) *i4 = static_cast<bool>((i >> 3) & 0x01);
|
||||
if (i5) *i5 = static_cast<bool>((i >> 4) & 0x01);
|
||||
if (i6) *i6 = static_cast<bool>((i >> 5) & 0x01);
|
||||
if (i7) *i7 = static_cast<bool>((i >> 6) & 0x01);
|
||||
if (i8) *i8 = static_cast<bool>((i >> 7) & 0x01);
|
||||
(*b)++;
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
/// Extract a 1 byte int from a buffer.
|
||||
static inline auto ExtractInt8(const char** b) -> int8_t {
|
||||
int8_t i = **b;
|
||||
(*b)++;
|
||||
return i;
|
||||
}
|
||||
|
||||
/// Extract a 2 byte int from a network-byte-order buffer.
|
||||
static inline auto ExtractInt16NBO(const char** b) -> int16_t {
|
||||
int16_t i;
|
||||
memcpy(&i, *b, sizeof(i));
|
||||
*b += 2;
|
||||
return ntohs(i); // NOLINT
|
||||
}
|
||||
|
||||
/// Extract a 4 byte int from a network-byte-order buffer.
|
||||
static inline auto ExtractInt32NBO(const char** b) -> int32_t {
|
||||
int32_t i;
|
||||
memcpy(&i, *b, sizeof(i));
|
||||
*b += 4;
|
||||
return ntohl(i); // NOLINT
|
||||
}
|
||||
|
||||
/// Extract a 2 byte (half) float from a network-byte-order buffer.
|
||||
static inline auto ExtractFloat16NBO(const char** b) -> float {
|
||||
uint16_t i;
|
||||
memcpy(&i, *b, sizeof(i));
|
||||
*b += 2;
|
||||
return HalfToFloat(ntohs(i)); // NOLINT
|
||||
}
|
||||
|
||||
/// Extract a 4 byte float from a buffer.
|
||||
static inline auto ExtractFloat32(const char** b) -> float {
|
||||
float i;
|
||||
memcpy(&i, *b, 4);
|
||||
*b += 4;
|
||||
return i;
|
||||
}
|
||||
|
||||
/// Return whether a sequence of some type pointer has nullptr members.
|
||||
template <typename T>
|
||||
static auto HasNullMembers(const T& sequence) -> bool {
|
||||
for (auto&& i : sequence) {
|
||||
if (i == nullptr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Simple lists of pre-calculated random values between 0 and 1
|
||||
/// (with no particular distribution)
|
||||
static float precalc_rands_1[];
|
||||
static float precalc_rands_2[];
|
||||
static float precalc_rands_3[];
|
||||
auto huffman() -> Huffman* { return huffman_.get(); }
|
||||
|
||||
/// Encrypt a string in a manner specific to this device.
|
||||
static auto LocalEncrypt(const std::string& s) -> std::string;
|
||||
static auto LocalEncrypt2(const std::string& s) -> std::string;
|
||||
|
||||
/// Decode a local string that was encoded specific to this device.
|
||||
/// Throws an exception on failure.
|
||||
static auto LocalDecrypt(const std::string& s) -> std::string;
|
||||
static auto LocalDecrypt2(const std::string& s) -> std::string;
|
||||
|
||||
/// Encrypt a string using a custom key.
|
||||
static auto EncryptCustom(const std::string& s, const std::string& key)
|
||||
-> std::string;
|
||||
/// Decrypt a string using a custom key.
|
||||
static auto DecryptCustom(const std::string& s, const std::string& key)
|
||||
-> std::string;
|
||||
|
||||
/// Encrypt/decrypt strings to send to the master-server
|
||||
static auto PublicEncrypt(const std::string& s) -> std::string;
|
||||
static auto PublicDecrypt(const std::string& s) -> std::string;
|
||||
static auto PublicEncrypt2(const std::string& s) -> std::string;
|
||||
static auto PublicDecrypt2(const std::string& s) -> std::string;
|
||||
|
||||
// FIXME - move to a nice math-y place
|
||||
static auto Sphrand(float radius = 1.0f) -> Vector3f;
|
||||
|
||||
// read a file into a string, throwing an Exception on error.
|
||||
static auto FileToString(const std::string& file_name) -> std::string;
|
||||
|
||||
// fixme: move this to a 'Math' class?..
|
||||
static auto SmoothStep(float edge0, float edge1, float x) -> float {
|
||||
float t;
|
||||
t = std::min(1.0f, std::max(0.0f, (x - edge0) / (edge1 - edge0)));
|
||||
return t * t * (3.0f - 2.0f * t);
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<Huffman> huffman_;
|
||||
};
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
#endif // BALLISTICA_GENERIC_UTILS_H_
|
||||
@ -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
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user