#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 || wt == util::WORLD_TYPE_STANDALONE) { id_global_pool_.init_master(); } else { id_global_pool_.init_synch(); } // Prepare to manipulate the lua state. LuaVar world, globtab; LuaStack LS(state(), world, globtab); // 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); // Set the tabletype of the registry. LS.settabletype(LuaRegistry, LUA_TT_REGISTRY); // Set the tabletype of the global environment. LS.getglobaltable(globtab); LS.settabletype(globtab, LUA_TT_GLOBALENV); // Create the tangibles table in the registry. LS.rawset(LuaRegistry, "tangibles", LuaNewTable); // Initialize the SourceDB. At this stage, the sourcedb is // empty, so it's just populating the lua builtins. source_db_.init(state()); source_db_.rebuild(); 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(); } } const Tangible *World::tangible_get(int64_t id) const { auto iter = tangibles_.find(id); if (iter == tangibles_.end()) { return nullptr; } else { return iter->second.get(); } } World::TanVector World::tangible_get_all(const IdVector &ids) const { TanVector result(ids.size()); for (int i = 0; i < int(ids.size()); i++) { result[i] = tangible_get(ids[i]); } return result; } Tangible *World::tangible_get(const LuaStack &LS, LuaSlot tab) { int64_t id = tangible_id(LS, tab); if (id == 0) { luaL_error(LS.state(), "parameter is not a tangible"); } Tangible *result = tangible_get(id); if (result == nullptr) { luaL_error(LS.state(), "parameter is not a tangible"); } return result; } int64_t World::tangible_id(const LuaStack &LS, LuaSlot tab) { int64_t id = 0; if (LS.istable(tab) && LS.gettabletype(tab) == LUA_TT_TANGIBLE) { lua_State *L = LS.state(); if (lua_getmetatable(L, tab.index())) { lua_pushstring(L, "id"); lua_rawget(L, -2); if (lua_type(L, -1) == LUA_TNUMBER) { id = lua_tointeger(L, -1); } lua_pop(L, 2); } } return id; } void World::tangible_delete(int64_t id) { lua_State *L = state(); LuaVar tangibles, database; LuaStack LS(L, tangibles, database); // Fetch the C++ side of the tangible. auto iter = tangibles_.find(id); if (iter == tangibles_.end()) { LS.result(); return; // Nothing to delete. } // 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); LS.settabletype(database, LUA_TT_DEADTANGIBLE); // Remove the lua portion from the tangibles table. LS.rawset(tangibles, id, LuaNil); // Remove the C++ portion from the tangibles table. tangibles_.erase(iter); LS.result(); } void World::tangible_walkto(int64_t id, int64_t animid, float x, float y) { Tangible *t = tangible_get(id); assert(animid != 0); assert(t != nullptr); AnimStep step; step.set_action("walkto"); step.set_x(x); step.set_y(y); t->anim_queue_.add(animid, step); } std::string World::tangible_anim_debug_string(int64_t id) const { const Tangible *t = tangible_get(id); if (t == 0) return "no such tangible"; return t->anim_queue_.steps_debug_string(); } std::string World::tangible_ids_debug_string() const { util::IdVector idv; for (const auto &pair : tangibles_) { idv.push_back(pair.first); } std::sort(idv.begin(), idv.end()); return util::id_vector_debug_string(idv); } std::string World::numbered_tables_debug_string() const { lua_State *L = state(); LuaVar ntmap, tab, tid; LuaStack LS(L, ntmap, tab, tid); std::vector result; std::ostringstream oss; // Fetch the numbered tables map. LS.rawget(ntmap, LuaRegistry, "ntmap"); // Iterate over the map. For each table, if it has // a TID, output that. Otherwise, output "unknown". for (int i = 1; i < 10000; i++) { LS.rawgeti(tab, ntmap, i); if (!LS.istable(tab)) break; LS.rawget(tid, tab, "TID"); if (LS.isstring(tid)) { result.push_back(LS.ckstring(tid)); } else { result.push_back("unknown"); } } LS.result(); std::sort(result.begin(), result.end()); for (const std::string &s : result) { oss << s << ";"; } return oss.str(); } std::string World::paired_tables_debug_string(lua_State *master) const { lua_State *synch = state(); LuaVar mntmap, sntmap, mtab, stab, mtid, stid; LuaStack MLS(master, mntmap, mtab, mtid); LuaStack SLS(synch, sntmap, stab, stid); std::vector> result; std::ostringstream oss; // Fetch the numbered tables map. MLS.rawget(mntmap, LuaRegistry, "ntmap"); SLS.rawget(sntmap, LuaRegistry, "ntmap"); int m_ntables = MLS.rawlen(mntmap); int s_ntables = MLS.rawlen(sntmap); assert(m_ntables == s_ntables); for (int i = 1; i <= m_ntables; i++) { MLS.rawget(mtab, mntmap, i); SLS.rawget(stab, sntmap, i); if (MLS.istable(mtab) && SLS.istable(stab)) { std::string mname = "unknown"; std::string sname = "unknown"; MLS.rawget(mtid, mtab, "TID"); if (MLS.isstring(mtid)) { mname = MLS.ckstring(mtid); } SLS.rawget(stid, stab, "TID"); if (SLS.isstring(stid)) { sname = SLS.ckstring(stid); } result.push_back(std::make_pair(mname, sname)); } } MLS.result(); SLS.result(); std::sort(result.begin(), result.end()); for (const auto &pair : result) { oss << pair.first << "=" << pair.second << ";"; } return oss.str(); } void World::tangible_set_string(int64_t id, const std::string &path, const std::string &value) { lua_State *L = state(); LuaVar tangibles, tab, subtab; LuaStack LS(L, tangibles, tab, subtab); // Fetch the lua side of the tangible. LS.rawget(tangibles, LuaRegistry, "tangibles"); LS.rawget(tab, tangibles, id); assert(LS.istable(tab)); // Split the path parts into the table names and final part. util::StringVec pathparts = util::split(path, '.'); assert(pathparts.size() >= 1); std::string finalpart = pathparts.back(); pathparts.pop_back(); // Create subtables as necessary. for (const std::string &subname : pathparts) { LS.rawget(subtab, tab, subname); if (LS.isnil(subtab)) { LS.set(subtab, LuaNewTable); LS.rawset(tab, subname, subtab); } assert(LS.istable(subtab)); LS.set(tab, subtab); } // Set the string value. LS.rawset(tab, finalpart, value); LS.result(); assert(stack_is_clear()); } void World::tangible_copy_global(int64_t id, const std::string &path, const std::string &global) { lua_State *L = state(); LuaVar tangibles, tab, subtab, globtab, value; LuaStack LS(L, tangibles, tab, subtab, globtab, value); // Fetch the lua side of the tangible. LS.rawget(tangibles, LuaRegistry, "tangibles"); LS.rawget(tab, tangibles, id); assert(LS.istable(tab)); // Split the path parts into the table names and final part. util::StringVec pathparts = util::split(path, '.'); assert(pathparts.size() >= 1); std::string finalpart = pathparts.back(); pathparts.pop_back(); // Create subtables as necessary. for (const std::string &subname : pathparts) { LS.rawget(subtab, tab, subname); if (LS.isnil(subtab)) { LS.set(subtab, LuaNewTable); LS.rawset(tab, subname, subtab); } assert(LS.istable(subtab)); LS.set(tab, subtab); } // Copy the global value. LS.getglobaltable(globtab); LS.rawget(value, globtab, global); LS.rawset(tab, finalpart, value); LS.result(); //assert(stack_is_clear()); } util::IdVector World::get_near_unsorted(int64_t player_id, float radius, bool exclude_nowhere) const { const 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); } util::IdVector World::get_near(int64_t player_id, float radius, bool exclude_nowhere) const { util::IdVector v = get_near_unsorted(player_id, radius, exclude_nowhere); std::sort(v.begin(), v.end()); return v; } Tangible *World::tangible_make(lua_State *L, int64_t id, bool pushdb) { // Get a state if we don't already have one. if (L == nullptr) { L = state(); assert(!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); // Mark the tangible using the tabletype field. LS.settabletype(database, LUA_TT_TANGIBLE); LS.settabletype(metatab, LUA_TT_TANGIBLEMETA); // 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; // Get the actor in both models. s_actor may be nil. const Tangible *s_actor = tangible_get(actor_id); const Tangible *m_actor = master->tangible_get(actor_id); assert(m_actor != nullptr); // Pass one: update the actor essentials. Create actor if doesn't exist. assert(tsb.at_eof()); diff_actor_essentials(m_actor, s_actor, &tsb); tsb.copy_into(sb); patch_actor_essentials(&tsb); // Get the list of tangibles visible in either model. // Some tangibles may be missing in the master, some may be missing in the sync. util::IdVector visible = util::sort_union_id_vectors( master->get_near_unsorted(actor_id, RadiusVisibility, true), get_near_unsorted(actor_id, RadiusVisibility, true)); TanVector m_visible = master->tangible_get_all(visible); TanVector s_visible = tangible_get_all(visible); assert(m_visible.size() == s_visible.size()); // Pass two: update visible animations. assert(tsb.at_eof()); diff_visible_animations(m_visible, s_visible, &tsb); tsb.copy_into(sb); patch_visible_animations(&tsb); // Copy the version number from master animqueue to synch animqueue. for (int i = 0; i < int(m_visible.size()); i++) { const Tangible *m_tan = m_visible[i]; if (m_tan != nullptr) { Tangible *s_tan = const_cast(s_visible[i]); if (s_tan == nullptr) { s_tan = tangible_get(m_tan->id()); assert(s_tan != nullptr); } s_tan->anim_queue_.update_version(m_tan->anim_queue_); } } // Obtain the list of tangibles whose tables we're going to transmit. util::IdVector closetans = get_near(actor_id, RadiusClose, true); assert(closetans == master->get_near(actor_id, RadiusClose, true)); util::HashValue closehash = util::hash_id_vector(closetans); // Confirm the close tangibles with the client. sb->write_hashvalue(closehash); // Number tables in both the master and the synchronous model. // Meanwhile, the client will number tables in the client-synchronous model. master->number_lua_tables(closetans); number_lua_tables(closetans); // Pair tables from the synchronous and master models. pair_lua_tables(closetans, master->state()); int ncreate = pair_new_tables(closetans, master->state()); sb->write_int32(ncreate); create_new_tables(ncreate); assert(tsb.at_eof()); } void World::apply_differences(StreamBuffer *sb) { int64_t actor_id = patch_actor_essentials(sb); patch_visible_animations(sb); util::IdVector closetans = get_near(actor_id, RadiusClose, true); util::HashValue closehash = util::hash_id_vector(closetans); util::HashValue m_closehash = sb->read_hashvalue(); assert(closehash == m_closehash); number_lua_tables(closetans); int ncreate = sb->read_int32(); create_new_tables(ncreate); } void World::diff_actor_essentials(const Tangible *m_actor, const Tangible *s_actor, StreamBuffer *sb) { sb->write_int64(m_actor->id()); if (s_actor == nullptr) { m_actor->id_player_pool_.serialize(sb); m_actor->anim_queue_.serialize(sb); } else { s_actor->id_player_pool_.diff(m_actor->id_player_pool_, sb); s_actor->anim_queue_.diff(m_actor->anim_queue_, sb); } } int64_t World::patch_actor_essentials(StreamBuffer *sb) { int64_t actor_id = sb->read_int64(); Tangible *s_actor = tangible_get(actor_id); if (s_actor == nullptr) { s_actor = tangible_make(nullptr, actor_id, false); s_actor->id_player_pool_.deserialize(sb); s_actor->anim_queue_.deserialize(sb); } else { s_actor->id_player_pool_.patch(sb); s_actor->anim_queue_.patch(sb); } s_actor->update_plane_item(); return actor_id; } void World::diff_visible_animations(const TanVector &mvis, const TanVector &svis, StreamBuffer *sb) { // 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(svis.size()); i++) { const Tangible *mt = mvis[i]; const Tangible *st = svis[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(svis.size()); i++) { const Tangible *mt = mvis[i]; const Tangible *st = svis[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(svis.size()); i++) { const Tangible *mt = mvis[i]; const Tangible *st = svis[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(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(LS, tanobj); 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(LS, tanobj); 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(LS, tanobj); 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(LS, tanobj); assert(tan != nullptr); // this should be checked above. if (tan->is_an_actor()) { luaL_error(L, "Cannot delete a player using tangible.delete, use tangible.redirect instead."); return 0; } w->tangible_delete(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(LS, actor1); 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(LS, actor2); tan2->configure_id_pool_for_actor(); w->redirects_[tan1->id()] = tan2->id(); } if (bulldoze) { w->tangible_delete(tan1->id()); } return LS.result(); } LuaDefine(tangible_id, "c") { LuaArg tanobj; LuaRet id; LuaStack LS(L, tanobj, id); LS.set(id, World::tangible_id(LS, tanobj)); 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; } LuaDefine(world_xtype, "f") { LuaArg tab; LuaRet rtype; LuaStack LS(L, tab, rtype); int xt = LS.xtype(tab); LS.set(rtype, xt); return LS.result(); } LuaDefine(world_settabletype, "f") { LuaArg tab, ttype; LuaStack LS(L, tab, ttype); if (!LS.istable(tab)) { luaL_error(L, "Not a table"); } int tt = LS.ckinteger(ttype); if ((tt < LUA_TT_GENERAL) || (tt > LUA_TT_CLASS)) { luaL_error(L, "table type out of range"); } LS.settabletype(tab, tt); return LS.result(); } static bool worlds_identical(const std::unique_ptr &w1, const std::unique_ptr &w2) { StreamBuffer sbw1, sbw2; w1->serialize(&sbw1); w2->serialize(&sbw2); return sbw1.contents_equal(&sbw2); } LuaDefine(unittests_world, "c") { std::unique_ptr m, ss, cs; StreamBuffer sb; int ncreate; // Test the numbering of lua tables. We create some general // tables using tangible_set_string. Then we install some // specialty tables (not numberable) using tangible_copy_global. // Finally, we number and dump the list of numbered tables. m.reset(new World(util::WORLD_TYPE_MASTER)); m->tangible_make(0, 123, false); m->tangible_set_string(123, "inventory.TID", "inventory"); m->tangible_set_string(123, "transactions.TID", "transactions"); m->tangible_set_string(123, "skills.TID", "skills"); m->tangible_set_string(123, "skills.leet.TID", "skills.leet"); m->tangible_set_string(123, "inventory.cplx.TID", "inventory.cplx"); m->tangible_copy_global(123, "math", "math"); m->tangible_copy_global(123, "gltab", "_G"); m->number_lua_tables(util::id_vector_create(123)); LuaAssertStrEq(L, m->numbered_tables_debug_string(), "inventory;inventory.cplx;skills;skills.leet;transactions;"); // Now we're going to create a synchronous model that's similar to, but not // exactly the same as that master model, and we're going to pair the tables. // Only some of these tables should pair: inventory, skills, and skills.leet ss.reset(new World(util::WORLD_TYPE_S_SYNC)); ss->tangible_make(0, 123, false); ss->tangible_set_string(123, "inventory.TID", "inventory"); ss->tangible_set_string(123, "skills.TID", "skills"); ss->tangible_set_string(123, "skills.crap.TID", "skills.crap"); ss->tangible_set_string(123, "skills.leet.TID", "skills.leet"); ss->tangible_set_string(123, "math.TID", "math"); ss->tangible_set_string(123, "gltab.TID", "gltab"); ss->number_lua_tables(util::id_vector_create(123)); LuaAssertStrEq(L, ss->numbered_tables_debug_string(), "gltab;inventory;math;skills;skills.crap;skills.leet;"); ss->pair_lua_tables(util::id_vector_create(123), m->state()); LuaAssertStrEq(L, ss->paired_tables_debug_string(m->state()), "inventory=inventory;skills=skills;skills.leet=skills.leet;"); // Test the creation of new tables during difference transmission. // The master world model above has two tables that couldn't be paired // to the client: inventory.cplx, and transactions. These two tables // should be paired to new, created tables. ncreate = m->pair_new_tables(util::id_vector_create(123), m->state()); LuaAssert(L, ncreate == 2); ss->create_new_tables(ncreate); LuaAssertStrEq(L, ss->paired_tables_debug_string(m->state()), "inventory=inventory;inventory.cplx=unknown;skills=skills;skills.leet=skills.leet;transactions=unknown;"); // Create new clean world models. m.reset(new World(util::WORLD_TYPE_MASTER)); ss.reset(new World(util::WORLD_TYPE_S_SYNC)); cs.reset(new World(util::WORLD_TYPE_C_SYNC)); // Create some tangibles, and add some animations. m->tangible_make(0, 123, false); m->tangible_make(0, 345, false); m->tangible_walkto(123, 770, 3, 4); m->tangible_walkto(345, 771, 6, 2); LuaAssertStrEq(L, m->tangible_ids_debug_string(), "123,345"); LuaAssertStrEq(L, m->tangible_anim_debug_string(123), "id=0 action= plane= x=0 y=0 z=0 facing=0 graphic=; " "id=770 action=walkto x=3 y=4; "); LuaAssertStrEq(L, m->tangible_anim_debug_string(345), "id=0 action= plane= x=0 y=0 z=0 facing=0 graphic=; " "id=771 action=walkto x=6 y=2; "); // Now difference transmit all that to the client. ss->difference_transmit(123, m.get(), &sb); cs->apply_differences(&sb); LuaAssertStrEq(L, ss->tangible_ids_debug_string(), "123,345"); LuaAssertStrEq(L, ss->tangible_anim_debug_string(123), "id=0 action= plane= x=0 y=0 z=0 facing=0 graphic=; " "id=770 action=walkto x=3 y=4; "); LuaAssertStrEq(L, ss->tangible_anim_debug_string(345), "id=0 action= plane= x=0 y=0 z=0 facing=0 graphic=; " "id=771 action=walkto x=6 y=2; "); LuaAssert(L, worlds_identical(ss, cs)); // Now add some more animation records to the master. m->tangible_walkto(123, 772, 7, 3); m->tangible_walkto(345, 773, 2, 5); LuaAssertStrEq(L, m->tangible_anim_debug_string(123), "id=0 action= plane= x=0 y=0 z=0 facing=0 graphic=; " "id=770 action=walkto x=3 y=4; " "id=772 action=walkto x=7 y=3; "); LuaAssertStrEq(L, m->tangible_anim_debug_string(345), "id=0 action= plane= x=0 y=0 z=0 facing=0 graphic=; " "id=771 action=walkto x=6 y=2; " "id=773 action=walkto x=2 y=5; "); // Now difference transmit all that to the client again. ss->difference_transmit(123, m.get(), &sb); cs->apply_differences(&sb); LuaAssertStrEq(L, ss->tangible_anim_debug_string(123), "id=0 action= plane= x=0 y=0 z=0 facing=0 graphic=; " "id=770 action=walkto x=3 y=4; " "id=772 action=walkto x=7 y=3; "); LuaAssertStrEq(L, ss->tangible_anim_debug_string(345), "id=0 action= plane= x=0 y=0 z=0 facing=0 graphic=; " "id=771 action=walkto x=6 y=2; " "id=773 action=walkto x=2 y=5; "); LuaAssert(L, worlds_identical(ss, cs)); // Delete tangible 345. m->tangible_delete(345); LuaAssertStrEq(L, m->tangible_ids_debug_string(), "123"); // And difference transmit ss->difference_transmit(123, m.get(), &sb); cs->apply_differences(&sb); LuaAssertStrEq(L, ss->tangible_ids_debug_string(), "123"); LuaAssert(L, worlds_identical(ss, cs)); return 0; }