Download unify/engine/runtime/bindings.cpp from Snapkitty/repoverse: direct link, hf CLI and curl.
- Browser
- Download file 31.5 kB
-
https://huggingface.co/Snapkitty/repoverse/resolve/main/unify/engine/runtime/bindings.cpp
- Command line
-
hf download hf://Snapkitty/repoverse/unify/engine/runtime/bindings.cpp
-
curl -L -o bindings.cpp https://huggingface.co/Snapkitty/repoverse/resolve/main/unify/engine/runtime/bindings.cpp
31.5 kB
| // The UNIFY Lua API. Gameplay scripts reach the engine only through these functions. | |
| // Objects cross the boundary as handles (entity, buffer, kernel, voice ids) and every | |
| // call validates its handle; Lua never receives an engine pointer. | |
| namespace unify { | |
| namespace { | |
| Engine& E(lua_State* L) { return *static_cast<Engine*>(lua_touserdata(L, lua_upvalueindex(1))); } | |
| EntityHandle check_entity(lua_State* L, int idx) { | |
| auto* p = static_cast<uint32_t*>(luaL_checkudata(L, idx, kEntityMeta)); | |
| EntityHandle e{*p}; | |
| if (!E(L).world().valid(e)) luaL_error(L, "stale entity handle (entity was destroyed)"); | |
| return e; | |
| } | |
| void push_entity(lua_State* L, EntityHandle e) { | |
| if (!e) { lua_pushnil(L); return; } | |
| *static_cast<uint32_t*>(lua_newuserdatauv(L, sizeof(uint32_t), 0)) = e.bits; | |
| luaL_setmetatable(L, kEntityMeta); | |
| } | |
| PhysicsBodyHandle body_of(lua_State* L, EntityHandle e) { | |
| BodyC* b = E(L).world().bodies.get(e); | |
| return b ? PhysicsBodyHandle{b->body} : PhysicsBodyHandle{}; | |
| } | |
| float num(lua_State* L, int i) { return float(luaL_checknumber(L, i)); } | |
| float opt(lua_State* L, int i, float d) { return float(luaL_optnumber(L, i, d)); } | |
| // Ensures the active scene holds a reference to an asset (so unloading the scene frees it). | |
| AssetHandle scene_asset(lua_State* L, AssetType type, const std::string& name) { | |
| Engine& e = E(L); | |
| AssetHandle h = e.assets().find(name); | |
| Scene* s = e.current_scene(); | |
| if (!h) { | |
| h = e.assets().load(type, name); | |
| if (!h) luaL_error(L, "%s asset '%s' could not be loaded", asset_type_name(type), name.c_str()); | |
| if (s) s->assets.push_back(h); else e.assets().acquire(h); | |
| } else if (s && std::find(s->assets.begin(), s->assets.end(), h) == s->assets.end()) { | |
| e.assets().acquire(h); | |
| s->assets.push_back(h); | |
| } | |
| return h; | |
| } | |
| // ------------------------------------------------------------------------------ entity | |
| int ent_set_position(lua_State* L) { | |
| EntityHandle e = check_entity(L, 1); | |
| Vec2 p{num(L, 2), num(L, 3)}; | |
| TransformC* t = E(L).world().transforms.get(e); | |
| t->current.position = p; | |
| if (lua_toboolean(L, 4)) t->previous.position = p; // teleport: no interpolation smear | |
| if (PhysicsBodyHandle b = body_of(L, e)) E(L).physics().set_transform(b, p, t->current.rotation); | |
| return 0; | |
| } | |
| int ent_position(lua_State* L) { | |
| EntityHandle e = check_entity(L, 1); | |
| Vec2 p = E(L).world().transforms.get(e)->current.position; | |
| lua_pushnumber(L, p.x); lua_pushnumber(L, p.y); | |
| return 2; | |
| } | |
| int ent_set_velocity(lua_State* L) { | |
| EntityHandle e = check_entity(L, 1); | |
| PhysicsBodyHandle b = body_of(L, e); | |
| if (!b) return luaL_error(L, "entity has no physics body"); | |
| E(L).physics().set_velocity(b, {num(L, 2), num(L, 3)}); | |
| return 0; | |
| } | |
| int ent_velocity(lua_State* L) { | |
| EntityHandle e = check_entity(L, 1); | |
| const Body* b = E(L).physics().get(body_of(L, e)); | |
| lua_pushnumber(L, b ? b->velocity.x : 0); | |
| lua_pushnumber(L, b ? b->velocity.y : 0); | |
| return 2; | |
| } | |
| int ent_apply_impulse(lua_State* L) { | |
| EntityHandle e = check_entity(L, 1); | |
| E(L).physics().apply_impulse(body_of(L, e), {num(L, 2), num(L, 3)}); | |
| return 0; | |
| } | |
| int ent_set_rotation(lua_State* L) { | |
| EntityHandle e = check_entity(L, 1); | |
| TransformC* t = E(L).world().transforms.get(e); | |
| t->current.rotation = num(L, 2); | |
| if (PhysicsBodyHandle b = body_of(L, e)) E(L).physics().set_transform(b, t->current.position, t->current.rotation); | |
| return 0; | |
| } | |
| int ent_rotation(lua_State* L) { lua_pushnumber(L, E(L).world().transforms.get(check_entity(L, 1))->current.rotation); return 1; } | |
| int ent_set_animation(lua_State* L) { | |
| EntityHandle e = check_entity(L, 1); | |
| AnimatorC* a = E(L).world().animators.get(e); | |
| const char* clip = luaL_checkstring(L, 2); | |
| if (!E(L).animation().clip(clip)) return luaL_error(L, "unknown animation '%s'", clip); | |
| if (!a) a = &E(L).world().animators.add(e, AnimatorC{E(L).animation().create_player(clip)}); | |
| E(L).animation().play(a->player, clip, opt(L, 3, 0), lua_toboolean(L, 4)); | |
| return 0; | |
| } | |
| int ent_animation(lua_State* L) { | |
| AnimatorC* a = E(L).world().animators.get(check_entity(L, 1)); | |
| AnimationPlayer* p = a ? E(L).animation().player(a->player) : nullptr; | |
| if (!p) { lua_pushnil(L); return 1; } | |
| lua_pushstring(L, p->clip.c_str()); | |
| lua_pushboolean(L, p->finished); | |
| return 2; | |
| } | |
| SpriteC& sprite_of(lua_State* L, EntityHandle e) { | |
| SpriteC* s = E(L).world().sprites.get(e); | |
| if (!s) luaL_error(L, "entity has no sprite"); | |
| return *s; | |
| } | |
| int ent_set_flip(lua_State* L) { sprite_of(L, check_entity(L, 1)).flip_x = lua_toboolean(L, 2); return 0; } | |
| int ent_set_visible(lua_State* L) { sprite_of(L, check_entity(L, 1)).visible = lua_toboolean(L, 2); return 0; } | |
| int ent_set_tint(lua_State* L) { | |
| SpriteC& s = sprite_of(L, check_entity(L, 1)); | |
| s.tint = {uint8_t(luaL_checkinteger(L, 2)), uint8_t(luaL_checkinteger(L, 3)), uint8_t(luaL_checkinteger(L, 4)), uint8_t(luaL_optinteger(L, 5, 255))}; | |
| return 0; | |
| } | |
| int ent_name(lua_State* L) { lua_pushstring(L, E(L).world().info(check_entity(L, 1))->name.c_str()); return 1; } | |
| int ent_valid(lua_State* L) { | |
| auto* p = static_cast<uint32_t*>(luaL_checkudata(L, 1, kEntityMeta)); | |
| lua_pushboolean(L, E(L).world().valid(EntityHandle{*p})); | |
| return 1; | |
| } | |
| int ent_id(lua_State* L) { lua_pushinteger(L, check_entity(L, 1).bits); return 1; } | |
| int ent_destroy(lua_State* L) { E(L).destroy_entity(check_entity(L, 1)); return 0; } | |
| int ent_eq(lua_State* L) { | |
| auto* a = static_cast<uint32_t*>(luaL_checkudata(L, 1, kEntityMeta)); | |
| auto* b = static_cast<uint32_t*>(luaL_checkudata(L, 2, kEntityMeta)); | |
| lua_pushboolean(L, *a == *b); | |
| return 1; | |
| } | |
| int ent_tostring(lua_State* L) { | |
| auto* p = static_cast<uint32_t*>(luaL_checkudata(L, 1, kEntityMeta)); | |
| EntityHandle e{*p}; | |
| const EntityInfo* in = E(L).world().info(e); | |
| lua_pushfstring(L, "Entity(%d:%d %s)", int(e.index()), int(e.generation()), in ? in->name.c_str() : "<destroyed>"); | |
| return 1; | |
| } | |
| // ------------------------------------------------------------------------------ world | |
| int world_spawn(lua_State* L) { | |
| std::string err; | |
| EntityHandle e = E(L).instantiate(luaL_checkstring(L, 2), {num(L, 3), num(L, 4)}, lua_toboolean(L, 5), err); | |
| if (!e) return luaL_error(L, "spawn failed: %s", err.c_str()); | |
| push_entity(L, e); | |
| return 1; | |
| } | |
| int world_destroy(lua_State* L) { lua_pushboolean(L, E(L).destroy_entity(check_entity(L, 2))); return 1; } | |
| int world_find(lua_State* L) { push_entity(L, E(L).world().find(luaL_checkstring(L, 2))); return 1; } | |
| int world_count(lua_State* L) { lua_pushinteger(L, E(L).world().alive()); return 1; } | |
| // ------------------------------------------------------------------------------ physics | |
| int phys_apply_force(lua_State* L) { | |
| PhysicsBodyHandle b = body_of(L, check_entity(L, 2)); | |
| if (!b) return luaL_error(L, "entity has no physics body"); | |
| E(L).physics().apply_force(b, {num(L, 3), num(L, 4)}); | |
| return 0; | |
| } | |
| int phys_apply_impulse(lua_State* L) { E(L).physics().apply_impulse(body_of(L, check_entity(L, 2)), {num(L, 3), num(L, 4)}); return 0; } | |
| int phys_raycast(lua_State* L) { | |
| uint32_t mask = uint32_t(luaL_optinteger(L, 6, 0xFFFF)); | |
| PhysicsBodyHandle ignore; | |
| if (lua_isuserdata(L, 7)) ignore = body_of(L, check_entity(L, 7)); | |
| RayHit hit; | |
| if (!E(L).physics().raycast({num(L, 2), num(L, 3)}, {num(L, 4), num(L, 5)}, mask, hit, ignore)) { lua_pushboolean(L, 0); return 1; } | |
| lua_pushboolean(L, 1); | |
| lua_pushnumber(L, hit.point.x); lua_pushnumber(L, hit.point.y); | |
| lua_pushnumber(L, hit.normal.x); lua_pushnumber(L, hit.normal.y); | |
| const Body* b = E(L).physics().get(hit.body); | |
| push_entity(L, b && E(L).world().valid(EntityHandle{b->user}) ? EntityHandle{b->user} : EntityHandle{}); | |
| return 6; | |
| } | |
| int phys_set_gravity(lua_State* L) { E(L).physics().gravity = {num(L, 2), num(L, 3)}; return 0; } | |
| int phys_add_static_box(lua_State* L) { | |
| // Static collision without a sprite (e.g. merged tile runs). Owned by the active scene. | |
| Engine& e = E(L); | |
| Scene* s = e.current_scene(); | |
| EntityHandle ent = e.world().create("static", s ? s->id : 0); | |
| Vec2 c{num(L, 2), num(L, 3)}; | |
| e.world().transforms.get(ent)->current.position = c; | |
| e.world().transforms.get(ent)->previous.position = c; | |
| BodyDef d; | |
| d.type = BodyType::Static; | |
| d.shape = Shape::aabb(num(L, 4) / 2, num(L, 5) / 2); | |
| d.position = c; | |
| d.friction = opt(L, 7, 0.8f); | |
| d.category = uint32_t(luaL_optinteger(L, 6, 1)); | |
| d.user = ent.bits; | |
| e.world().bodies.add(ent, BodyC{e.physics().create_body(d).bits}); | |
| push_entity(L, ent); | |
| return 1; | |
| } | |
| int phys_is_awake(lua_State* L) { | |
| const Body* b = E(L).physics().get(body_of(L, check_entity(L, 2))); | |
| lua_pushboolean(L, b && b->awake); | |
| return 1; | |
| } | |
| // ------------------------------------------------------------------------------ audio | |
| int audio_play(lua_State* L) { | |
| Engine& e = E(L); | |
| AssetHandle h = scene_asset(L, AssetType::Audio, luaL_checkstring(L, 2)); | |
| PlayParams p; | |
| p.bus = Bus::Sfx; p.volume = opt(L, 3, 1); p.pan = opt(L, 4, 0); p.pitch = opt(L, 5, 1); | |
| AudioHandle v = e.mixer().play(e.assets().get(h)->clip, p); | |
| if (v && e.current_scene()) e.current_scene()->voices.push_back(v); | |
| lua_pushinteger(L, v.bits); | |
| return 1; | |
| } | |
| int audio_stream(lua_State* L) { | |
| Engine& e = E(L); | |
| AssetHandle h = scene_asset(L, AssetType::Music, luaL_checkstring(L, 2)); | |
| auto src = e.assets().open_stream(h); | |
| if (!src) return luaL_error(L, "could not open stream '%s'", luaL_checkstring(L, 2)); | |
| PlayParams p; | |
| p.bus = Bus::Music; p.volume = opt(L, 3, 1); | |
| p.loop = lua_isnoneornil(L, 4) ? true : lua_toboolean(L, 4); | |
| p.fade_in = opt(L, 5, 0); | |
| AudioHandle v = e.mixer().stream(src, p); | |
| if (v && e.current_scene()) e.current_scene()->voices.push_back(v); | |
| lua_pushinteger(L, v.bits); | |
| return 1; | |
| } | |
| AudioHandle voice(lua_State* L, int i) { return AudioHandle{uint32_t(luaL_checkinteger(L, i))}; } | |
| int audio_stop(lua_State* L) { lua_pushboolean(L, E(L).mixer().stop(voice(L, 2), opt(L, 3, 0))); return 1; } | |
| int audio_pause(lua_State* L) { lua_pushboolean(L, E(L).mixer().pause(voice(L, 2))); return 1; } | |
| int audio_resume(lua_State* L) { lua_pushboolean(L, E(L).mixer().resume(voice(L, 2))); return 1; } | |
| int audio_fade(lua_State* L) { lua_pushboolean(L, E(L).mixer().fade(voice(L, 2), num(L, 3), num(L, 4))); return 1; } | |
| int audio_playing(lua_State* L) { lua_pushboolean(L, E(L).mixer().playing(voice(L, 2))); return 1; } | |
| int audio_set_bus_volume(lua_State* L) { | |
| std::string bus = luaL_checkstring(L, 2); | |
| Bus b = bus == "music" ? Bus::Music : bus == "sfx" ? Bus::Sfx : Bus::Master; | |
| E(L).mixer().set_bus_volume(b, num(L, 3)); | |
| return 0; | |
| } | |
| // ------------------------------------------------------------------------------ input | |
| int in_pressed(lua_State* L) { lua_pushboolean(L, E(L).input().is_pressed(luaL_checkstring(L, 2))); return 1; } | |
| int in_held(lua_State* L) { lua_pushboolean(L, E(L).input().is_held(luaL_checkstring(L, 2))); return 1; } | |
| int in_released(lua_State* L) { lua_pushboolean(L, E(L).input().is_released(luaL_checkstring(L, 2))); return 1; } | |
| int in_axis(lua_State* L) { lua_pushnumber(L, E(L).input().axis(luaL_checkstring(L, 2))); return 1; } | |
| int in_buffered(lua_State* L) { lua_pushboolean(L, E(L).input().buffered(luaL_checkstring(L, 2), uint32_t(luaL_optinteger(L, 3, 6)))); return 1; } | |
| int in_consume(lua_State* L) { E(L).input().consume_buffered(luaL_checkstring(L, 2)); return 0; } | |
| int in_pointer(lua_State* L) { Vec2 p = E(L).input().pointer(); lua_pushnumber(L, p.x); lua_pushnumber(L, p.y); return 2; } | |
| uint16_t key_code(const std::string& n) { | |
| static const std::map<std::string, uint16_t> keys = { | |
| {"space", key::Space}, {"left", key::Left}, {"right", key::Right}, {"up", key::Up}, {"down", key::Down}, | |
| {"e", key::E}, {"i", key::I}, {"a", key::A}, {"d", key::D}, {"w", key::W}, {"s", key::S}, {"r", key::R}, {"p", key::P}, | |
| {"enter", key::Enter}, {"escape", key::Escape}, {"f1", key::F1}, {"f2", key::F2}, {"f3", key::F3}, | |
| {"f5", key::F5}, {"f9", key::F9}, {"shift", key::LShift}, {"mouse_left", key::MouseLeft}, | |
| {"pad_a", key::PadA}, {"pad_b", key::PadB}, {"pad_x", key::PadX}, {"pad_y", key::PadY}, {"pad_start", key::PadStart}, | |
| {"pad_left", key::PadLeft}, {"pad_right", key::PadRight}, {"pad_up", key::PadUp}, {"pad_down", key::PadDown}, | |
| {"pad_left_x", key::PadAxisLeftX}, {"pad_left_y", key::PadAxisLeftY}, {"touch", key::Touch0}}; | |
| auto it = keys.find(n); | |
| return it == keys.end() ? 0 : it->second; | |
| } | |
| int in_bind(lua_State* L) { | |
| const char* action = luaL_checkstring(L, 2); | |
| for (int i = 3; i <= lua_gettop(L); i++) { | |
| uint16_t c = key_code(luaL_checkstring(L, i)); | |
| if (!c) return luaL_error(L, "unknown key '%s'", lua_tostring(L, i)); | |
| E(L).input().bind(action, c); | |
| } | |
| return 0; | |
| } | |
| int in_bind_axis(lua_State* L) { | |
| uint16_t neg = key_code(luaL_checkstring(L, 3)), pos = key_code(luaL_checkstring(L, 4)); | |
| uint16_t analog = lua_isstring(L, 5) ? key_code(lua_tostring(L, 5)) : 0; | |
| if (!neg || !pos) return luaL_error(L, "unknown key in axis binding"); | |
| E(L).input().bind_axis(luaL_checkstring(L, 2), neg, pos, analog); | |
| return 0; | |
| } | |
| // ------------------------------------------------------------------------------ camera / ui | |
| int cam_set(lua_State* L) { | |
| Camera& c = E(L).camera(); | |
| c.position = {num(L, 2), num(L, 3)}; | |
| if (!lua_isnoneornil(L, 4)) c.zoom = num(L, 4); | |
| return 0; | |
| } | |
| int cam_get(lua_State* L) { | |
| const Camera& c = E(L).camera(); | |
| lua_pushnumber(L, c.position.x); lua_pushnumber(L, c.position.y); lua_pushnumber(L, c.zoom); | |
| return 3; | |
| } | |
| void require_render(lua_State* L) { | |
| if (!E(L).in_render_phase()) luaL_error(L, "ui calls are only valid inside render()"); | |
| } | |
| Color color_args(lua_State* L, int i) { | |
| return {uint8_t(luaL_optinteger(L, i, 255)), uint8_t(luaL_optinteger(L, i + 1, 255)), uint8_t(luaL_optinteger(L, i + 2, 255)), uint8_t(luaL_optinteger(L, i + 3, 255))}; | |
| } | |
| int ui_text(lua_State* L) { | |
| require_render(L); | |
| E(L).font().draw(E(L).renderer(), luaL_checkstring(L, 2), {num(L, 3), num(L, 4)}, opt(L, 5, 2), color_args(L, 6), 1000); | |
| return 0; | |
| } | |
| int ui_rect(lua_State* L) { | |
| require_render(L); | |
| E(L).renderer().fill_screen_rect({num(L, 2), num(L, 3), num(L, 4), num(L, 5)}, color_args(L, 6), 999); | |
| return 0; | |
| } | |
| int ui_image(lua_State* L) { | |
| require_render(L); | |
| const char* name = luaL_checkstring(L, 2); | |
| const TextureAsset* tex = E(L).texture(name); | |
| if (!tex) return luaL_error(L, "image '%s' could not be loaded", name); | |
| E(L).renderer().screen_rect(tex->texture.bits, {0, 0, float(tex->width), float(tex->height)}, | |
| {num(L, 3), num(L, 4), num(L, 5), num(L, 6)}, color_args(L, 7), int16_t(luaL_optinteger(L, 11, 998))); | |
| return 0; | |
| } | |
| int ui_width(lua_State* L) { lua_pushnumber(L, E(L).font().width(luaL_checkstring(L, 2), opt(L, 3, 2))); return 1; } | |
| int ui_screen(lua_State* L) { lua_pushinteger(L, E(L).config().width); lua_pushinteger(L, E(L).config().height); return 2; } | |
| // ------------------------------------------------------------------------------ timer / scene / engine | |
| int timer_after(lua_State* L) { E(L).add_timer(luaL_checknumber(L, 1), false, luaL_checkstring(L, 2)); return 0; } | |
| int timer_every(lua_State* L) { E(L).add_timer(luaL_checknumber(L, 1), true, luaL_checkstring(L, 2)); return 0; } | |
| int scene_transition(lua_State* L) { | |
| // Deferred to the end of the gameplay stage: never tear down a scene from inside its own callback. | |
| E(L).request_transition(luaL_checkstring(L, 1)); | |
| return 0; | |
| } | |
| int scene_state(lua_State* L) { | |
| // The active scene's persistent state table (read-only inspection for tools and tests). | |
| const Scene* s = E(L).active_scene(); | |
| if (!s || s->lua_ref < 0) { lua_pushnil(L); return 1; } | |
| lua_rawgeti(L, LUA_REGISTRYINDEX, s->lua_ref); | |
| lua_getfield(L, -1, "state"); | |
| return 1; | |
| } | |
| int scene_current(lua_State* L) { const Scene* s = E(L).active_scene(); lua_pushstring(L, s ? s->name.c_str() : ""); return 1; } | |
| int engine_tick(lua_State* L) { lua_pushinteger(L, lua_Integer(E(L).sim_clock().tick)); return 1; } | |
| int engine_time(lua_State* L) { lua_pushnumber(L, E(L).sim_clock().seconds()); return 1; } | |
| int engine_quit(lua_State* L) { E(L).request_quit(); return 0; } | |
| int engine_hash(lua_State* L) { | |
| char buf[20]; | |
| std::snprintf(buf, sizeof buf, "%016llx", static_cast<unsigned long long>(E(L).state_hash())); | |
| lua_pushstring(L, buf); | |
| return 1; | |
| } | |
| int engine_stats(lua_State* L) { | |
| Engine& e = E(L); | |
| lua_createtable(L, 0, 6); | |
| lua_pushinteger(L, e.world().alive()); lua_setfield(L, -2, "entities"); | |
| lua_pushinteger(L, e.physics().stats.awake); lua_setfield(L, -2, "awake_bodies"); | |
| lua_pushinteger(L, lua_Integer(e.lua().bytes())); lua_setfield(L, -2, "lua_bytes"); | |
| lua_pushinteger(L, e.mixer().active_voices()); lua_setfield(L, -2, "voices"); | |
| lua_pushinteger(L, e.renderer().last_frame_stats().batches); lua_setfield(L, -2, "batches"); | |
| lua_pushnumber(L, e.render_clock().frame_ms); lua_setfield(L, -2, "frame_ms"); | |
| return 1; | |
| } | |
| int log_fn(lua_State* L) { | |
| std::string out; | |
| for (int i = 1; i <= lua_gettop(L); i++) { if (i > 1) out += " "; out += luaL_tolstring(L, i, nullptr); lua_pop(L, 1); } | |
| E(L).script_log(out); | |
| return 0; | |
| } | |
| int save_state_fn(lua_State* L) { E(L).request_save(luaL_optstring(L, 1, "")); lua_pushboolean(L, 1); return 1; } | |
| int load_state_fn(lua_State* L) { E(L).request_load(luaL_optstring(L, 1, "")); lua_pushboolean(L, 1); return 1; } | |
| // ------------------------------------------------------------------------------ gc | |
| int gc_step(lua_State* L) { E(L).lua().gc_step(); return 0; } | |
| int gc_budget(lua_State* L) { if (!lua_isnoneornil(L, 1)) E(L).lua().gc_budget(int(luaL_checkinteger(L, 1))); lua_pushinteger(L, E(L).lua().gc_budget_kb()); return 1; } | |
| int gc_pause(lua_State* L) { E(L).lua().gc_pause(); return 0; } | |
| int gc_resume(lua_State* L) { E(L).lua().gc_resume(); return 0; } | |
| int gc_collect(lua_State* L) { E(L).lua().gc_collect(); return 0; } | |
| int gc_bytes(lua_State* L) { lua_pushinteger(L, lua_Integer(E(L).lua().bytes())); return 1; } | |
| // ------------------------------------------------------------------------------ animation / map | |
| int anim_define(lua_State* L) { | |
| luaL_checktype(L, 1, LUA_TTABLE); | |
| AnimationClip c; | |
| lua_getfield(L, 1, "name"); c.name = luaL_checkstring(L, -1); lua_pop(L, 1); | |
| lua_getfield(L, 1, "mode"); | |
| std::string mode = lua_isstring(L, -1) ? lua_tostring(L, -1) : "loop"; | |
| lua_pop(L, 1); | |
| c.mode = mode == "once" ? PlayMode::Once : mode == "pingpong" ? PlayMode::PingPong : PlayMode::Loop; | |
| lua_getfield(L, 1, "texture"); | |
| const TextureAsset* tex = lua_isstring(L, -1) ? E(L).texture(lua_tostring(L, -1)) : nullptr; | |
| lua_pop(L, 1); | |
| lua_getfield(L, 1, "frames"); | |
| if (lua_istable(L, -1)) { | |
| if (!tex) return luaL_error(L, "animation '%s' has frames but no texture", c.name.c_str()); | |
| for (int i = 1; i <= int(lua_rawlen(L, -1)); i++) { | |
| lua_rawgeti(L, -1, i); | |
| lua_rawgeti(L, -1, 1); lua_rawgeti(L, -2, 2); | |
| const char* region = luaL_checkstring(L, -2); | |
| int r = tex->atlas.find(region); | |
| if (r < 0) return luaL_error(L, "animation '%s': unknown atlas region '%s'", c.name.c_str(), region); | |
| c.frames.push_back({uint32_t(r), float(luaL_checknumber(L, -1))}); | |
| lua_pop(L, 3); | |
| } | |
| } | |
| lua_pop(L, 1); | |
| lua_getfield(L, 1, "tracks"); | |
| if (lua_istable(L, -1)) { | |
| lua_pushnil(L); | |
| while (lua_next(L, -2)) { | |
| Track t; | |
| t.property = luaL_checkstring(L, -2); | |
| for (int i = 1; i <= int(lua_rawlen(L, -1)); i++) { | |
| lua_rawgeti(L, -1, i); | |
| lua_rawgeti(L, -1, 1); lua_rawgeti(L, -2, 2); | |
| t.keys.push_back({float(luaL_checknumber(L, -2)), float(luaL_checknumber(L, -1))}); | |
| lua_pop(L, 3); | |
| } | |
| std::sort(t.keys.begin(), t.keys.end(), [](const Keyframe& a, const Keyframe& b) { return a.time < b.time; }); | |
| c.tracks.push_back(t); | |
| lua_pop(L, 1); | |
| } | |
| } | |
| lua_pop(L, 1); | |
| E(L).animation().add_clip(c); | |
| return 0; | |
| } | |
| int map_load(lua_State* L) { | |
| AssetHandle h = scene_asset(L, AssetType::Map, luaL_checkstring(L, 1)); | |
| const TileMap& m = E(L).assets().get(h)->map; | |
| lua_createtable(L, 0, 4); | |
| lua_pushinteger(L, m.width); lua_setfield(L, -2, "width"); | |
| lua_pushinteger(L, m.height); lua_setfield(L, -2, "height"); | |
| lua_pushnumber(L, m.tile_size); lua_setfield(L, -2, "tile_size"); | |
| lua_createtable(L, m.height, 0); | |
| for (int i = 0; i < m.height; i++) { lua_pushstring(L, m.rows[size_t(i)].c_str()); lua_rawseti(L, -2, i + 1); } | |
| lua_setfield(L, -2, "rows"); | |
| return 1; | |
| } | |
| // ------------------------------------------------------------------------------ buffer / kernel | |
| LuaBuffer* check_buffer(lua_State* L, int i) { | |
| auto* b = static_cast<LuaBuffer*>(luaL_checkudata(L, i, kBufferMeta)); | |
| if (!E(L).compute().buffer_valid(BufferHandle{b->bits})) luaL_error(L, "stale buffer handle"); | |
| return b; | |
| } | |
| int buffer_create(lua_State* L) { | |
| lua_Integer n = luaL_checkinteger(L, 1); | |
| if (n <= 0 || n > (1 << 24)) return luaL_error(L, "buffer size out of range"); | |
| bool is_int = std::strcmp(luaL_optstring(L, 2, "float"), "int") == 0; | |
| BufferHandle h = E(L).compute().buffer_create(uint32_t(n) * 4); | |
| if (!h) return luaL_error(L, "out of kernel memory"); | |
| if (Scene* s = E(L).current_scene()) s->buffers.push_back(h); | |
| auto* b = static_cast<LuaBuffer*>(lua_newuserdatauv(L, sizeof(LuaBuffer), 0)); | |
| *b = {h.bits, is_int}; | |
| luaL_setmetatable(L, kBufferMeta); | |
| return 1; | |
| } | |
| int buffer_upload(lua_State* L) { | |
| LuaBuffer* b = check_buffer(L, 1); | |
| luaL_checktype(L, 2, LUA_TTABLE); | |
| uint32_t offset = uint32_t(luaL_optinteger(L, 3, 0)); | |
| uint32_t n = uint32_t(lua_rawlen(L, 2)); | |
| uint32_t cap = E(L).compute().buffer_size(BufferHandle{b->bits}) / 4; | |
| if (offset + n > cap) return luaL_error(L, "upload of %d elements at %d overflows buffer of %d", int(n), int(offset), int(cap)); | |
| std::vector<uint32_t> tmp(n); | |
| for (uint32_t i = 0; i < n; i++) { | |
| lua_rawgeti(L, 2, lua_Integer(i) + 1); | |
| if (b->is_int) { int32_t v = int32_t(lua_tointeger(L, -1)); std::memcpy(&tmp[i], &v, 4); } | |
| else { float v = float(lua_tonumber(L, -1)); std::memcpy(&tmp[i], &v, 4); } | |
| lua_pop(L, 1); | |
| } | |
| E(L).compute().buffer_upload(BufferHandle{b->bits}, tmp.data(), n * 4, offset * 4); | |
| return 0; | |
| } | |
| int buffer_download(lua_State* L) { | |
| LuaBuffer* b = check_buffer(L, 1); | |
| uint32_t cap = E(L).compute().buffer_size(BufferHandle{b->bits}) / 4; | |
| uint32_t n = uint32_t(luaL_optinteger(L, 2, cap)); | |
| if (n > cap) n = cap; | |
| std::vector<uint32_t> tmp(n); | |
| E(L).compute().buffer_download(BufferHandle{b->bits}, tmp.data(), n * 4); | |
| lua_createtable(L, int(n), 0); | |
| for (uint32_t i = 0; i < n; i++) { | |
| if (b->is_int) { int32_t v; std::memcpy(&v, &tmp[i], 4); lua_pushinteger(L, v); } | |
| else { float v; std::memcpy(&v, &tmp[i], 4); lua_pushnumber(L, v); } | |
| lua_rawseti(L, -2, lua_Integer(i) + 1); | |
| } | |
| return 1; | |
| } | |
| int buffer_destroy(lua_State* L) { | |
| LuaBuffer* b = check_buffer(L, 1); | |
| E(L).compute().buffer_destroy(BufferHandle{b->bits}); | |
| if (Scene* s = E(L).current_scene()) s->buffers.erase(std::remove(s->buffers.begin(), s->buffers.end(), BufferHandle{b->bits}), s->buffers.end()); | |
| return 0; | |
| } | |
| int buffer_size(lua_State* L) { lua_pushinteger(L, E(L).compute().buffer_size(BufferHandle{check_buffer(L, 1)->bits}) / 4); return 1; } | |
| int kernel_compile(lua_State* L) { | |
| std::string src = luaL_checkstring(L, 1); | |
| if (src.find("__global__") == std::string::npos) { // an asset name, e.g. "kernels/particles" | |
| AssetHandle h = scene_asset(L, AssetType::Kernel, src); | |
| src = E(L).assets().get(h)->text; | |
| } | |
| kernel::CompiledKernel k = E(L).compute().compile(src); | |
| lua_createtable(L, 0, 6); | |
| lua_pushboolean(L, k.ok); lua_setfield(L, -2, "ok"); | |
| lua_pushstring(L, k.error.c_str()); lua_setfield(L, -2, "error"); | |
| lua_pushstring(L, k.name.c_str()); lua_setfield(L, -2, "name"); | |
| lua_pushstring(L, k.ir_text.c_str()); lua_setfield(L, -2, "ir"); | |
| lua_pushinteger(L, lua_Integer(k.wasm.size())); lua_setfield(L, -2, "wasm_bytes"); | |
| lua_pushstring(L, src.c_str()); lua_setfield(L, -2, "source"); | |
| return 1; | |
| } | |
| int kernel_load(lua_State* L) { | |
| luaL_checktype(L, 1, LUA_TTABLE); | |
| lua_getfield(L, 1, "source"); | |
| std::string src = luaL_checkstring(L, -1); | |
| lua_pop(L, 1); | |
| std::string err; | |
| KernelHandle h = E(L).compute().compile_and_load(src, err); | |
| if (!h) return luaL_error(L, "kernel load failed: %s", err.c_str()); | |
| if (Scene* s = E(L).current_scene()) s->kernels.push_back(h); | |
| *static_cast<uint32_t*>(lua_newuserdatauv(L, sizeof(uint32_t), 0)) = h.bits; | |
| luaL_setmetatable(L, kKernelMeta); | |
| return 1; | |
| } | |
| int kernel_dispatch(lua_State* L) { | |
| KernelHandle k{*static_cast<uint32_t*>(luaL_checkudata(L, 1, kKernelMeta))}; | |
| if (!E(L).compute().kernel_valid(k)) return luaL_error(L, "stale kernel handle"); | |
| uint32_t grid = uint32_t(luaL_checkinteger(L, 2)), block = uint32_t(luaL_checkinteger(L, 3)); | |
| std::vector<KernelArg> args; | |
| for (int i = 4; i <= lua_gettop(L); i++) { | |
| if (luaL_testudata(L, i, kBufferMeta)) args.push_back(KernelArg::buf(BufferHandle{check_buffer(L, i)->bits})); | |
| else if (lua_isinteger(L, i)) args.push_back(KernelArg::integer(int32_t(lua_tointeger(L, i)))); | |
| else args.push_back(KernelArg::real(float(luaL_checknumber(L, i)))); | |
| } | |
| DispatchResult r = E(L).compute().dispatch(k, grid, block, args); | |
| if (!r.ok) return luaL_error(L, "kernel dispatch failed: %s", r.error.c_str()); | |
| lua_pushinteger(L, lua_Integer(r.instructions)); | |
| return 1; | |
| } | |
| void make_lib(lua_State* L, Engine& e, const char* global, const luaL_Reg* fns) { | |
| lua_newtable(L); | |
| lua_pushlightuserdata(L, &e); | |
| luaL_setfuncs(L, fns, 1); | |
| lua_setglobal(L, global); | |
| } | |
| } // namespace | |
| void register_bindings(Engine& engine) { | |
| lua_State* L = engine.lua().L(); | |
| luaL_newmetatable(L, kEntityMeta); | |
| static const luaL_Reg entity_methods[] = { | |
| {"set_position", ent_set_position}, {"position", ent_position}, {"set_velocity", ent_set_velocity}, {"velocity", ent_velocity}, | |
| {"apply_impulse", ent_apply_impulse}, {"set_rotation", ent_set_rotation}, {"rotation", ent_rotation}, | |
| {"set_animation", ent_set_animation}, {"animation", ent_animation}, {"set_flip", ent_set_flip}, {"set_visible", ent_set_visible}, | |
| {"set_tint", ent_set_tint}, {"name", ent_name}, {"valid", ent_valid}, {"id", ent_id}, {"destroy", ent_destroy}, {nullptr, nullptr}}; | |
| lua_newtable(L); | |
| lua_pushlightuserdata(L, &engine); | |
| luaL_setfuncs(L, entity_methods, 1); | |
| lua_setfield(L, -2, "__index"); | |
| lua_pushlightuserdata(L, &engine); lua_pushcclosure(L, ent_eq, 1); lua_setfield(L, -2, "__eq"); | |
| lua_pushlightuserdata(L, &engine); lua_pushcclosure(L, ent_tostring, 1); lua_setfield(L, -2, "__tostring"); | |
| lua_pop(L, 1); | |
| luaL_newmetatable(L, kBufferMeta); lua_pop(L, 1); | |
| luaL_newmetatable(L, kKernelMeta); lua_pop(L, 1); | |
| static const luaL_Reg world_fns[] = {{"spawn", world_spawn}, {"destroy", world_destroy}, {"find", world_find}, {"count", world_count}, {nullptr, nullptr}}; | |
| static const luaL_Reg physics_fns[] = {{"apply_force", phys_apply_force}, {"apply_impulse", phys_apply_impulse}, {"raycast", phys_raycast}, | |
| {"set_gravity", phys_set_gravity}, {"add_static_box", phys_add_static_box}, {"is_awake", phys_is_awake}, {nullptr, nullptr}}; | |
| static const luaL_Reg audio_fns[] = {{"play", audio_play}, {"stream", audio_stream}, {"stop", audio_stop}, {"pause", audio_pause}, {"resume", audio_resume}, | |
| {"fade", audio_fade}, {"playing", audio_playing}, {"set_bus_volume", audio_set_bus_volume}, {nullptr, nullptr}}; | |
| static const luaL_Reg input_fns[] = {{"is_pressed", in_pressed}, {"is_held", in_held}, {"is_released", in_released}, {"axis", in_axis}, | |
| {"buffered", in_buffered}, {"consume", in_consume}, {"pointer", in_pointer}, {"bind", in_bind}, {"bind_axis", in_bind_axis}, {nullptr, nullptr}}; | |
| static const luaL_Reg camera_fns[] = {{"set", cam_set}, {"get", cam_get}, {nullptr, nullptr}}; | |
| static const luaL_Reg ui_fns[] = {{"text", ui_text}, {"image", ui_image}, {"rect", ui_rect}, {"width", ui_width}, {"screen", ui_screen}, {nullptr, nullptr}}; | |
| static const luaL_Reg timer_fns[] = {{"after", timer_after}, {"every", timer_every}, {nullptr, nullptr}}; | |
| static const luaL_Reg scene_fns[] = {{"transition", scene_transition}, {"current", scene_current}, {"state", scene_state}, {nullptr, nullptr}}; | |
| static const luaL_Reg engine_fns[] = {{"tick", engine_tick}, {"time", engine_time}, {"quit", engine_quit}, {"state_hash", engine_hash}, {"stats", engine_stats}, {nullptr, nullptr}}; | |
| static const luaL_Reg gc_fns[] = {{"step", gc_step}, {"budget", gc_budget}, {"pause", gc_pause}, {"resume", gc_resume}, {"collect", gc_collect}, {"bytes", gc_bytes}, {nullptr, nullptr}}; | |
| static const luaL_Reg anim_fns[] = {{"define", anim_define}, {nullptr, nullptr}}; | |
| static const luaL_Reg map_fns[] = {{"load", map_load}, {nullptr, nullptr}}; | |
| static const luaL_Reg buffer_fns[] = {{"create", buffer_create}, {"upload", buffer_upload}, {"download", buffer_download}, {"destroy", buffer_destroy}, {"size", buffer_size}, {nullptr, nullptr}}; | |
| static const luaL_Reg kernel_fns[] = {{"compile", kernel_compile}, {"load", kernel_load}, {"dispatch", kernel_dispatch}, {nullptr, nullptr}}; | |
| make_lib(L, engine, "world", world_fns); | |
| make_lib(L, engine, "physics", physics_fns); | |
| make_lib(L, engine, "audio", audio_fns); | |
| make_lib(L, engine, "input", input_fns); | |
| make_lib(L, engine, "camera", camera_fns); | |
| make_lib(L, engine, "ui", ui_fns); | |
| make_lib(L, engine, "timer", timer_fns); | |
| make_lib(L, engine, "scene", scene_fns); | |
| make_lib(L, engine, "engine", engine_fns); | |
| make_lib(L, engine, "gc", gc_fns); | |
| make_lib(L, engine, "animation", anim_fns); | |
| make_lib(L, engine, "map", map_fns); | |
| make_lib(L, engine, "buffer", buffer_fns); | |
| make_lib(L, engine, "kernel", kernel_fns); | |
| // gc_step() etc. also as globals, matching the engine-level API names. | |
| for (auto* r = gc_fns; r->name; r++) { | |
| lua_pushlightuserdata(L, &engine); | |
| lua_pushcclosure(L, r->func, 1); | |
| lua_setglobal(L, (std::string("gc_") + r->name).c_str()); | |
| } | |
| lua_pushlightuserdata(L, &engine); lua_pushcclosure(L, log_fn, 1); lua_setglobal(L, "log"); | |
| lua_pushlightuserdata(L, &engine); lua_pushcclosure(L, save_state_fn, 1); lua_setglobal(L, "save_state"); | |
| lua_pushlightuserdata(L, &engine); lua_pushcclosure(L, load_state_fn, 1); lua_setglobal(L, "load_state"); | |
| lua_newtable(L); | |
| lua_setglobal(L, "game"); // persistent gameplay state, serialized in save states | |
| } | |
| } // namespace unify | |