Download unify/engine/runtime/engine.cpp from Snapkitty/repoverse: direct link, hf CLI and curl.
- Browser
- Download file 38 kB
-
https://huggingface.co/Snapkitty/repoverse/resolve/main/unify/engine/runtime/engine.cpp
- Command line
-
hf download hf://Snapkitty/repoverse/unify/engine/runtime/engine.cpp
-
curl -L -o engine.cpp https://huggingface.co/Snapkitty/repoverse/resolve/main/unify/engine/runtime/engine.cpp
38 kB
| namespace unify { | |
| const char* stage_name(Stage s) { | |
| static const char* names[] = {"INPUT", "INPUT BUFFER", "FIXED SIMULATION", "PHYSICS", "GAMEPLAY / LUA", "ANIMATION", "AUDIO", "RENDER PREPARATION", "RENDER", "PRESENT"}; | |
| return s < Stage::Count ? names[size_t(s)] : "?"; | |
| } | |
| Engine::Engine(Backend& backend, EngineConfig config) | |
| : backend_(backend), config_(std::move(config)), mixer_(44100, 64), target_(config_.width, config_.height), rng_(config_.seed) { | |
| sim_.ticks_per_second = config_.tick_rate; | |
| assets_ = std::make_unique<AssetManager>(backend_.render_device(), config_.asset_root); | |
| renderer_ = std::make_unique<Renderer2D>(backend_.render_device()); | |
| lua_ = std::make_unique<LuaVM>(config_.lua_memory_limit); | |
| lua_->gc_budget(config_.lua_gc_budget_kb); | |
| // Entity destruction releases the subsystem resources it owns. | |
| world_.add_destroy_hook([this](EntityHandle e) { | |
| if (BodyC* b = world_.bodies.get(e)) physics_.destroy_body(PhysicsBodyHandle{b->body}); | |
| if (AnimatorC* a = world_.animators.get(e)) anim_.destroy_player(a->player); | |
| if (SpriteC* s = world_.sprites.get(e)) (void)s; // textures are owned by scenes, not entities | |
| }); | |
| physics_.gravity = {0, -30}; | |
| trace_.reserve(64); | |
| events_scratch_.reserve(512); | |
| } | |
| Engine::~Engine() { | |
| streamer_.stop(); | |
| backend_.audio_device().stop(); | |
| mixer_.stop_all(); | |
| while (!scenes_.empty()) { unload_scene(scenes_.back()); scenes_.pop_back(); } | |
| } | |
| bool Engine::boot(std::string& error) { | |
| if (!backend_.init(config_, error)) return false; | |
| font_ = BitmapFont::build(); | |
| font_.texture = backend_.render_device().create_texture(font_.image).bits; | |
| if (!backend_.audio_device().start(mixer_)) UNIFY_LOG_WARN("AUDIO", "audio device '%s' failed to start; continuing silent", backend_.audio_device().name()); | |
| streamer_.start(mixer_); | |
| lua_->sandbox(&rng_); | |
| register_bindings(*this); | |
| UNIFY_LOG_INFO("BOOT", "%s on %s backend, %dx%d, %u Hz simulation", kEngineVersion, backend_.name(), config_.width, config_.height, config_.tick_rate); | |
| if (!transition(config_.boot_scene, error)) return false; | |
| sim_origin_ns_ = backend_.now_ns(); | |
| booted_ = true; | |
| return true; | |
| } | |
| void Engine::script_log(const std::string& s) { | |
| script_log_.push_back(s); | |
| if (script_log_.size() > 256) script_log_.erase(script_log_.begin()); | |
| UNIFY_LOG_INFO("LUA", "%s", s.c_str()); | |
| } | |
| void Engine::report_script_error(const std::string& where) { | |
| std::string msg = where + ": " + lua_->last_error(); | |
| script_log("ERROR " + msg); | |
| UNIFY_LOG_ERROR("LUA", "%s", msg.c_str()); | |
| } | |
| // ------------------------------------------------------------------------------ scheduler | |
| void Engine::frame() { | |
| auto t0 = std::chrono::steady_clock::now(); | |
| trace_.clear(); | |
| mark(Stage::Input); | |
| backend_.poll(input_, quit_); | |
| const uint64_t now = backend_.now_ns(); | |
| const uint64_t dt_ns = 1000000000ull / sim_.ticks_per_second; | |
| uint32_t ran = 0; | |
| while (sim_origin_ns_ + (sim_.tick + 1) * 1000000000ull / sim_.ticks_per_second <= now && ran < uint32_t(config_.max_ticks_per_frame) && !quit_) { | |
| tick(); | |
| ran++; | |
| } | |
| if (ran == uint32_t(config_.max_ticks_per_frame)) { | |
| // Too far behind (breakpoint, stall): drop the backlog instead of spiralling. | |
| sim_origin_ns_ = now - sim_.tick * 1000000000ull / sim_.ticks_per_second; | |
| } | |
| stats_.ticks_this_frame = ran; | |
| uint64_t tick_start = sim_origin_ns_ + sim_.tick * 1000000000ull / sim_.ticks_per_second; | |
| float alpha = now > tick_start ? clampf(float(double(now - tick_start) / double(dt_ns)), 0, 1) : 0; | |
| render(alpha); | |
| render_clock_.frame_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count(); | |
| } | |
| int Engine::run() { | |
| while (!quit_) frame(); | |
| return 0; | |
| } | |
| void Engine::tick_impl(const InputFrame* replay) { | |
| if (!trace_.empty() && trace_.back() == Stage::Present) trace_.clear(); | |
| uint64_t heap0 = HeapStats::allocations.load(); | |
| auto stage_start = std::chrono::steady_clock::now(); | |
| auto lap = [&](Stage s) { | |
| auto t = std::chrono::steady_clock::now(); | |
| stats_.stage_ms[size_t(s)] = std::chrono::duration<double, std::milli>(t - stage_start).count(); | |
| stage_start = t; | |
| }; | |
| const float dt = sim_.dtf(); | |
| mark(Stage::InputBuffer); | |
| if (replay) input_.apply_frame(*replay); | |
| else input_.consume_tick(sim_.tick, sim_origin_ns_ + sim_.tick_end_ns(sim_.tick)); | |
| lap(Stage::InputBuffer); | |
| mark(Stage::FixedSimulation); | |
| for (size_t i = 0; i < world_.transforms.size(); i++) world_.transforms.at(i).previous = world_.transforms.at(i).current; | |
| lap(Stage::FixedSimulation); | |
| mark(Stage::Physics); | |
| physics_.step(dt); | |
| sync_transforms_from_physics(); | |
| lap(Stage::Physics); | |
| mark(Stage::Gameplay); | |
| dispatch_physics_events(); | |
| fire_timers(); | |
| lua_pushnumber(lua_->L(), dt); | |
| if (!call_scene("update", 1)) report_script_error("scene update"); | |
| if (!pending_transition_.empty()) { | |
| std::string target = pending_transition_, err; | |
| pending_transition_.clear(); | |
| if (!transition(target, err)) UNIFY_LOG_ERROR("SCENE", "transition to '%s' failed: %s", target.c_str(), err.c_str()); | |
| } | |
| lua_->gc_step(); // bounded incremental GC every tick: no unpredictable full-collection stalls | |
| lap(Stage::Gameplay); | |
| mark(Stage::Animation); | |
| anim_.advance(dt); | |
| lap(Stage::Animation); | |
| mark(Stage::Audio); | |
| for (auto& s : scenes_) // forget voices that finished on the audio thread | |
| s.voices.erase(std::remove_if(s.voices.begin(), s.voices.end(), [&](AudioHandle h) { return !mixer_.alive(h); }), s.voices.end()); | |
| lap(Stage::Audio); | |
| sim_.tick++; | |
| stats_.heap_allocs_last_tick = HeapStats::allocations.load() - heap0; | |
| stats_.lua_bytes = lua_->bytes(); | |
| process_pending_state_ops(); | |
| } | |
| void Engine::render(float alpha) { | |
| auto t0 = std::chrono::steady_clock::now(); | |
| mark(Stage::RenderPrep); | |
| render_clock_.alpha = alpha; | |
| render_clock_.frame++; | |
| render_clock_.seconds = double(backend_.now_ns()) * 1e-9; | |
| frame_alloc_.reset(); | |
| renderer_->begin(target_, camera_, {}); | |
| for (size_t i = 0; i < world_.sprites.size(); i++) { | |
| const SpriteC& s = world_.sprites.at(i); | |
| if (!s.visible) continue; | |
| EntityHandle e = world_.sprites.owner(i); | |
| const TransformC* t = world_.transforms.get(e); | |
| if (!t) continue; | |
| // Interpolate between the last two simulation states: smooth at any frame rate. | |
| Vec2 pos = lerp(t->previous.position, t->current.position, alpha); | |
| float rot = t->previous.rotation + (t->current.rotation - t->previous.rotation) * alpha; | |
| const AssetRecord* tex = assets_->get(AssetHandle{s.texture}); | |
| uint32_t region = s.atlas_region; | |
| Vec2 size = s.size; | |
| Color tint = s.tint; | |
| if (const AnimatorC* a = world_.animators.get(e)) { | |
| if (uint32_t r = anim_.sprite_region(a->player)) region = r; | |
| size.x *= anim_.value(a->player, "scale_x", 1); | |
| size.y *= anim_.value(a->player, "scale_y", 1); | |
| rot += anim_.value(a->player, "rotation", 0); | |
| tint.a = uint8_t(clampf(float(tint.a) * anim_.value(a->player, "alpha", 1), 0, 255)); | |
| } | |
| RectF src{0, 0, 1, 1}; | |
| uint32_t device_tex = 0; | |
| if (tex && tex->type == AssetType::Texture) { | |
| device_tex = tex->texture.texture.bits; | |
| const auto& regs = tex->texture.atlas.regions; | |
| RectI rr = regs[region < regs.size() ? region : 0].rect; | |
| src = {float(rr.x), float(rr.y), float(rr.w), float(rr.h)}; | |
| } | |
| renderer_->sprite(device_tex, src, pos, size, rot, tint, s.layer, s.depth, BlendMode::Alpha, s.flip_x, s.pivot); | |
| } | |
| rendering_ = true; // ui:* calls are only valid here | |
| if (!call_scene("render")) report_script_error("scene render"); | |
| rendering_ = false; | |
| stats_.stage_ms[size_t(Stage::RenderPrep)] = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count(); | |
| mark(Stage::Render); | |
| auto t1 = std::chrono::steady_clock::now(); | |
| renderer_->end(); | |
| stats_.stage_ms[size_t(Stage::Render)] = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t1).count(); | |
| mark(Stage::Present); | |
| backend_.present(target_); | |
| } | |
| void Engine::sync_transforms_from_physics() { | |
| for (size_t i = 0; i < world_.bodies.size(); i++) { | |
| EntityHandle e = world_.bodies.owner(i); | |
| const Body* b = physics_.get(PhysicsBodyHandle{world_.bodies.at(i).body}); | |
| TransformC* t = world_.transforms.get(e); | |
| if (!b || !t) continue; | |
| // Physics has already contained its own failures; only finite poses reach the ECS. | |
| if (!finite(b->position) || !std::isfinite(b->angle)) continue; | |
| t->current.position = b->position; | |
| t->current.rotation = b->angle; | |
| } | |
| } | |
| static void push_entity(lua_State* L, uint32_t bits) { | |
| if (!bits) { lua_pushnil(L); return; } | |
| *static_cast<uint32_t*>(lua_newuserdatauv(L, sizeof(uint32_t), 0)) = bits; | |
| luaL_setmetatable(L, kEntityMeta); | |
| } | |
| void Engine::dispatch_physics_events() { | |
| static const char* names[] = {"begin", "persist", "end", "trigger_begin", "trigger_end"}; | |
| // Copy first: gameplay may destroy bodies from inside a callback, which appends End | |
| // events to the physics event list and would invalidate a live iterator. | |
| events_scratch_ = physics_.events(); | |
| for (const ContactEvent& ev : events_scratch_) { | |
| if (ev.type == ContactEventType::Persist) continue; // high-volume; gameplay polls contacts via raycasts instead | |
| lua_State* L = lua_->L(); | |
| push_entity(L, world_.valid(EntityHandle{ev.user_a}) ? ev.user_a : 0); | |
| push_entity(L, world_.valid(EntityHandle{ev.user_b}) ? ev.user_b : 0); | |
| lua_pushstring(L, names[int(ev.type)]); | |
| lua_pushnumber(L, ev.normal.x); | |
| lua_pushnumber(L, ev.normal.y); | |
| lua_pushnumber(L, ev.impulse); | |
| if (!call_scene("on_contact", 6)) report_script_error("scene on_contact"); | |
| } | |
| } | |
| void Engine::add_timer(double seconds, bool repeat, const std::string& name) { | |
| uint64_t ticks = std::max<uint64_t>(1, uint64_t(seconds * sim_.ticks_per_second + 0.5)); | |
| timers_.push_back({sim_.tick + ticks, repeat ? ticks : 0, name, current_scene() ? current_scene()->id : uint16_t(0)}); | |
| } | |
| void Engine::fire_timers() { | |
| for (size_t i = 0; i < timers_.size();) { | |
| Timer& t = timers_[i]; | |
| if (t.fire_tick > sim_.tick) { i++; continue; } | |
| std::string name = t.name; | |
| if (t.interval_ticks) { t.fire_tick += t.interval_ticks; i++; } | |
| else timers_.erase(timers_.begin() + long(i)); | |
| lua_pushstring(lua_->L(), name.c_str()); | |
| if (!call_scene("on_timer", 1)) report_script_error("scene on_timer"); | |
| } | |
| } | |
| // ------------------------------------------------------------------------------ scenes | |
| bool Engine::call_scene(const char* fn, int nargs) { | |
| lua_State* L = lua_->L(); | |
| Scene* s = current_scene(); | |
| if (!s || s->lua_ref < 0) { lua_pop(L, nargs); return true; } | |
| lua_rawgeti(L, LUA_REGISTRYINDEX, s->lua_ref); | |
| lua_getfield(L, -1, fn); | |
| if (!lua_isfunction(L, -1)) { lua_pop(L, 2 + nargs); return true; } | |
| lua_insert(L, -(nargs + 2)); // fn below args | |
| lua_insert(L, -(nargs + 1)); // scene table as first arg (method call) | |
| if (lua_pcall(L, nargs + 1, 0, 0) != LUA_OK) { | |
| const char* msg = lua_tostring(L, -1); | |
| lua_->set_error(msg ? msg : "(non-string error)"); | |
| lua_pop(L, 1); | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool Engine::load_scene_script(Scene& s, std::string& error) { | |
| s.script = assets_->load(AssetType::Script, "scenes/" + s.name); | |
| if (!s.script) { error = "scene script 'scenes/" + s.name + "' not found"; return false; } | |
| const AssetRecord* rec = assets_->get(s.script); | |
| if (!lua_->run(rec->text, rec->path, 1)) { error = lua_->last_error(); return false; } | |
| lua_State* L = lua_->L(); | |
| if (!lua_istable(L, -1)) { lua_pop(L, 1); error = "scene script must return a table"; return false; } | |
| lua_pushstring(L, s.name.c_str()); | |
| lua_setfield(L, -2, "name"); | |
| s.lua_ref = luaL_ref(L, LUA_REGISTRYINDEX); | |
| return true; | |
| } | |
| void Engine::unload_scene(Scene& s) { | |
| // Entities owned by this scene (persistent ones have scene 0 and survive). | |
| std::vector<EntityHandle> doomed; | |
| world_.each_entity([&](EntityHandle e) { if (world_.info(e)->scene == s.id) doomed.push_back(e); }); | |
| for (EntityHandle e : doomed) world_.destroy(e); | |
| for (AudioHandle v : s.voices) mixer_.stop(v); | |
| for (BufferHandle b : s.buffers) compute_.buffer_destroy(b); | |
| for (KernelHandle k : s.kernels) compute_.unload(k); | |
| for (AssetHandle a : s.assets) assets_->release(a); | |
| if (s.script) assets_->release(s.script); | |
| if (s.lua_ref >= 0) luaL_unref(lua_->L(), LUA_REGISTRYINDEX, s.lua_ref); | |
| timers_.erase(std::remove_if(timers_.begin(), timers_.end(), [&](const Timer& t) { return t.scene == s.id; }), timers_.end()); | |
| s = Scene{}; | |
| } | |
| bool Engine::transition(const std::string& name, std::string& error) { | |
| if (!scenes_.empty()) { | |
| call_scene("unload"); | |
| unload_scene(scenes_.back()); | |
| scenes_.pop_back(); | |
| } | |
| return push_scene(name, error); | |
| } | |
| bool Engine::push_scene(const std::string& name, std::string& error) { | |
| Scene s; | |
| s.id = next_scene_id_++; | |
| s.name = name; | |
| s.voices.reserve(64); // playing a sound never grows this list mid-tick | |
| scenes_.push_back(s); | |
| if (!load_scene_script(scenes_.back(), error)) { unload_scene(scenes_.back()); scenes_.pop_back(); return false; } | |
| lua_->gc_collect(); // scene loads are the one place a full collection is allowed | |
| // init(): code-level setup (bindings, animation clips); also runs after a restore. | |
| // load(): creates the scene's world content; skipped on restore (the save provides it). | |
| if (!call_scene("init") || !call_scene("load")) { | |
| // A scene that fails to load is removed completely: no half-built world is left running. | |
| error = lua_->last_error(); | |
| report_script_error("scene load"); | |
| unload_scene(scenes_.back()); | |
| scenes_.pop_back(); | |
| return false; | |
| } | |
| UNIFY_LOG_INFO("SCENE", "loaded '%s' (id %u)", name.c_str(), scenes_.back().id); | |
| return true; | |
| } | |
| bool Engine::pop_scene(std::string& error) { | |
| if (scenes_.size() <= 1) { error = "cannot pop the last scene"; return false; } | |
| call_scene("unload"); | |
| unload_scene(scenes_.back()); | |
| scenes_.pop_back(); | |
| return true; | |
| } | |
| ResourceCounts Engine::resources() const { | |
| uint32_t refs = 0; | |
| for (auto& s : scenes_) refs += s.lua_ref >= 0; | |
| return {world_.alive(), physics_.body_count(), uint32_t(const_cast<AnimationSystem&>(anim_).live_players()), mixer_.active_voices(), | |
| compute_.live_buffers(), compute_.live_kernels(), assets_->live(), const_cast<Backend&>(backend_).render_device().live_textures(), refs}; | |
| } | |
| // ------------------------------------------------------------------------------ prefabs | |
| const TextureAsset* Engine::texture(const std::string& name) { | |
| AssetHandle h = assets_->find(name); | |
| Scene* s = current_scene(); | |
| if (!h) { | |
| h = assets_->load(AssetType::Texture, name); | |
| if (!h) return nullptr; | |
| if (s) s->assets.push_back(h); else assets_->acquire(h); | |
| } else if (s && std::find(s->assets.begin(), s->assets.end(), h) == s->assets.end()) { | |
| assets_->acquire(h); | |
| s->assets.push_back(h); | |
| } | |
| return &assets_->get(h)->texture; | |
| } | |
| static float field_num(lua_State* L, int t, const char* k, float def) { | |
| lua_getfield(L, t, k); | |
| float v = lua_isnumber(L, -1) ? float(lua_tonumber(L, -1)) : def; | |
| lua_pop(L, 1); | |
| return v; | |
| } | |
| static std::string field_str(lua_State* L, int t, const char* k, const std::string& def = {}) { | |
| lua_getfield(L, t, k); | |
| std::string v = lua_isstring(L, -1) ? lua_tostring(L, -1) : def; | |
| lua_pop(L, 1); | |
| return v; | |
| } | |
| static bool field_bool(lua_State* L, int t, const char* k, bool def) { | |
| lua_getfield(L, t, k); | |
| bool v = lua_isboolean(L, -1) ? lua_toboolean(L, -1) : def; | |
| lua_pop(L, 1); | |
| return v; | |
| } | |
| const PrefabDef* Engine::prefab(const std::string& name, std::string& error) { | |
| auto it = prefabs_.find(name); | |
| if (it != prefabs_.end()) return &it->second; | |
| AssetHandle h = assets_->load(AssetType::Prefab, name); | |
| if (!h) { error = "prefab '" + name + "' not found"; return nullptr; } | |
| std::string text = assets_->get(h)->text, path = assets_->get(h)->path; | |
| assets_->release(h); // parsed into a PrefabDef; the source text is not kept | |
| if (!lua_->run(text, path, 1)) { error = lua_->last_error(); return nullptr; } | |
| lua_State* L = lua_->L(); | |
| int t = lua_gettop(L); | |
| if (!lua_istable(L, t)) { lua_pop(L, 1); error = "prefab must return a table"; return nullptr; } | |
| PrefabDef d; | |
| d.name = field_str(L, t, "name", name); | |
| d.animation = field_str(L, t, "animation"); | |
| d.tags = uint32_t(field_num(L, t, "tags", 0)); | |
| lua_getfield(L, t, "sprite"); | |
| if (lua_istable(L, -1)) { | |
| int st = lua_gettop(L); | |
| d.has_sprite = true; | |
| d.texture = field_str(L, st, "texture"); | |
| d.region = field_str(L, st, "region"); | |
| d.sprite_size = {field_num(L, st, "w", 1), field_num(L, st, "h", 1)}; | |
| d.layer = int16_t(field_num(L, st, "layer", 0)); | |
| d.depth = field_num(L, st, "depth", 0); | |
| lua_getfield(L, st, "tint"); | |
| if (lua_istable(L, -1)) { | |
| int tt = lua_gettop(L); | |
| d.tint = {uint8_t(field_num(L, tt, "r", 255)), uint8_t(field_num(L, tt, "g", 255)), uint8_t(field_num(L, tt, "b", 255)), uint8_t(field_num(L, tt, "a", 255))}; | |
| } | |
| lua_pop(L, 1); | |
| } | |
| lua_pop(L, 1); | |
| lua_getfield(L, t, "body"); | |
| if (lua_istable(L, -1)) { | |
| int bt = lua_gettop(L); | |
| d.has_body = true; | |
| BodyDef& b = d.body; | |
| std::string type = field_str(L, bt, "type", "dynamic"); | |
| b.type = type == "static" ? BodyType::Static : type == "kinematic" ? BodyType::Kinematic : BodyType::Dynamic; | |
| std::string shape = field_str(L, bt, "shape", "aabb"); | |
| float w = field_num(L, bt, "w", 1), hh = field_num(L, bt, "h", 1); | |
| if (shape == "circle") b.shape = Shape::circle(field_num(L, bt, "r", 0.5f)); | |
| else if (shape == "box") b.shape = Shape::box(w / 2, hh / 2); | |
| else if (shape == "polygon") { | |
| Vec2 pts[phys::kMaxPolygonVertices]; | |
| int n = 0; | |
| lua_getfield(L, bt, "points"); | |
| if (lua_istable(L, -1)) { | |
| for (int i = 1; i <= int(lua_rawlen(L, -1)) && n < phys::kMaxPolygonVertices; i++) { | |
| lua_rawgeti(L, -1, i); | |
| int pt = lua_gettop(L); | |
| lua_rawgeti(L, pt, 1); lua_rawgeti(L, pt, 2); | |
| pts[n++] = {float(lua_tonumber(L, -2)), float(lua_tonumber(L, -1))}; | |
| lua_pop(L, 3); | |
| } | |
| } | |
| lua_pop(L, 1); | |
| b.shape = Shape::polygon(pts, n); | |
| } else b.shape = Shape::aabb(w / 2, hh / 2); | |
| b.density = field_num(L, bt, "density", 1); | |
| b.mass = field_num(L, bt, "mass", 0); | |
| b.friction = field_num(L, bt, "friction", 0.6f); | |
| b.restitution = field_num(L, bt, "restitution", 0); | |
| b.gravity_scale = field_num(L, bt, "gravity_scale", 1); | |
| b.linear_damping = field_num(L, bt, "linear_damping", 0); | |
| b.fixed_rotation = field_bool(L, bt, "fixed_rotation", false); | |
| b.bullet = field_bool(L, bt, "bullet", false); | |
| b.sensor = field_bool(L, bt, "sensor", false); | |
| b.can_sleep = field_bool(L, bt, "can_sleep", true); | |
| b.category = uint32_t(field_num(L, bt, "category", 1)); | |
| b.mask = uint32_t(field_num(L, bt, "mask", 65535)); | |
| } | |
| lua_pop(L, 2); | |
| return &(prefabs_[name] = d); | |
| } | |
| EntityHandle Engine::instantiate(const std::string& name, Vec2 pos, bool persistent, std::string& error) { | |
| const PrefabDef* d = prefab(name, error); | |
| if (!d) return {}; | |
| Scene* s = current_scene(); | |
| EntityHandle e = world_.create(d->name, persistent || !s ? 0 : s->id); | |
| world_.info(e)->prefab = name; | |
| world_.info(e)->tags = d->tags; | |
| TransformC* t = world_.transforms.get(e); | |
| t->current.position = t->previous.position = pos; | |
| if (d->has_sprite) { | |
| SpriteC sp; | |
| if (!d->texture.empty()) { | |
| const TextureAsset* tex = texture(d->texture); | |
| if (!tex) { world_.destroy(e); error = "texture '" + d->texture + "' not found"; return {}; } | |
| sp.texture = assets_->find(d->texture).bits; | |
| int r = d->region.empty() ? 0 : tex->atlas.find(d->region); | |
| sp.atlas_region = r < 0 ? 0 : uint32_t(r); | |
| } | |
| sp.size = d->sprite_size; sp.layer = d->layer; sp.depth = d->depth; sp.tint = d->tint; | |
| world_.sprites.add(e, sp); | |
| } | |
| if (d->has_body) { | |
| BodyDef b = d->body; | |
| b.position = pos; | |
| b.user = e.bits; | |
| world_.bodies.add(e, BodyC{physics_.create_body(b).bits}); | |
| } | |
| if (!d->animation.empty()) world_.animators.add(e, AnimatorC{anim_.create_player(d->animation)}); | |
| return e; | |
| } | |
| bool Engine::destroy_entity(EntityHandle e) { return world_.destroy(e); } | |
| // ------------------------------------------------------------------------------ input injection | |
| uint64_t Engine::next_tick_input_ns() const { | |
| return sim_origin_ns_ + uint64_t(sim_.tick) * 1000000000ull / sim_.ticks_per_second + 1; | |
| } | |
| void Engine::inject(uint16_t code, bool down) { | |
| InputEvent e; | |
| e.timestamp_ns = next_tick_input_ns(); | |
| e.type = down ? InputEventType::ButtonDown : InputEventType::ButtonUp; | |
| e.code = code; | |
| input_.push(e); | |
| } | |
| // ------------------------------------------------------------------------------ save states | |
| namespace { | |
| void write_color(BinaryWriter& w, Color c) { w.u8(c.r); w.u8(c.g); w.u8(c.b); w.u8(c.a); } | |
| Color read_color(BinaryReader& r) { Color c; c.r = r.u8(); c.g = r.u8(); c.b = r.u8(); c.a = r.u8(); return c; } | |
| void write_transform(BinaryWriter& w, const Transform& t) { w.vec2(t.position); w.f32(t.rotation); w.vec2(t.scale); } | |
| Transform read_transform(BinaryReader& r) { Transform t; t.position = r.vec2(); t.rotation = r.f32(); t.scale = r.vec2(); return t; } | |
| } // namespace | |
| std::vector<uint8_t> Engine::save_state(bool include_audio) { | |
| BinaryWriter w; | |
| lua_State* L = lua_->L(); | |
| size_t sec; | |
| sec = w.begin_section("HEAD"); | |
| w.str("UNIFY-SAVE"); w.u32(kSaveVersion); w.str(kEngineVersion); w.u32(sim_.ticks_per_second); w.u64(config_.seed); | |
| w.end_section(sec); | |
| sec = w.begin_section("TIME"); | |
| w.u64(sim_.tick); | |
| w.u32(uint32_t(timers_.size())); | |
| for (auto& t : timers_) { w.u64(t.fire_tick); w.u64(t.interval_ticks); w.str(t.name); w.u16(t.scene); } | |
| w.end_section(sec); | |
| sec = w.begin_section("RNG "); | |
| for (int i = 0; i < 4; i++) w.u32(rng_.state()[i]); | |
| w.end_section(sec); | |
| sec = w.begin_section("SCEN"); | |
| w.u16(next_scene_id_); | |
| w.u32(uint32_t(scenes_.size())); | |
| for (auto& s : scenes_) { | |
| w.u16(s.id); w.str(s.name); | |
| w.u32(uint32_t(s.assets.size())); | |
| for (AssetHandle a : s.assets) { const AssetRecord* r = assets_->get(a); w.u8(uint8_t(r->type)); w.str(r->name); } | |
| w.u32(uint32_t(s.buffers.size())); for (auto b : s.buffers) w.u32(b.bits); | |
| w.u32(uint32_t(s.kernels.size())); for (auto k : s.kernels) w.u32(k.bits); | |
| } | |
| w.end_section(sec); | |
| sec = w.begin_section("ECS "); | |
| { | |
| auto es = world_.entity_pool().save(); | |
| w.u32(uint32_t(es.gens.size())); | |
| for (size_t i = 0; i < es.gens.size(); i++) { | |
| w.u8(es.gens[i]); w.u8(es.live[i]); | |
| if (es.live[i]) { const EntityInfo& in = world_.infos()[i]; w.str(in.name); w.str(in.prefab); w.u16(in.scene); w.u32(in.tags); } | |
| } | |
| w.u32(uint32_t(es.free.size())); | |
| for (auto f : es.free) w.u32(f); | |
| // Component pools in dense order (their order is part of the deterministic state). | |
| w.u32(uint32_t(world_.transforms.size())); | |
| for (size_t i = 0; i < world_.transforms.size(); i++) { | |
| w.u32(world_.transforms.owner(i).bits); | |
| write_transform(w, world_.transforms.at(i).current); | |
| write_transform(w, world_.transforms.at(i).previous); | |
| } | |
| w.u32(uint32_t(world_.sprites.size())); | |
| for (size_t i = 0; i < world_.sprites.size(); i++) { | |
| const SpriteC& s = world_.sprites.at(i); | |
| w.u32(world_.sprites.owner(i).bits); | |
| const AssetRecord* tex = assets_->get(AssetHandle{s.texture}); | |
| w.str(tex ? tex->name : ""); // assets by name: handles are not stable across runs | |
| w.u32(s.atlas_region); w.vec2(s.size); w.vec2(s.pivot); write_color(w, s.tint); | |
| w.u16(uint16_t(s.layer)); w.f32(s.depth); w.boolean(s.flip_x); w.boolean(s.visible); | |
| } | |
| w.u32(uint32_t(world_.bodies.size())); | |
| for (size_t i = 0; i < world_.bodies.size(); i++) { w.u32(world_.bodies.owner(i).bits); w.u32(world_.bodies.at(i).body); } | |
| w.u32(uint32_t(world_.animators.size())); | |
| for (size_t i = 0; i < world_.animators.size(); i++) { w.u32(world_.animators.owner(i).bits); w.u32(world_.animators.at(i).player); } | |
| } | |
| w.end_section(sec); | |
| sec = w.begin_section("PHYS"); physics_.serialize(w); w.end_section(sec); | |
| sec = w.begin_section("ANIM"); anim_.serialize(w); w.end_section(sec); | |
| sec = w.begin_section("INPT"); input_.serialize(w); w.end_section(sec); | |
| sec = w.begin_section("CAMR"); w.vec2(camera_.position); w.f32(camera_.zoom); w.f32(camera_.rotation); w.end_section(sec); | |
| sec = w.begin_section("COMP"); compute_.serialize(w); w.end_section(sec); | |
| // Lua: the global `game` table plus each scene's `state` field. Functions are code, not state: | |
| // they come back from the scene scripts themselves. | |
| sec = w.begin_section("LUA "); | |
| { | |
| std::string err; | |
| lua_getglobal(L, "game"); | |
| if (!lua_->serialize_value(-1, w, err)) UNIFY_LOG_ERROR("SAVE", "game table: %s", err.c_str()); | |
| lua_pop(L, 1); | |
| w.u32(uint32_t(scenes_.size())); | |
| for (auto& s : scenes_) { | |
| lua_rawgeti(L, LUA_REGISTRYINDEX, s.lua_ref); | |
| lua_getfield(L, -1, "state"); | |
| if (!lua_->serialize_value(-1, w, err)) UNIFY_LOG_ERROR("SAVE", "scene '%s' state: %s", s.name.c_str(), err.c_str()); | |
| lua_pop(L, 2); | |
| } | |
| } | |
| w.end_section(sec); | |
| if (include_audio) { | |
| sec = w.begin_section("AUDI"); | |
| auto voices = mixer_.snapshot(); | |
| w.u32(uint32_t(voices.size())); | |
| for (auto& v : voices) { | |
| uint16_t owner = 0; | |
| for (auto& s : scenes_) if (std::find(s.voices.begin(), s.voices.end(), v.handle) != s.voices.end()) owner = s.id; | |
| w.str(v.asset); w.boolean(v.is_stream); w.u8(uint8_t(v.bus)); w.f32(v.volume); w.f32(v.pan); w.f32(v.pitch); | |
| w.boolean(v.loop); w.boolean(v.paused); w.f64(v.position); w.u16(owner); | |
| } | |
| w.f32(mixer_.bus_volume(Bus::Master)); w.f32(mixer_.bus_volume(Bus::Sfx)); w.f32(mixer_.bus_volume(Bus::Music)); | |
| w.end_section(sec); | |
| } | |
| return w.take(); | |
| } | |
| bool Engine::load_state(const std::vector<uint8_t>& bytes, std::string& error) { | |
| std::map<std::string, BinaryReader> sections; | |
| try { | |
| BinaryReader all(bytes); | |
| char tag[5]; | |
| BinaryReader body(nullptr, 0); | |
| while (all.next_section(tag, body)) sections.emplace(tag, body); | |
| for (const char* need : {"HEAD", "TIME", "RNG ", "SCEN", "ECS ", "PHYS", "ANIM", "INPT", "CAMR", "COMP", "LUA "}) | |
| if (!sections.count(need)) { error = std::string("save state is missing section ") + need; return false; } | |
| BinaryReader& head = sections.at("HEAD"); | |
| if (head.str() != "UNIFY-SAVE") { error = "not a UNIFY save state"; return false; } | |
| uint32_t version = head.u32(); | |
| if (version > kSaveVersion) { error = "save state version " + std::to_string(version) + " is newer than this engine"; return false; } | |
| head.str(); | |
| if (head.u32() != sim_.ticks_per_second) { error = "save state was recorded at a different tick rate"; return false; } | |
| } catch (const SerializationError& e) { | |
| error = std::string("corrupt save state: ") + e.what(); | |
| return false; | |
| } | |
| lua_State* L = lua_->L(); | |
| try { | |
| // Keep every currently loaded asset alive across the swap so unchanged assets keep | |
| // their handles and are not reloaded from disk. | |
| std::vector<AssetHandle> keep; | |
| for (auto& s : scenes_) for (auto a : s.assets) { assets_->acquire(a); keep.push_back(a); } | |
| // Tear down the live world completely; the saved one replaces it. | |
| mixer_.stop_all(); | |
| for (auto& s : scenes_) { | |
| s.voices.clear(); s.buffers.clear(); s.kernels.clear(); // compute state is replaced wholesale below | |
| } | |
| world_.clear(); | |
| physics_.clear(); | |
| while (!scenes_.empty()) { unload_scene(scenes_.back()); scenes_.pop_back(); } | |
| BinaryReader& time = sections.at("TIME"); | |
| sim_.tick = time.u64(); | |
| std::vector<Timer> saved_timers; | |
| uint32_t nt = time.u32(); | |
| for (uint32_t i = 0; i < nt; i++) { Timer t; t.fire_tick = time.u64(); t.interval_ticks = time.u64(); t.name = time.str(); t.scene = time.u16(); saved_timers.push_back(t); } | |
| BinaryReader& rng = sections.at("RNG "); | |
| uint32_t st[4]; | |
| for (auto& x : st) x = rng.u32(); | |
| rng_.set_state(st); | |
| std::string cerr; | |
| if (!compute_.deserialize(sections.at("COMP"), cerr)) { error = cerr; return false; } | |
| BinaryReader& sc = sections.at("SCEN"); | |
| next_scene_id_ = sc.u16(); | |
| uint32_t ns = sc.u32(); | |
| for (uint32_t i = 0; i < ns; i++) { | |
| Scene s; | |
| s.id = sc.u16(); | |
| s.name = sc.str(); | |
| s.voices.reserve(64); | |
| scenes_.push_back(s); | |
| Scene& live = scenes_.back(); | |
| if (!load_scene_script(live, error)) return false; | |
| // Restore the scene's asset list before init(), so init() finds its assets already | |
| // owned instead of appending duplicates (which would change the saved state). | |
| uint32_t na = sc.u32(); | |
| for (uint32_t k = 0; k < na; k++) { | |
| AssetType type = AssetType(sc.u8()); | |
| std::string name = sc.str(); | |
| AssetHandle h = assets_->load(type, name); | |
| if (!h) { error = "asset '" + name + "' from the save state could not be loaded"; return false; } | |
| live.assets.push_back(h); | |
| } | |
| if (!call_scene("init")) { error = "scene init: " + lua_->last_error(); return false; } | |
| uint32_t nb = sc.u32(); for (uint32_t k = 0; k < nb; k++) live.buffers.push_back(BufferHandle{sc.u32()}); | |
| uint32_t nk = sc.u32(); for (uint32_t k = 0; k < nk; k++) live.kernels.push_back(KernelHandle{sc.u32()}); | |
| } | |
| for (auto a : keep) assets_->release(a); | |
| timers_ = saved_timers; | |
| BinaryReader& ecs = sections.at("ECS "); | |
| { | |
| HandlePool<EntityHandle>::State es; | |
| uint32_t n = ecs.u32(); | |
| es.gens.resize(n); es.live.resize(n); | |
| world_.infos().assign(n, EntityInfo{}); | |
| for (uint32_t i = 0; i < n; i++) { | |
| es.gens[i] = ecs.u8(); es.live[i] = ecs.u8(); | |
| if (!es.live[i]) continue; | |
| es.alive++; | |
| EntityInfo& in = world_.infos()[i]; | |
| in.name = ecs.str(); in.prefab = ecs.str(); in.scene = ecs.u16(); in.tags = ecs.u32(); | |
| } | |
| es.free.resize(ecs.u32()); | |
| for (auto& f : es.free) f = ecs.u32(); | |
| world_.entity_pool().load(es); | |
| uint32_t nt2 = ecs.u32(); | |
| for (uint32_t i = 0; i < nt2; i++) { | |
| EntityHandle e{ecs.u32()}; | |
| TransformC t; t.current = read_transform(ecs); t.previous = read_transform(ecs); | |
| world_.transforms.add(e, t); | |
| } | |
| uint32_t nsp = ecs.u32(); | |
| for (uint32_t i = 0; i < nsp; i++) { | |
| EntityHandle e{ecs.u32()}; | |
| SpriteC s; | |
| std::string tex = ecs.str(); | |
| s.texture = tex.empty() ? 0 : assets_->find(tex).bits; | |
| s.atlas_region = ecs.u32(); s.size = ecs.vec2(); s.pivot = ecs.vec2(); s.tint = read_color(ecs); | |
| s.layer = int16_t(ecs.u16()); s.depth = ecs.f32(); s.flip_x = ecs.boolean(); s.visible = ecs.boolean(); | |
| world_.sprites.add(e, s); | |
| } | |
| uint32_t nb = ecs.u32(); | |
| for (uint32_t i = 0; i < nb; i++) { EntityHandle e{ecs.u32()}; world_.bodies.add(e, BodyC{ecs.u32()}); } | |
| uint32_t na = ecs.u32(); | |
| for (uint32_t i = 0; i < na; i++) { EntityHandle e{ecs.u32()}; world_.animators.add(e, AnimatorC{ecs.u32()}); } | |
| } | |
| physics_.deserialize(sections.at("PHYS")); | |
| anim_.deserialize(sections.at("ANIM")); | |
| input_.deserialize(sections.at("INPT")); | |
| BinaryReader& cam = sections.at("CAMR"); | |
| camera_.position = cam.vec2(); camera_.zoom = cam.f32(); camera_.rotation = cam.f32(); | |
| BinaryReader& lr = sections.at("LUA "); | |
| std::string lerr; | |
| if (!lua_->deserialize_value(lr, lerr)) { error = "game table: " + lerr; return false; } | |
| lua_setglobal(L, "game"); | |
| uint32_t nls = lr.u32(); | |
| for (uint32_t i = 0; i < nls && i < scenes_.size(); i++) { | |
| lua_rawgeti(L, LUA_REGISTRYINDEX, scenes_[i].lua_ref); | |
| if (!lua_->deserialize_value(lr, lerr)) { lua_pop(L, 1); error = "scene state: " + lerr; return false; } | |
| lua_setfield(L, -2, "state"); | |
| lua_pop(L, 1); | |
| } | |
| if (sections.count("AUDI")) { | |
| BinaryReader& au = sections.at("AUDI"); | |
| uint32_t nv = au.u32(); | |
| for (uint32_t i = 0; i < nv; i++) { | |
| std::string asset = au.str(); | |
| bool is_stream = au.boolean(); | |
| PlayParams p; | |
| p.bus = Bus(au.u8()); p.volume = au.f32(); p.pan = au.f32(); p.pitch = au.f32(); p.loop = au.boolean(); | |
| bool paused = au.boolean(); | |
| double pos = au.f64(); | |
| uint16_t owner = au.u16(); | |
| AudioHandle h; | |
| if (is_stream) { | |
| auto src = assets_->open_stream(assets_->find(asset)); | |
| if (src) { src->seek(uint32_t(std::max(0.0, pos))); h = mixer_.stream(src, p); } | |
| } else if (const AssetRecord* r = assets_->get(assets_->find(asset))) { | |
| p.start = pos; | |
| h = mixer_.play(r->clip, p); | |
| } | |
| if (h && paused) mixer_.pause(h); | |
| for (auto& s : scenes_) if (s.id == owner && h) s.voices.push_back(h); | |
| } | |
| mixer_.set_bus_volume(Bus::Master, au.f32()); mixer_.set_bus_volume(Bus::Sfx, au.f32()); mixer_.set_bus_volume(Bus::Music, au.f32()); | |
| } | |
| } catch (const SerializationError& e) { | |
| error = std::string("corrupt save state: ") + e.what(); | |
| return false; | |
| } | |
| // Real time continues from the restored tick. | |
| // Same exact formula as tick boundaries (SimClock::tick_end_ns); a truncated per-tick | |
| // duration here would drift input windows by up to a tick after restoring late in a session. | |
| sim_origin_ns_ = backend_.now_ns() - sim_.tick * 1000000000ull / sim_.ticks_per_second; | |
| lua_->gc_collect(); | |
| return true; | |
| } | |
| uint64_t Engine::state_hash() { | |
| std::vector<uint8_t> s = save_state(false); | |
| return fnv1a(s.data(), s.size()); | |
| } | |
| bool Engine::save_to_file(const std::string& slot, std::string& error) { | |
| std::string path = assets_->path_for(AssetType::SaveData, "saves/" + slot); | |
| if (!write_file(path, save_state())) { error = "could not write " + path; return false; } | |
| return true; | |
| } | |
| bool Engine::load_from_file(const std::string& slot, std::string& error) { | |
| std::vector<uint8_t> data; | |
| std::string path = assets_->path_for(AssetType::SaveData, "saves/" + slot); | |
| if (!read_file(path, data)) { error = "no save at " + path; return false; } | |
| return load_state(data, error); | |
| } | |
| void Engine::process_pending_state_ops() { | |
| // Requests made during the tick run here, at the tick boundary, in request order: | |
| // state is never captured or replaced halfway through a simulation step. | |
| std::vector<std::pair<bool, std::string>> ops; | |
| ops.swap(pending_ops_); | |
| for (auto& [is_save, slot] : ops) { | |
| std::string err; | |
| if (is_save) { | |
| bool ok = true; | |
| if (slot.empty()) quick_slot_ = save_state(); | |
| else if (!(ok = save_to_file(slot, err))) UNIFY_LOG_ERROR("SAVE", "%s", err.c_str()); | |
| lua_pushboolean(lua_->L(), ok); | |
| lua_pushstring(lua_->L(), slot.c_str()); | |
| if (!call_scene("on_saved", 2)) report_script_error("scene on_saved"); | |
| } else { | |
| bool ok = slot.empty() ? (!quick_slot_.empty() && load_state(quick_slot_, err)) : load_from_file(slot, err); | |
| if (!ok) UNIFY_LOG_ERROR("SAVE", "load failed: %s", err.empty() ? "no quick save" : err.c_str()); | |
| lua_pushboolean(lua_->L(), ok); | |
| lua_pushstring(lua_->L(), slot.c_str()); | |
| if (!call_scene("on_loaded", 2)) report_script_error("scene on_loaded"); | |
| } | |
| } | |
| } | |
| } // namespace unify | |