// BeatTime — universal @beat clock, header-only C++17 (single file, no deps). // https://beattime.live // // BeatTime is one universal time: 1000 .beats/day, anchored to UTC, no timezones. // The clock is fully derived from UTC, so ticking it needs NO network at all — // perfect for a game HUD. Drop this header into Unreal, Godot, SDL, a server, // anywhere with std::chrono. // // #include "beattime.hpp" // // double b = beattime::beats_now(); // 523.4567... // std::string s = beattime::format(b); // "@523.45" // std::string w = beattime::format(b, false); // "@523" // // // Optional server sync (SNTP-style drift correction) — fetch // // https://beattime.live/api/sync/ with your engine's HTTP stack, then: // beattime::Clock clock; // clock.apply_sync(server_unix_ms, t0_ms, t1_ms); // t0/t1 = before/after the request // std::string hud = clock.str(); // ticks on the corrected clock // // --------------------------------------------------------------------------- // Unreal Engine example (FHttpModule + a UMG text block): // // // .Build.cs: PrivateDependencyModuleNames.Add("HTTP"); (+ "Json") // void AMyHud::SyncBeatClock() // { // int64 T0 = FDateTime::UtcNow().ToUnixTimestamp() * 1000; // auto Req = FHttpModule::Get().CreateRequest(); // Req->SetURL(TEXT("https://beattime.live/api/sync/")); // Req->OnProcessRequestComplete().BindLambda( // [this, T0](FHttpRequestPtr, FHttpResponsePtr Res, bool bOk) // { // if (!bOk || !Res.IsValid()) return; // keep ticking locally // int64 T1 = FDateTime::UtcNow().ToUnixTimestamp() * 1000; // TSharedPtr Json; // auto Reader = TJsonReaderFactory<>::Create(Res->GetContentAsString()); // if (FJsonSerializer::Deserialize(Reader, Json) && Json.IsValid()) // BeatClock.apply_sync((int64_t)Json->GetNumberField(TEXT("server_unix_ms")), T0, T1); // }); // Req->ProcessRequest(); // } // // Tick (every frame is fine — it's a few integer ops): // BeatText->SetText(FText::FromString(UTF8_TO_TCHAR(BeatClock.str().c_str()))); // // Plain libcurl (or any HTTP client) works the same way: GET /api/sync/, parse // "server_unix_ms", call apply_sync(). The open REST API also offers conversion // (/api/convert/) and proof-of-existence (/api/proof/*) — URL builders below, // full docs at https://beattime.live/docs/. // // No API key, no limits beyond fair-use rate limiting. MIT-style: use freely. #pragma once #include #include #include #include #include namespace beattime { inline constexpr const char* kBaseUrl = "https://beattime.live"; inline constexpr std::int64_t kMsPerDay = 86400000; // ms per day inline constexpr std::int64_t kMsPerBeat = 86400; // ms per beat (1000 beats/day) // .beat for a unix-ms instant: ms-of-day (UTC) / 86400. Anchor = UTC. inline double beats_at(std::int64_t unix_ms) noexcept { const std::int64_t d = ((unix_ms % kMsPerDay) + kMsPerDay) % kMsPerDay; return static_cast(d) / static_cast(kMsPerBeat); } // Current unix time in ms from the system UTC clock. inline std::int64_t unix_ms_now() { using namespace std::chrono; return duration_cast(system_clock::now().time_since_epoch()).count(); } // Current .beat [0,1000) from the device clock (add an offset from apply_sync). inline double beats_now(std::int64_t offset_ms = 0) { return beats_at(unix_ms_now() + offset_ms); } // Format beats: "@523.45" (centibeats, default) or "@523". inline std::string format(double beats, bool centi = true) { const int whole = static_cast(std::floor(beats)); char buf[16]; if (centi) { const int cb = static_cast(std::floor((beats - whole) * 100.0)); std::snprintf(buf, sizeof(buf), "@%03d.%02d", whole, cb); } else { std::snprintf(buf, sizeof(buf), "@%03d", whole); } return buf; } // Ticking clock with optional server-sync drift correction. class Clock { public: // Feed the /api/sync/ response: server_unix_ms plus the local unix-ms // timestamps taken just before (t0) and after (t1) the request. // SNTP-style: assumes half the round-trip each way. void apply_sync(std::int64_t server_unix_ms, std::int64_t t0_ms, std::int64_t t1_ms) noexcept { offset_ms_ = server_unix_ms + (t1_ms - t0_ms) / 2 - t1_ms; } std::int64_t offset_ms() const noexcept { return offset_ms_; } double beats() const { return beats_now(offset_ms_); } std::string str(bool centi = true) const { return format(beats(), centi); } private: std::int64_t offset_ms_ = 0; // server - local; 0 until the first sync }; // --- open REST API endpoints (bring your own HTTP client) ------------------- inline std::string now_url() { return std::string(kBaseUrl) + "/api/now/"; } inline std::string sync_url() { return std::string(kBaseUrl) + "/api/sync/"; } inline std::string health_url() { return std::string(kBaseUrl) + "/api/health/"; } // Convert an instant (ISO 8601, URL-encoded by you; no timezone = UTC) to @beat. inline std::string time_to_beat_url(const std::string& iso8601_urlencoded) { return std::string(kBaseUrl) + "/api/convert/?at=" + iso8601_urlencoded; } // Convert a beat [0,1000) to UTC time. Optional date "YYYY-MM-DD" and // tz_offset in minutes (e.g. 120 for UTC+2) to also get the local time. inline std::string beat_to_time_url(double beat, const std::string& date = "", int tz_offset = INT32_MIN) { char b[32]; std::snprintf(b, sizeof(b), "%.6f", beat); std::string url = std::string(kBaseUrl) + "/api/convert/?beat=" + b; if (!date.empty()) url += "&date=" + date; if (tz_offset != INT32_MIN) url += "&tz_offset=" + std::to_string(tz_offset); return url; } // Proof of existence: POST {"digest":"<64-hex sha256>"} to stamp_url(). // Only the hash is sent — the file never leaves the machine. inline std::string stamp_url() { return std::string(kBaseUrl) + "/api/proof/stamp"; } inline std::string verify_api_url(const std::string& digest) { return std::string(kBaseUrl) + "/api/proof/verify?digest=" + digest; } inline std::string cert_url(const std::string& digest) { return std::string(kBaseUrl) + "/api/proof/cert/" + digest; // PDF certificate } inline std::string verify_page_url(const std::string& digest) { return std::string(kBaseUrl) + "/proof/?h=" + digest; // public page } } // namespace beattime