#include "world.hpp" #include "idalloc.hpp" #include "animqueue.hpp" #include "gui.hpp" #include "traceback.hpp" #include void World::store_global_pointer(lua_State *L, World *v) { lua_pushstring(L, "world"); lua_pushlightuserdata(L, v); lua_rawset(L, LUA_REGISTRYINDEX); } World *World::fetch_global_pointer(lua_State *L) { lua_pushstring(L, "world"); lua_rawget(L, LUA_REGISTRYINDEX); World *result = (World *)lua_touserdata(L, -1); if (result == nullptr) { luaL_error(L, "No world pointer stored."); } lua_pop(L, 1); return result; } World::~World() { } World::World(util::WorldType wt) { // Master world model by default. world_type_ = wt; // Initialize the ID allocator in master mode. if (wt == util::WORLD_TYPE_MASTER) { id_global_pool_.init_master(); } else { id_global_pool_.init_synch(); } // Prepare to manipulate the lua state. LuaVar world; LuaStack LS(state(), world); // Put the world pointer into the lua registry. World::store_global_pointer(state(), this); // Clear the global GUI pointer. Gui::store_global_pointer(state(), nullptr); // Create the tangibles table in the registry. LS.rawset(LuaRegistry, "tangibles", LuaNewTable); // Initialize the SourceDB source_db_.init(state()); source_db_.rebuild(); // Do standalone initializations. if (world_type_ == util::WORLD_TYPE_STANDALONE) { // Load the lua source from disk then rebuild the environment. source_db_.update(); source_db_.rebuild(); // Run unit tests. source_db_.run_unittests(); } LS.result(); assert (stack_is_clear()); } Tangible::Tangible(World *w, int64_t id) : world_(w), anim_queue_(w->world_type_), id_player_pool_(&w->id_global_pool_) { plane_item_.set_id(id); plane_item_.track(&w->plane_map_); } void Tangible::update_plane_item() { const AnimStep &aqback = anim_queue_.back(); plane_item_.set_pos(aqback.plane(), aqback.xyz().x, aqback.xyz().y, aqback.xyz().z); } void Tangible::serialize(StreamBuffer *sb) { anim_queue_.serialize(sb); id_player_pool_.serialize(sb); } void Tangible::deserialize(StreamBuffer *sb) { anim_queue_.deserialize(sb); id_player_pool_.deserialize(sb); update_plane_item(); } Tangible *World::tangible_get(int64_t id) { auto iter = tangibles_.find(id); if (iter == tangibles_.end()) { return nullptr; } else { return iter->second.get(); } } std::vector World::tangible_get_all(const std::vector &ids) { std::vector result(ids.size()); for (int i = 0; i < int(ids.size()); i++) { result[i] = tangible_get(ids[i]); } return result; } Tangible *World::tangible_get(lua_State *L, int idx) { Tangible *result = nullptr; int top = lua_gettop(L); if (lua_istable(L, idx)) { lua_getmetatable(L, idx); if (lua_istable(L, -1)) { lua_pushstring(L, "id"); lua_rawget(L, -2); lua_Number id = lua_tonumber(L, -1); result = tangible_get(int64_t(id)); } } lua_settop(L, top); if (result == nullptr) { luaL_error(L, "parameter is not a tangible"); } return result; } void World::tangible_delete(lua_State *L, int64_t id) { LuaVar tangibles, database; LuaStack LS(L, tangibles, database); // Fetch the C++ side of the tangible. auto iter = tangibles_.find(id); if (iter == tangibles_.end()) { luaL_error(L, "Not a tangible: %lld", id); } // Fetch the lua side of the tangible. LS.rawget(tangibles, LuaRegistry, "tangibles"); LS.rawget(database, tangibles, id); assert(LS.istable(database)); // Clear out the database. LS.clearmetatable(database); LS.cleartable(database); // Remove the lua portion from the table. LS.rawset(tangibles, id, LuaNil); // Remove the C++ portion from the table. tangibles_.erase(iter); LS.result(); } util::IdVector World::get_near(int64_t player_id, float radius, bool exclude_nowhere) { Tangible *player = tangible_get(player_id); if (player == nullptr) { return IdVector(); } // Find out where's the center of the world. const AnimStep &aqback = player->anim_queue_.back(); if (exclude_nowhere && (aqback.plane() == "nowhere")) { return IdVector(); } return plane_map_.scan_radius(aqback.plane(), aqback.xyz().x, aqback.xyz().y, radius, player_id); } Tangible *World::tangible_make(lua_State *L, int64_t id, bool pushdb) { LuaVar tangibles, metatab; LuaRet database; LuaStack LS(L, tangibles, database, metatab); // Allocate an ID if we don't already have one. if (id == 0) id = id_global_pool_.alloc_id_for_thread(L); // Create the C++ part of the structure. std::unique_ptr &t = tangibles_[id]; assert (t == nullptr); t.reset(new Tangible(this, id)); // Create the tangible's database and metatable. LS.set(database, LuaNewTable); LS.set(metatab, LuaNewTable); LS.setmetatable(database, metatab); // Store the database into the tangibles table. LS.rawget(tangibles, LuaRegistry, "tangibles"); LS.rawset(tangibles, id, database); // Populate the database and metatable with initial stuff. LS.rawset(database, "inventory", LuaNewTable); LS.rawset(metatab, "id", id); LS.rawset(metatab, "threads", LuaNewTable); // LS.rawset(metatab, "__metatable", LuaNil); LS.result(); if (!pushdb) lua_pop(L, 1); return t.get(); } World::Redirects World::fetch_redirects() { World::Redirects result = std::move(redirects_); redirects_.clear(); return std::move(result); } int64_t World::create_login_actor() { Tangible *tan = tangible_make(state(), 0, true); LuaArg database; LuaVar classtab, mt; LuaStack LS(state(), database, classtab, mt); LS.makeclass(classtab, "login"); LS.getmetatable(mt, database); LS.rawset(mt, "__index", classtab); LS.result(); tan->configure_id_pool_for_actor(); assert(stack_is_clear()); return tan->id(); } void World::update_gui(int64_t actor_id, int64_t place_id, Gui *gui) { assert(stack_is_clear()); gui->clear(); lua_State *L = state(); LuaVar actor, place, ugui, func, tangibles, mt, index; LuaStack LS(L, actor, place, ugui, func, tangibles, mt, index); // Get the actor and place. LS.rawget(tangibles, LuaRegistry, "tangibles"); LS.rawget(actor, tangibles, actor_id); LS.rawget(place, tangibles, place_id); if (!LS.istable(actor) || !LS.istable(place)) { LS.result(); return; } // Get the interface closure. LS.getmetatable(mt, place); if (!LS.istable(mt)) { LS.result(); return; } LS.rawget(index, mt, "__index"); if (!LS.istable(index)) { LS.result(); return; } LS.rawget(func, index, "interface"); if (!LS.isfunction(func)) { LS.result(); return; } // Call the interface function. lua_pushvalue(L, func.index()); lua_pushvalue(L, actor.index()); lua_pushvalue(L, place.index()); Gui::store_global_pointer(L, gui); int status = traceback_pcall(L, 2, 0); Gui::store_global_pointer(L, nullptr); if (status != 0) { gui->clear(); std::cerr << lua_tostring(L, -1); LS.result(); return; } // And we're done. LS.result(); assert(stack_is_clear()); } void World::invoke(const Invocation &inv) { switch (inv.kind()) { case Invocation::KIND_PLAN: invoke_plan(inv.actor(), inv.place(), inv.action(), inv.data()); break; default: // Do nothing. Standard behavior for any invalid command is to // simply do nothing at all. Perhaps eventually we may add a flag // to the world model to indicate that we've detected an invalid // command, to allow us to close the connection to a client that // is misbehaving. break; } } void World::invoke_plan(int64_t actor_id, int64_t place_id, const std::string &action, const InvocationData &idata) { assert(stack_is_clear()); // Validate that the action is legal. Gui validation_gui; update_gui(actor_id, place_id, &validation_gui); if (!validation_gui.has_action(action)) { return; } // Get the actor and place. Make sure both exist. Tangible *tactor = tangible_get(actor_id); Tangible *tplace = tangible_get(place_id); if ((tactor == nullptr) || (tplace == nullptr)) { return; } // Get an ID batch for the thread, and take one for the thread itself. int64_t id_batch = tactor->id_player_pool_.get_batch(); int64_t tid = id_batch++; // Set up for Lua manipulation. lua_State *L = state(); LuaVar actor, place, func, tangibles, mt, index, actions, thread, threads, message, invdata; LuaStack LS(L, actor, place, func, tangibles, mt, index, actions, thread, threads, message, invdata); // Get the actor and place. LS.rawget(tangibles, LuaRegistry, "tangibles"); LS.rawget(actor, tangibles, actor_id); LS.rawget(place, tangibles, place_id); if (!LS.istable(actor) || !LS.istable(place)) { LS.result(); return; } // Get the action closure. LS.getmetatable(mt, place); if (!LS.istable(mt)) { LS.result(); return; } LS.rawget(index, mt, "__index"); if (!LS.istable(index)) { LS.result(); return; } LS.rawget(actions, index, "action"); if (!LS.istable(actions)) { LS.result(); return; } LS.rawget(func, actions, action); if (!LS.isfunction(func)) { LS.result(); return; } // Convert the InvocationData into a lua table. LS.newtable(invdata); for (const auto &p : idata) { LS.rawset(invdata, p.first, p.second); } // Create a new thread, set up function and parameters. lua_State *CO = LS.newthread(thread); lua_pushvalue(L, func.index()); lua_pushvalue(L, actor.index()); lua_pushvalue(L, place.index()); lua_pushvalue(L, invdata.index()); lua_xmove(L, CO, 4); // Store the thread into place's thread table. LS.rawget(threads, mt, "threads"); if (!LS.istable(threads)) { LS.result(); return; } LS.rawset(threads, tid, thread); LS.result(); // Push the thread's ID into the runnable thread queue, // then run the thread queue. thread_sched_.add(0, tid, place_id); run_scheduled_threads(0); assert(stack_is_clear()); } void World::run_scheduled_threads(int64_t clk) { assert(stack_is_clear()); lua_State *L = state(); LuaVar tangibles, place, mt, threads, thread; LuaStack LS(L, tangibles, place, mt, threads, thread); LS.rawget(tangibles, LuaRegistry, "tangibles"); while (thread_sched_.ready(clk)) { SchedEntry sched = thread_sched_.pop(); LS.rawget(place, tangibles, sched.place_id()); if (!LS.istable(place)) { continue; } LS.getmetatable(mt, place); if (!LS.istable(mt)) { continue; } LS.rawget(threads, mt, "threads"); if (!LS.istable(threads)) { continue; } LS.rawget(thread, threads, sched.thread_id()); if (!LS.isthread(thread)) { continue; } // Resume the coroutine. lua_State *CO = LS.ckthread(thread); int top = lua_gettop(CO); int status = lua_resume(CO, nullptr, (top > 0) ? (top - 1) : 0); // Three possible outcomes: finished, yielded, or errored. if (status == LUA_YIELD) { // When the wait statement yields, it yields the desired timestamp. if ((lua_gettop(CO) != 1) || (!lua_isnumber(CO, 1))) { std::cerr << "Thread yielded incorrectly. Killing it." << std::endl; LS.rawset(threads, sched.thread_id(), LuaNil); } else { lua_Number delay = lua_tonumber(CO, 1); lua_settop(CO, 0); std::cerr << "Thread wait = " << delay << std::endl; thread_sched_.add(sched.clock() + int64_t(delay), sched.thread_id(), sched.place_id()); std::cerr << "Added to schedule." << std::endl; } } else if (status == LUA_OK) { // Successfully ran to completion. Remove from thread table. std::cerr << "Thread ran to completion." << std::endl; LS.rawset(threads, sched.thread_id(), LuaNil); } else { // Generated an error. Add a traceback, print, and kill the coroutine. traceback_coroutine(CO); std::cerr << lua_tostring(CO, -1); LS.rawset(threads, sched.thread_id(), LuaNil); } } LS.result(); assert(stack_is_clear()); } void World::serialize(StreamBuffer *sb) { assert(stack_is_clear()); assert(redirects_.empty()); int64_t wc0 = sb->total_writes(); lua_snap_.serialize(sb); id_global_pool_.serialize(sb); thread_sched_.serialize(sb); sb->write_uint32(tangibles_.size()); for (const auto &p : tangibles_) { sb->write_int64(p.first); p.second->serialize(sb); } int64_t wc1 = sb->total_writes(); std::cerr << "World serialized to " << wc1-wc0 << " bytes." << std::endl; assert(stack_is_clear()); } void World::deserialize(StreamBuffer *sb) { assert(stack_is_clear()); redirects_.clear(); lua_snap_.deserialize(sb); id_global_pool_.deserialize(sb); thread_sched_.deserialize(sb); // Mark all tangibles for deletion by setting ID to zero. for (const auto &p : tangibles_) { p.second->plane_item_.set_id(0); } // Deserialize tangibles. size_t ntan = sb->read_uint32(); for (size_t i = 0; i < ntan; i++) { int64_t id = sb->read_int64(); std::unique_ptr &t = tangibles_[id]; if (t == nullptr) { t.reset(new Tangible(this, id)); } else { t->plane_item_.set_id(id); } t->deserialize(sb); } // Delete tangibles that didn't get deserialized. for (auto iter = tangibles_.begin(); iter != tangibles_.end(); ) { if (iter->second->plane_item_.id() == 0) { tangibles_.erase(iter++); } else { ++iter; } } assert(stack_is_clear()); } void World::snapshot() { snapshot_.clear(); serialize(&snapshot_); } void World::rollback() { assert(!snapshot_.at_eof()); deserialize(&snapshot_); } void World::difference_transmit(int64_t actor_id, World *master, StreamBuffer *sb) { StreamBuffer tsb; diff_actor_essentials(actor_id, master, &tsb); tsb.copy_into(sb); patch_actor_essentials(&tsb); diff_visible_animations(actor_id, master, &tsb); tsb.copy_into(sb); patch_visible_animations(&tsb); } void World::diff_actor_essentials(int64_t actor_id, World *master, StreamBuffer *sb) { Tangible *s_actor = tangible_get(actor_id); Tangible *m_actor = master->tangible_get(actor_id); assert(s_actor != nullptr); assert(m_actor != nullptr); sb->write_int64(actor_id); s_actor->id_player_pool_.diff(m_actor->id_player_pool_, sb); s_actor->anim_queue_.diff(m_actor->anim_queue_, sb); } void World::patch_actor_essentials(StreamBuffer *sb) { int64_t actor_id = sb->read_int64(); Tangible *s_actor = tangible_get(actor_id); s_actor->id_player_pool_.patch(sb); s_actor->anim_queue_.patch(sb); s_actor->update_plane_item(); } void World::diff_visible_animations(int64_t actor_id, World *master, StreamBuffer *sb) { // Get the list of tangibles visible in either model. util::IdVector visible = PlaneMap::sort_union_id_vectors( master->get_near(actor_id, 100.0, true), this->get_near(actor_id, 100.0, true)); // Some tangibles may be missing in the master, some may be missing in the sync. std::vector m_visible = tangible_get_all(visible); std::vector s_visible = tangible_get_all(visible); assert(m_visible.size() == s_visible.size()); // For each tangible missing in the synchronous model, send the // necessary information to create the tangible. sb->write_int32(0); int count_pos = sb->total_writes(); int count = 0; for (int i = 0; i < int(s_visible.size()); i++) { Tangible *mt = m_visible[i]; Tangible *st = s_visible[i]; if (st == nullptr) { count += 1; sb->write_int64(mt->id()); mt->anim_queue_.serialize(sb); } } sb->overwrite_int32(count_pos, count); // For each tangible present in the synchronous model that doesn't // exist in the master model, send command to delete it. sb->write_int32(0); count_pos = sb->total_writes(); count = 0; for (int i = 0; i < int(s_visible.size()); i++) { Tangible *mt = m_visible[i]; Tangible *st = s_visible[i]; if (mt == nullptr) { count += 1; sb->write_int64(st->id()); } } sb->overwrite_int32(count_pos, count); // For each tangible present in both models, compare // the animation queues. sb->write_int32(0); count_pos = sb->total_writes(); count = 0; for (int i = 0; i < int(s_visible.size()); i++) { Tangible *mt = m_visible[i]; Tangible *st = s_visible[i]; if ((mt != nullptr) && (st != nullptr)) { if (st->anim_queue_.need_patch(mt->anim_queue_)) { count++; sb->write_int64(st->id()); st->anim_queue_.diff(mt->anim_queue_, sb); } } } sb->overwrite_int32(count_pos, count); } void World::patch_visible_animations(StreamBuffer *sb) { // Receive create messages. int count = sb->read_int32(); for (int i = 0; i < count; i++) { int64_t id = sb->read_int64(); Tangible *t = tangible_make(state(), id, false); t->anim_queue_.deserialize(sb); t->update_plane_item(); } // Receive delete messages count = sb->read_int32(); for (int i = 0; i < count; i++) { int64_t id = sb->read_int64(); tangible_delete(state(), id); } // Receive update messages count = sb->read_int32(); for (int i = 0; i < count; i++) { int64_t id = sb->read_int64(); Tangible *t = tangible_get(id); assert(t != nullptr); t->anim_queue_.patch(sb); } } LuaDefine(tangible_animstate, "c") { LuaArg tanobj; LuaRet graphic, plane, x, y, z, facing; LuaStack LS(L, tanobj, graphic, plane, x, y, z, facing); World *w = World::fetch_global_pointer(L); Tangible *tan = w->tangible_get(L, tanobj.index()); const AnimStep &aqback = tan->anim_queue_.back(); LS.set(graphic, aqback.graphic()); LS.set(plane, aqback.plane()); LS.set(x, aqback.xyz().x); LS.set(y, aqback.xyz().y); LS.set(z, aqback.xyz().z); LS.set(facing, aqback.facing()); return LS.result(); } LuaDefine(tangible_animate, "c") { LuaArg tanobj, config; LuaStack LS(L, tanobj, config); World *w = World::fetch_global_pointer(L); Tangible *tan = w->tangible_get(L, tanobj.index()); int64_t id = w->id_global_pool_.alloc_id_for_thread(L); const AnimStep &prev = tan->anim_queue_.back(); AnimStep step; step.from_lua(L, config.index(), prev); if (step.action() == "") { luaL_error(L, "animation action must be specified"); } tan->anim_queue_.add(id, step); tan->update_plane_item(); return LS.result(); } LuaDefine(tangible_setclass, "c") { LuaArg tanobj, classname; LuaVar classtab, mt; LuaStack LS(L, tanobj, classname, classtab, mt); World *w = World::fetch_global_pointer(L); w->tangible_get(L, tanobj.index()); LS.getclass(classtab, classname); LS.getmetatable(mt, tanobj); LS.rawset(mt, "__index", classtab); return LS.result(); } LuaDefine(tangible_delete, "c") { LuaArg tanobj; LuaStack LS(L, tanobj); World *w = World::fetch_global_pointer(L); Tangible *tan = w->tangible_get(L, tanobj.index()); if (tan->is_an_actor()) { luaL_error(L, "Cannot delete a player using tangible.delete, use tangible.redirect instead."); return 0; } w->tangible_delete(L, tan->id()); return LS.result(); } LuaDefine(tangible_build, "c") { LuaArg config; LuaVar classname, classtab, mt; LuaRet database; LuaStack LS(L, config, classname, classtab, database, mt); LS.checktable(config); // Get the class of the new tangible. LS.rawget(classname, config, "class"); if (LS.isnil(classname)) { luaL_error(L, "must specify a class name"); } LS.getclass(classtab, classname); // Parse the initial animation step. AnimStep initstep, blank; initstep.from_lua(L, config.index(), blank); if (!initstep.has_xyz()) { luaL_error(L, "You must specify (X,Y,Z) for new tangible"); } if (!initstep.has_plane()) { luaL_error(L, "You must specify plane for new tangible"); } if (!initstep.has_graphic()) { luaL_error(L, "You must specify graphic for new tangible"); } // TODO: generate error if there's extra crap in the config table. World *w = World::fetch_global_pointer(L); Tangible *tan = w->tangible_make(L, 0, true); lua_replace(L, database.index()); // Update the class of the new tangible. LS.getmetatable(mt, database); LS.rawset(mt, "__index", classtab); // Update the animation queue and planemap of the new tangible. int64_t stepid = w->id_global_pool_.alloc_id_for_thread(L); tan->anim_queue_.add(stepid, initstep); tan->update_plane_item(); return LS.result(); } LuaDefine(tangible_get, "c") { LuaArg id; LuaVar tangibles; LuaRet database; LuaStack LS(L, id, tangibles, database); int64_t nid = LS.ckinteger(id); LS.rawget(tangibles, LuaRegistry, "tangibles"); LS.rawget(database, tangibles, id); if (!LS.istable(database)) { luaL_error(L, "Not a tangible ID: %d", nid); } return LS.result(); } LuaDefine(tangible_redirect, "c") { LuaArg actor1, actor2, bldz; LuaStack LS(L, actor1, actor2, bldz); World *w = World::fetch_global_pointer(L); bool bulldoze = LS.ckboolean(bldz); Tangible *tan1 = w->tangible_get(L, actor1.index()); if (!tan1->is_an_actor()) { luaL_error(L, "redirect source is not an actor"); } if (LS.isnil(actor2)) { w->redirects_[tan1->id()] = 0; } else { Tangible *tan2 = w->tangible_get(L, actor2.index()); tan2->configure_id_pool_for_actor(); w->redirects_[tan1->id()] = tan2->id(); } if (bulldoze) { w->tangible_delete(L, tan1->id()); } return LS.result(); } LuaDefine(world_wait, "f") { if ((lua_gettop(L) != 1) || (lua_type(L, -1) != LUA_TNUMBER)) { luaL_error(L, "Argument to wait must be a number."); } return lua_yield(L, 1); } LuaDefine(world_getregistry, "f") { lua_pushvalue(L, LUA_REGISTRYINDEX); return 1; }