mirror of
https://github.com/Toastconcern/FrameStudio.git
synced 2026-10-28 21:02:20 +01:00
Frame Studio - Gaussian splats (PLY, SPZ v2-v4), PLY vertex colours, panorama skies - Copy, cut, paste and duplicate for objects, splats and world items - Walkable-area rebake moved off the UI thread; viewport one frame sooner; refresh-locked pacing (FS_PACE=timer for the old one) - Fixed: bundle thumbnail use-after-free crash, auto-save deleting environments, saves into scratch copies, edits lost on non-ASCII paths, unopenable manifests with inf/nan, bundle save data loss, opening a home marking it changed, gizmo NaN, long paths, many malformed-file crashes - Updater verifies sha256, journals the swap and never leaves the exe missing Frame Home - Seats turn you to face their way - Draw batching (Hand Tutorials up to ~20% less GPU time), cut-out EQUAL pass, late head-pose read before submit - Proper queue-family hand-over to the compositor layer; layer descriptor generations and seqlocked slots; structural SPIR-V check - Fixed: hangs/crashes on damaged .fenv files and shaders, render-pass validation error, sync hazard in the foveation smoothing pass, unverified update downloads, half-done installs Tests: tests_loco/env_share/spv_check wired into CMake (FRAME_TESTS) and CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsaXGU27gsrXjDoFtyFJP9
239 lines
8.0 KiB
C++
239 lines
8.0 KiB
C++
#pragma once
|
|
// Minimal single-header fork of nlohmann/json v3.11 for HSTF parsing.
|
|
// Only what's needed: string, int, float, bool, null, object, array.
|
|
// Size-optimized for embedded use in the HSR renderer.
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <map>
|
|
#include <stdexcept>
|
|
#include <sstream>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <limits>
|
|
|
|
namespace tinyjson {
|
|
|
|
enum class Type { Null, Bool, Int, Float, String, Array, Object };
|
|
|
|
class Value {
|
|
public:
|
|
Type type = Type::Null;
|
|
bool boolVal = false;
|
|
int64_t intVal = 0;
|
|
double floatVal = 0.0;
|
|
std::string strVal;
|
|
std::vector<Value> arrVal;
|
|
std::map<std::string, Value> objVal;
|
|
|
|
Value() = default;
|
|
Value(Type t) : type(t) {}
|
|
|
|
bool isNull() const { return type == Type::Null; }
|
|
bool isBool() const { return type == Type::Bool; }
|
|
bool isInt() const { return type == Type::Int; }
|
|
bool isFloat() const { return type == Type::Float; }
|
|
bool isString() const { return type == Type::String; }
|
|
bool isArray() const { return type == Type::Array; }
|
|
bool isObject() const { return type == Type::Object; }
|
|
bool isNumber() const { return type == Type::Int || type == Type::Float; }
|
|
|
|
std::string asString() const { return strVal; }
|
|
int64_t asInt() const {
|
|
if (type == Type::Int) return intVal;
|
|
if (type == Type::Float) // (NaN / out of range: undefined as a cast; 0 or the nearest end instead)
|
|
return floatVal >= 9.2e18 ? INT64_MAX : floatVal <= -9.2e18 ? INT64_MIN : floatVal == floatVal ? (int64_t)floatVal : 0;
|
|
// Asset IDs may be quoted ("targetId": "2719744159" in community shellconfig/HSTF) —
|
|
// parse unsigned so u32 targetIds above INT_MAX survive the (u32) cast at call sites.
|
|
if (type == Type::String) return (int64_t)std::strtoull(strVal.c_str(), nullptr, 10);
|
|
return 0;
|
|
}
|
|
double asFloat() const {
|
|
if (type == Type::Float) return floatVal;
|
|
if (type == Type::Int) return (double)intVal;
|
|
return 0.0;
|
|
}
|
|
bool asBool() const { return boolVal; }
|
|
|
|
Value& operator[](const std::string& k) { return objVal[k]; }
|
|
// Out-of-range / non-array index reads as Null (file data drives the indices). The mutable overload hands
|
|
// back a per-thread scratch Null so a stray write can't corrupt anything.
|
|
Value& operator[](size_t i) {
|
|
if (i < arrVal.size()) return arrVal[i];
|
|
thread_local Value scratch; scratch = Value(); return scratch;
|
|
}
|
|
const Value& operator[](const std::string& k) const {
|
|
static Value empty;
|
|
auto it = objVal.find(k);
|
|
return it != objVal.end() ? it->second : empty;
|
|
}
|
|
const Value& operator[](size_t i) const {
|
|
static const Value empty;
|
|
return i < arrVal.size() ? arrVal[i] : empty;
|
|
}
|
|
// Element count of an array (0 otherwise), so size()/operator[](size_t) loops can't index an object.
|
|
size_t size() const { return isArray() ? arrVal.size() : 0; }
|
|
|
|
bool has(const std::string& k) const { return objVal.find(k) != objVal.end(); }
|
|
};
|
|
|
|
// Fast JSON parser — no extra allocations for simple values
|
|
class Parser {
|
|
const char* p;
|
|
const char* end;
|
|
// Nesting cap: recursion depth (and the recursive Value destructor) stays bounded on hostile input.
|
|
static constexpr int kMaxDepth = 256;
|
|
int depth = 0;
|
|
public:
|
|
Parser(const std::string& s) : p(s.data()), end(s.data() + s.size()) {}
|
|
|
|
Value parse() {
|
|
skipWS();
|
|
return parseValue();
|
|
}
|
|
|
|
private:
|
|
void skipWS() { while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) ++p; }
|
|
|
|
char peek() { return p < end ? *p : 0; }
|
|
char next() { return p < end ? *p++ : 0; }
|
|
|
|
Value parseValue() {
|
|
skipWS();
|
|
switch (peek()) {
|
|
case '"': return parseString();
|
|
case '{': return parseObject();
|
|
case '[': return parseArray();
|
|
case 't': case 'f': return parseBool();
|
|
case 'n': return parseNull();
|
|
default: return parseNumber();
|
|
}
|
|
}
|
|
|
|
Value parseNull() {
|
|
if (p + 4 <= end && memcmp(p, "null", 4) == 0) { p += 4; return Value(Type::Null); }
|
|
throw std::runtime_error("Expected null");
|
|
}
|
|
|
|
Value parseBool() {
|
|
if (p + 4 <= end && memcmp(p, "true", 4) == 0) {
|
|
p += 4; Value v(Type::Bool); v.boolVal = true; return v;
|
|
}
|
|
if (p + 5 <= end && memcmp(p, "false", 5) == 0) {
|
|
p += 5; Value v(Type::Bool); v.boolVal = false; return v;
|
|
}
|
|
throw std::runtime_error("Expected bool");
|
|
}
|
|
|
|
Value parseNumber() {
|
|
bool neg = false;
|
|
if (peek() == '-') { neg = true; next(); }
|
|
|
|
// Try to parse as integer first
|
|
bool isFloat = false;
|
|
int64_t intVal = 0;
|
|
double floatVal = 0;
|
|
const char* start = p;
|
|
|
|
uint64_t acc = 0; // unsigned: overlong digit runs wrap instead of signed-overflow UB
|
|
while (p < end && *p >= '0' && *p <= '9') {
|
|
acc = acc * 10 + (uint64_t)(*p - '0');
|
|
++p;
|
|
}
|
|
intVal = (int64_t)acc;
|
|
if (peek() == '.') {
|
|
isFloat = true;
|
|
next();
|
|
double frac = 0, scale = 0.1;
|
|
while (p < end && *p >= '0' && *p <= '9') {
|
|
frac += (*p - '0') * scale;
|
|
scale *= 0.1;
|
|
++p;
|
|
}
|
|
floatVal = (double)intVal + frac;
|
|
}
|
|
if (peek() == 'e' || peek() == 'E') {
|
|
if (!isFloat) floatVal = (double)intVal; // "1e0": no '.', so the value so far is the integer part
|
|
isFloat = true;
|
|
next();
|
|
bool eNeg = (peek() == '-');
|
|
if (eNeg || peek() == '+') next();
|
|
double exp = 0;
|
|
while (p < end && *p >= '0' && *p <= '9') { exp = exp * 10 + (*p - '0'); ++p; }
|
|
floatVal = floatVal * std::pow(10.0, eNeg ? -exp : exp);
|
|
}
|
|
if (p == start) throw std::runtime_error("Expected number");
|
|
|
|
if (neg) { intVal = (int64_t)(0 - (uint64_t)intVal); floatVal = -floatVal; }
|
|
Value v(isFloat ? Type::Float : Type::Int);
|
|
v.intVal = intVal;
|
|
v.floatVal = floatVal;
|
|
return v;
|
|
}
|
|
|
|
Value parseString() {
|
|
next(); // skip opening "
|
|
Value v(Type::String);
|
|
while (p < end && peek() != '"') {
|
|
if (peek() == '\\') {
|
|
next();
|
|
switch (next()) {
|
|
case '"': v.strVal += '"'; break;
|
|
case '\\': v.strVal += '\\'; break;
|
|
case '/': v.strVal += '/'; break;
|
|
case 'n': v.strVal += '\n'; break;
|
|
case 't': v.strVal += '\t'; break;
|
|
case 'r': v.strVal += '\r'; break;
|
|
default: v.strVal += '\\'; break;
|
|
}
|
|
} else {
|
|
v.strVal += next();
|
|
}
|
|
}
|
|
if (peek() == '"') next();
|
|
return v;
|
|
}
|
|
|
|
void enter() { if (++depth > kMaxDepth) throw std::runtime_error("JSON nested too deep"); }
|
|
|
|
Value parseArray() {
|
|
enter();
|
|
next(); // skip '['
|
|
Value v(Type::Array);
|
|
skipWS();
|
|
while (peek() != ']' && p < end) {
|
|
v.arrVal.push_back(parseValue());
|
|
skipWS();
|
|
if (peek() == ',') { next(); skipWS(); }
|
|
}
|
|
if (peek() == ']') next();
|
|
--depth;
|
|
return v;
|
|
}
|
|
|
|
Value parseObject() {
|
|
enter();
|
|
next(); // skip '{'
|
|
Value v(Type::Object);
|
|
skipWS();
|
|
while (peek() != '}' && p < end) {
|
|
Value key = parseValue();
|
|
skipWS();
|
|
if (peek() == ':') next();
|
|
Value val = parseValue();
|
|
v.objVal[key.asString()] = std::move(val);
|
|
skipWS();
|
|
if (peek() == ',') { next(); skipWS(); }
|
|
}
|
|
if (peek() == '}') next();
|
|
--depth;
|
|
return v;
|
|
}
|
|
};
|
|
|
|
inline Value parse(const std::string& s) { return Parser(s).parse(); }
|
|
|
|
} // namespace tinyjson
|